package com.github.mustachejava.reflect;

import com.github.mustachejava.MustacheException;
import com.github.mustachejava.ObjectHandler;
import com.github.mustachejava.util.GuardException;
import com.github.mustachejava.util.Wrapper;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * Used for evaluating values at a callsite
 */
public class ReflectionWrapper extends GuardedWrapper {
  // Context
  protected final int scopeIndex;
  protected final Wrapper[] wrappers;
  protected final ObjectHandler oh;

  // Dispatch
  protected final Method method;
  protected final Field field;
  protected final Object[] arguments;

  public ReflectionWrapper(int scopeIndex, Wrapper[] wrappers, Guard[] guard, AccessibleObject method, Object[] arguments, ObjectHandler oh) {
    super(guard);
    this.wrappers = wrappers;
    this.oh = oh;
    if (method instanceof Field) {
      this.method = null;
      this.field = (Field) method;
    } else {
      this.method = (Method) method;
      this.field = null;
    }
    this.arguments = arguments;
    this.scopeIndex = scopeIndex;
  }

  public ReflectionWrapper(ReflectionWrapper rw) {
    this(rw.scopeIndex, rw.wrappers, rw.guards, rw.method == null ? rw.field : rw.method, rw.arguments, rw.oh);
  }

  protected Object unwrap(Object[] scopes) {
    if (wrappers == null || wrappers.length == 0) {
      return scopes[scopeIndex];
    } else {
      return ReflectionObjectHandler.unwrap(oh, scopeIndex, wrappers, scopes);
    }
  }

  @Override
  public Object call(Object[] scopes) throws GuardException {
    guardCall(scopes);
    Object scope = oh.coerce(unwrap(scopes));
    try {
      if (scope == null) return null;
      if (method == null) {
        return field.get(scope);
      } else {
        return method.invoke(scope, arguments);
      }
    } catch (IllegalArgumentException e) {
      throw new MustacheException("Error accessing " + getTargetDescription() + " on " + elementToString(scope)
          + ", scope: [" + elementsToString(scopes, scopeIndex) + "]" + ", guards: " + Arrays.toString(guards), e);
    } catch (IllegalAccessException e) {
      throw new MustacheException("Error accessing " + getTargetDescription() + " on " + elementToString(scope)
          + ", scope: [" + elementsToString(scopes, scopeIndex) + "]" + ", guards: " + Arrays.toString(guards), e);
    } catch (InvocationTargetException e) {
      throw new MustacheException("Error invoking " + getTargetDescription() + " on " + elementToString(scope), e.getTargetException());
    } catch (Exception e) {
      throw new MustacheException("Error invoking " + getTargetDescription() + " on " + elementToString(scope), e);
    }
  }

  public Method getMethod() {
    return method;
  }

  public Field getField() {
    return field;
  }

  public Object[] getArguments() {
    return arguments;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    if (field == null) {
      sb.append(method.toString());
      if (arguments != null) {
        for (Object arg : arguments) {
          sb.append(",").append(arg);
        }
      }
    } else {
      sb.append(field);
    }
    return sb.toString();
  }

  public Wrapper[] getWrappers() {
    return wrappers;
  }

  private String getTargetDescription() {
    return method == null
        ? "field " + field.getDeclaringClass() + "." + field.getName()
        : "method " + method.getDeclaringClass().getCanonicalName() + "." + method.getName() + "(" + elementsToString(arguments, method.getParameterTypes().length - 1) + ")";
  }
  
  private String elementsToString(Object[] objects, int showUpTo) {
    if (objects == null || objects.length == 0 || showUpTo < 0) {
      return "";
    }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i <= showUpTo && i < objects.length; i++) {
      if (sb.length() > 0)
        sb.append(",");
      sb.append(elementToString(objects[i]));
    }
    return sb.toString();
  }

  private String elementToString(Object object) {
    return object == null ? null : object.getClass().getCanonicalName() + '@' + object.hashCode();
  }
  
}
