/**
 * Copyright (c) 2012-2015 Edgar Espina
 *
 * This file is part of Handlebars.java.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jknack.handlebars.helper;

import static org.apache.commons.lang3.Validate.notNull;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;

/**
 * Wrap a method as Handlebars helper.
 *
 * @author edgar.espina
 * @see Handlebars#registerHelpers(Object)
 * @see Handlebars#registerHelpers(Class)
 */
public class MethodHelper implements Helper<Object> {

  /** No args. */
  private static final Object[] NO_ARGS = new Object[0];

  /**
   * The source or instance object. Might be null.
   */
  private Object source;

  /**
   * The method to invoke. Required.
   */
  private Method method;

  /**
   * Creates a new {@link MethodHelper}.
   *
   * @param method The method to invoke. Required.
   * @param source The source or instance object. Might be null.
   */
  public MethodHelper(final Method method, final Object source) {
    this.method = notNull(method, "A helper method is required.");
    this.source = source;
  }

  @Override
  public Object apply(final Object context, final Options options) throws IOException {
    try {
      Class<?>[] paramTypes = method.getParameterTypes();
      Object[] args = NO_ARGS;
      if (paramTypes.length > 0) {
        args = new Object[paramTypes.length];
        args[0] = paramTypes[0] == Options.class ? options : context;
        for (int i = 1; i < args.length; i++) {
          if (paramTypes[i] == Options.class) {
            args[i] = options;
          } else {
            args[i] = options.param(i - 1, null);
          }
        }
      }
      return method.invoke(source, args);
    } catch (ArrayIndexOutOfBoundsException ex) {
      throw new IllegalArgumentException(
          "could not execute helper: " + toString(method) + ", with the given arguments: "
              + toString(options.params),
          ex);
    } catch (InvocationTargetException ex) {
      throw launderThrowable(ex.getCause());
    } catch (IllegalAccessException ex) {
      throw new IllegalStateException("could not execute helper: " + toString(method), ex);
    }
  }

  /**
   * Describe params.
   *
   * @param params Params to describe.
   * @return ToString of params
   */
  private String toString(final Object[] params) {
    StringBuilder buff = new StringBuilder();
    buff.append("[");
    for (Object param : params) {
      buff.append(param == null ? "null" : param.getClass().getSimpleName()).append(", ");
    }
    if (buff.length() > 1) {
      buff.setLength(buff.length() - 2);
    }
    return buff.append("]").toString();
  }

  /**
   * Describes method.
   *
   * @param method Method to describe.
   * @return ToString of method.
   */
  private String toString(final Method method) {
    return method.getName() + "(" + toString(method.getParameterTypes()) + ")";
  }

  /**
   * Describe types.
   *
   * @param types Types to describe.
   * @return ToString of types.
   */
  private String toString(final Class<?>[] types) {
    StringBuilder buff = new StringBuilder();
    for (Class<?> type : types) {
      buff.append(type.getSimpleName()).append(", ");
    }
    if (buff.length() > 0) {
      buff.setLength(buff.length() - 2);
    }
    return buff.toString();
  }

  /**
   * Return a runtime exception or throw an {@link IOException}.
   *
   * @param cause The invocation cause.
   * @return A runtime exception or throw an {@link IOException}.
   * @throws IOException If the cause is an {@link IOException}.
   */
  private RuntimeException launderThrowable(final Throwable cause) throws IOException {
    if (cause instanceof RuntimeException) {
      return (RuntimeException) cause;
    }
    if (cause instanceof IOException) {
      throw (IOException) cause;
    }
    return new IllegalStateException("could not execute helper: " + method.getName(), cause);
  }

}
