/**
 * MVEL 2.0
 * Copyright (C) 2007 The Codehaus
 * Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
 *
 * 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 org.mvel2.optimizers.impl.refl;

import org.mvel2.CompileException;
import org.mvel2.MVEL;
import org.mvel2.OptimizationFailure;
import org.mvel2.ParserContext;
import org.mvel2.PropertyAccessException;
import org.mvel2.ast.FunctionInstance;
import org.mvel2.ast.TypeDescriptor;
import org.mvel2.compiler.Accessor;
import org.mvel2.compiler.AccessorNode;
import org.mvel2.compiler.ExecutableLiteral;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.compiler.PropertyVerifier;
import org.mvel2.integration.GlobalListenerFactory;
import org.mvel2.integration.PropertyHandler;
import org.mvel2.integration.VariableResolver;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.optimizers.AbstractOptimizer;
import org.mvel2.optimizers.AccessorOptimizer;
import org.mvel2.optimizers.impl.refl.collection.ArrayCreator;
import org.mvel2.optimizers.impl.refl.collection.ExprValueAccessor;
import org.mvel2.optimizers.impl.refl.collection.ListCreator;
import org.mvel2.optimizers.impl.refl.collection.MapCreator;
import org.mvel2.optimizers.impl.refl.nodes.ArrayAccessor;
import org.mvel2.optimizers.impl.refl.nodes.ArrayAccessorNest;
import org.mvel2.optimizers.impl.refl.nodes.ArrayLength;
import org.mvel2.optimizers.impl.refl.nodes.ConstructorAccessor;
import org.mvel2.optimizers.impl.refl.nodes.DynamicFieldAccessor;
import org.mvel2.optimizers.impl.refl.nodes.DynamicFunctionAccessor;
import org.mvel2.optimizers.impl.refl.nodes.FieldAccessor;
import org.mvel2.optimizers.impl.refl.nodes.FieldAccessorNH;
import org.mvel2.optimizers.impl.refl.nodes.FunctionAccessor;
import org.mvel2.optimizers.impl.refl.nodes.GetterAccessor;
import org.mvel2.optimizers.impl.refl.nodes.GetterAccessorNH;
import org.mvel2.optimizers.impl.refl.nodes.IndexedCharSeqAccessor;
import org.mvel2.optimizers.impl.refl.nodes.IndexedCharSeqAccessorNest;
import org.mvel2.optimizers.impl.refl.nodes.IndexedVariableAccessor;
import org.mvel2.optimizers.impl.refl.nodes.ListAccessor;
import org.mvel2.optimizers.impl.refl.nodes.ListAccessorNest;
import org.mvel2.optimizers.impl.refl.nodes.MapAccessor;
import org.mvel2.optimizers.impl.refl.nodes.MapAccessorNest;
import org.mvel2.optimizers.impl.refl.nodes.MethodAccessor;
import org.mvel2.optimizers.impl.refl.nodes.MethodAccessorNH;
import org.mvel2.optimizers.impl.refl.nodes.Notify;
import org.mvel2.optimizers.impl.refl.nodes.NullSafe;
import org.mvel2.optimizers.impl.refl.nodes.PropertyHandlerAccessor;
import org.mvel2.optimizers.impl.refl.nodes.SetterAccessor;
import org.mvel2.optimizers.impl.refl.nodes.StaticReferenceAccessor;
import org.mvel2.optimizers.impl.refl.nodes.StaticVarAccessor;
import org.mvel2.optimizers.impl.refl.nodes.StaticVarAccessorNH;
import org.mvel2.optimizers.impl.refl.nodes.ThisValueAccessor;
import org.mvel2.optimizers.impl.refl.nodes.Union;
import org.mvel2.optimizers.impl.refl.nodes.VariableAccessor;
import org.mvel2.optimizers.impl.refl.nodes.WithAccessor;
import org.mvel2.util.ArrayTools;
import org.mvel2.util.ErrorUtil;
import org.mvel2.util.MethodStub;
import org.mvel2.util.NullType;
import org.mvel2.util.ParseTools;
import org.mvel2.util.PropertyTools;
import org.mvel2.util.StringAppender;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.lang.Integer.parseInt;
import static java.lang.Thread.currentThread;
import static java.lang.reflect.Array.getLength;
import static org.mvel2.DataConversion.canConvert;
import static org.mvel2.DataConversion.convert;
import static org.mvel2.MVEL.eval;
import static org.mvel2.ast.TypeDescriptor.getClassReference;
import static org.mvel2.integration.GlobalListenerFactory.*;
import static org.mvel2.integration.PropertyHandlerFactory.*;
import static org.mvel2.util.CompilerTools.expectType;
import static org.mvel2.util.ParseTools.*;
import static org.mvel2.util.PropertyTools.getFieldOrAccessor;
import static org.mvel2.util.PropertyTools.getFieldOrWriteAccessor;
import static org.mvel2.util.ReflectionUtil.toNonPrimitiveType;
import static org.mvel2.util.Varargs.normalizeArgsForVarArgs;
import static org.mvel2.util.Varargs.paramTypeVarArgsSafe;

public class ReflectiveAccessorOptimizer extends AbstractOptimizer implements AccessorOptimizer {
  private static final Logger LOG = Logger.getLogger(ReflectiveAccessorOptimizer.class.getName());

  private AccessorNode rootNode;
  private AccessorNode currNode;

  private Object ctx;
  private Object thisRef;
  private Object val;

  private VariableResolverFactory variableFactory;

  private static final int DONE = -1;

  private static final Object[] EMPTYARG = new Object[0];
  private static final Class[] EMPTYCLS = new Class[0];

  private boolean first = true;

  private Class ingressType;
  private Class returnType;

  public ReflectiveAccessorOptimizer() {
  }

  public void init() {
  }

  private ReflectiveAccessorOptimizer(ParserContext pCtx, char[] property, int start, int offset, Object ctx,
                                      Object thisRef, VariableResolverFactory variableFactory) {
    super(pCtx);
    this.expr = property;
    this.start = start;
    this.length = property != null ? offset : start;
    this.end = start + length;
    this.ctx = ctx;
    this.variableFactory = variableFactory;
    this.thisRef = thisRef;
  }

  public Accessor optimizeAccessor(ParserContext pCtx, char[] property, int start, int offset, Object ctx, Object thisRef,
                                   VariableResolverFactory factory, boolean root, Class ingressType) {
    this.rootNode = this.currNode = null;
    this.expr = property;
    this.start = start;
    this.end = start + offset;
    this.length = end - start;


    this.first = true;
    this.ctx = ctx;
    this.thisRef = thisRef;
    this.variableFactory = factory;
    this.ingressType = ingressType;

    this.pCtx = pCtx;

    return compileGetChain();
  }

  public Accessor optimizeSetAccessor(ParserContext pCtx, char[] property, int start, int offset, Object ctx,
                                      Object thisRef, VariableResolverFactory factory, boolean rootThisRef,
                                      Object value, Class ingressType) {
    this.rootNode = this.currNode = null;
    this.expr = property;
    this.start = start;
    this.first = true;

    this.length = start + offset;
    this.ctx = ctx;
    this.thisRef = thisRef;
    this.variableFactory = factory;
    this.ingressType = ingressType;

    char[] root = null;

    int split = findLastUnion();

    PropertyVerifier verifier = new PropertyVerifier(property, this.pCtx = pCtx);

    if (split != -1) {
      root = subset(property, 0, split++);
      //todo: must use the property verifier.
      property = subset(property, split, property.length - split);
    }

    if (root != null) {
      this.length = end = (this.expr = root).length;

      compileGetChain();
      ctx = this.val;
    }

    if (ctx == null) {
      throw new PropertyAccessException("could not access property: " + new String(property, this.start, Math.min(length, property.length)) + "; parent is null: "
          + new String(expr), expr, this.start, pCtx);
    }

    try {
      this.length = end = (this.expr = property).length;
      int st;
      this.cursor = st = 0
      ;

      skipWhitespace();

      if (collection) {
        st = cursor;

        if (cursor == end)
          throw new PropertyAccessException("unterminated '['", expr, this.start, pCtx);

        if (scanTo(']'))
          throw new PropertyAccessException("unterminated '['", expr, this.start, pCtx);

        String ex = new String(property, st, cursor - st);

        if (ctx instanceof Map) {
          if (MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING && hasPropertyHandler(Map.class)) {
            propHandlerSet(ex, ctx, Map.class, value);
          }
          else {
            //noinspection unchecked
            ((Map) ctx).put(eval(ex, ctx, variableFactory), convert(value, returnType = verifier.analyze()));

            addAccessorNode(new MapAccessorNest(ex, returnType));
          }

          return rootNode;
        }
        else if (ctx instanceof List) {
          if (MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING && hasPropertyHandler(List.class)) {
            propHandlerSet(ex, ctx, List.class, value);
          }
          else {
            //noinspection unchecked
            ((List) ctx).set(eval(ex, ctx, variableFactory, Integer.class),
                convert(value, returnType = verifier.analyze()));

            addAccessorNode(new ListAccessorNest(ex, returnType));
          }

          return rootNode;
        }
        else if (MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING && hasPropertyHandler(ctx.getClass())) {
          propHandlerSet(ex, ctx, ctx.getClass(), value);
          return rootNode;
        }
        else if (ctx.getClass().isArray()) {
          if (MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING && hasPropertyHandler(Array.class)) {
            propHandlerSet(ex, ctx, Array.class, value);
          }
          else {
            //noinspection unchecked
            Array.set(ctx, eval(ex, ctx, variableFactory, Integer.class),
                convert(value, getBaseComponentType(ctx.getClass())));
            addAccessorNode(new ArrayAccessorNest(ex));
          }
          return rootNode;
        }
        else {
          throw new PropertyAccessException("cannot bind to collection property: " + new String(property) +
              ": not a recognized collection type: " + ctx.getClass(), expr, this.st, pCtx);
        }
      }
      else if (MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING && hasPropertyHandler(ctx.getClass())) {
        propHandlerSet(new String(property), ctx, ctx.getClass(), value);
        return rootNode;
      }

      String tk = new String(property, 0, length).trim();

      if (hasSetListeners()) {
        notifySetListeners(ctx, tk, variableFactory, value);
        addAccessorNode(new Notify(tk));
      }

      Member member = getFieldOrWriteAccessor(ctx.getClass(), tk, value == null ? null : ingressType);

      if (member instanceof Field) {
        Field fld = (Field) member;

        if (value != null && !fld.getType().isAssignableFrom(value.getClass())) {
          if (!canConvert(fld.getType(), value.getClass())) {
            throw new CompileException("cannot convert type: "
                + value.getClass() + ": to " + fld.getType(), this.expr, this.start);
          }

          fld.set(ctx, convert(value, fld.getType()));
          addAccessorNode(new DynamicFieldAccessor(fld));
        }
        else if (value == null && fld.getType().isPrimitive()) {
          fld.set(ctx, PropertyTools.getPrimitiveInitialValue(fld.getType()));
          addAccessorNode(new FieldAccessor(fld));
        }
        else {
          fld.set(ctx, value);
          addAccessorNode(new FieldAccessor(fld));
        }
      }
      else if (member != null) {
        Method meth = (Method) member;

        if (value != null && !meth.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
          if (!canConvert(meth.getParameterTypes()[0], value.getClass())) {
            throw new CompileException("cannot convert type: "
                + value.getClass() + ": to " + meth.getParameterTypes()[0], this.expr, this.start);
          }

          meth.invoke(ctx, convert(value, meth.getParameterTypes()[0]));
        }
        else if (value == null && meth.getParameterTypes()[0].isPrimitive()) {
          meth.invoke(ctx, PropertyTools.getPrimitiveInitialValue(meth.getParameterTypes()[0]));
        }
        else {
          meth.invoke(ctx, value);
        }

        addAccessorNode(new SetterAccessor(meth));
      }
      else if (ctx instanceof Map) {
        //noinspection unchecked
        ((Map) ctx).put(tk, value);

        addAccessorNode(new MapAccessor(tk));
      }
      else {
        throw new PropertyAccessException("could not access property (" + tk + ") in: " + ingressType.getName()
            , this.expr, this.start, pCtx);
      }
    }
    catch (InvocationTargetException e) {
      throw new PropertyAccessException("could not access property: " + new String(property), this.expr, st, e, pCtx);
    }
    catch (IllegalAccessException e) {
      throw new PropertyAccessException("could not access property: " + new String(property), this.expr, st, e, pCtx);
    }
    catch (IllegalArgumentException e) {
      throw new PropertyAccessException("error binding property: " + new String(property) + " (value <<" + value + ">>::"
          + (value == null ? "null" : value.getClass().getCanonicalName()) + ")", this.expr, st, e, pCtx);
    }

    return rootNode;
  }

  private Accessor compileGetChain() {
    Object curr = ctx;
    cursor = start;

    try {
      if (!MVEL.COMPILER_OPT_ALLOW_OVERRIDE_ALL_PROPHANDLING) {
        while (cursor < end) {
          switch (nextSubToken()) {
            case BEAN:
              curr = getBeanProperty(curr, capture());
              break;
            case METH:
              curr = getMethod(curr, capture());
              break;
            case COL:
              curr = getCollectionProperty(curr, capture());
              break;
            case WITH:
              curr = getWithProperty(curr);
              break;
            case DONE:
              break;
          }

          first = false;
          if (curr != null) returnType = curr.getClass();
          if (cursor < end) {
            if (nullSafe) {
              int os = expr[cursor] == '.' ? 1 : 0;
              addAccessorNode(new NullSafe(expr, cursor + os, end - cursor - os, pCtx));
              if (curr == null) break;
            }
            if (curr == null) throw new NullPointerException();
          }
          staticAccess = false;
        }

      }
      else {
        while (cursor < end) {
          switch (nextSubToken()) {
            case BEAN:
              curr = getBeanPropertyAO(curr, capture());
              break;
            case METH:
              curr = getMethod(curr, capture());
              break;
            case COL:
              curr = getCollectionPropertyAO(curr, capture());
              break;
            case WITH:
              curr = getWithProperty(curr);
              break;
            case DONE:
              break;
          }

          first = false;
          if (curr != null) returnType = curr.getClass();
          if (cursor < end) {
            if (nullSafe) {
              int os = expr[cursor] == '.' ? 1 : 0;
              addAccessorNode(new NullSafe(expr, cursor + os, end - cursor - os, pCtx));
              if (curr == null) break;
            }
            if (curr == null) throw new NullPointerException();
          }
          staticAccess = false;
        }
      }

      val = curr;
      return rootNode;
    }
    catch (InvocationTargetException e) {
      if (MVEL.INVOKED_METHOD_EXCEPTIONS_BUBBLE) {
        if (e.getTargetException() instanceof RuntimeException) {
          throw (RuntimeException) e.getTargetException();
        }
        else {
          throw new RuntimeException(e);
        }
      }

      throw new PropertyAccessException(new String(expr, start, length) + ": "
          + e.getTargetException().getMessage(), this.expr, this.st, e, pCtx);
    }
    catch (IllegalAccessException e) {
      throw new PropertyAccessException(new String(expr, start, length) + ": "
          + e.getMessage(), this.expr, this.st, e, pCtx);
    }
    catch (IndexOutOfBoundsException e) {
      throw new PropertyAccessException(new String(expr, start, length)
          + ": array index out of bounds.", this.expr, this.st, e, pCtx);
    }
    catch (CompileException e) {
      throw e;
    }
    catch (NullPointerException e) {
      throw new PropertyAccessException("null pointer: " + new String(expr, start, length), this.expr, this.st, e, pCtx);
    }
    catch (Exception e) {
      LOG.log(Level.WARNING, "", e);
      throw new CompileException(e.getMessage(), this.expr, st, e);
    }
  }

  private void addAccessorNode(AccessorNode an) {
    if (rootNode == null)
      rootNode = currNode = an;
    else {
      currNode = currNode.setNextNode(an);
    }
  }

  private Object getWithProperty(Object ctx) {
    currType = null;
    String root = start == cursor ? null : new String(expr, start, cursor - 1).trim();

    int st = cursor + 1;
    cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '{', pCtx);

    WithAccessor wa = new WithAccessor(pCtx, root, expr, st, cursor++ - st, ingressType);
    addAccessorNode(wa);
    return wa.getValue(ctx, thisRef, variableFactory);
  }

  private Object getBeanPropertyAO(Object ctx, String property)
      throws Exception {

    if (GlobalListenerFactory.hasGetListeners()) {
      notifyGetListeners(ctx, property, variableFactory);
      addAccessorNode(new Notify(property));
    }

    if (ctx != null && hasPropertyHandler(ctx.getClass())) return propHandler(property, ctx, ctx.getClass());

    return getBeanProperty(ctx, property);
  }

  private Object getBeanProperty(Object ctx, String property) throws Exception {
    if ((pCtx == null ? currType : pCtx.getVarOrInputTypeOrNull(property)) == Object.class
        && !pCtx.isStrongTyping()) {
      currType = null;
    }

    if (first) {
      if ("this".equals(property)) {
        addAccessorNode(new ThisValueAccessor());
        return this.thisRef;
      }
      else if (variableFactory != null && variableFactory.isResolveable(property)) {


        if (variableFactory.isIndexedFactory() && variableFactory.isTarget(property)) {
          int idx;
          addAccessorNode(new IndexedVariableAccessor(idx = variableFactory.variableIndexOf(property)));

          VariableResolver vr = variableFactory.getIndexedVariableResolver(idx);
          if (vr == null) {
            variableFactory.setIndexedVariableResolver(idx, variableFactory.getVariableResolver(property));
          }

          return variableFactory.getIndexedVariableResolver(idx).getValue();
        }
        else {
          addAccessorNode(new VariableAccessor(property));

          return variableFactory.getVariableResolver(property).getValue();
        }
      }
    }

    boolean classRef = false;

    Class<?> cls;
    if (ctx instanceof Class) {
      if (MVEL.COMPILER_OPT_SUPPORT_JAVA_STYLE_CLASS_LITERALS
          && "class".equals(property)) {
        return ctx;
      }

      cls = (Class<?>) ctx;

      classRef = true;
    }
    else if (ctx != null) {
      cls = ctx.getClass();
    }
    else {
      cls = currType;
    }

    if (hasPropertyHandler(cls)) {
      PropertyHandlerAccessor acc = new PropertyHandlerAccessor(property, cls, getPropertyHandler(cls));
      addAccessorNode(acc);
      return acc.getValue(ctx, thisRef, variableFactory);
    }

    Member member = cls != null ? getFieldOrAccessor(cls, property) : null;

    if (member != null && classRef && (member.getModifiers() & Modifier.STATIC) == 0) {
      member = null;
    }

    Object o;

    if (member instanceof Method) {
      try {
        o = ctx != null ? ((Method) member).invoke(ctx, EMPTYARG) : null;

        if (hasNullPropertyHandler()) {
          addAccessorNode(new GetterAccessorNH((Method) member, getNullPropertyHandler()));
          if (o == null) o = getNullPropertyHandler().getProperty(member.getName(), ctx, variableFactory);
        }
        else {
          addAccessorNode(new GetterAccessor((Method) member));
        }
      }
      catch (IllegalAccessException e) {
        Method iFaceMeth = determineActualTargetMethod((Method) member);

        if (iFaceMeth == null)
          throw new PropertyAccessException("could not access field: "
              + cls.getName() + "." + property, this.expr, this.start, pCtx);

        o = iFaceMeth.invoke(ctx, EMPTYARG);

        if (hasNullPropertyHandler()) {
          addAccessorNode(new GetterAccessorNH((Method) member, getNullMethodHandler()));
          if (o == null) o = getNullMethodHandler().getProperty(member.getName(), ctx, variableFactory);
        }
        else {
          addAccessorNode(new GetterAccessor(iFaceMeth));
        }
      }
      catch (IllegalArgumentException e) {
        if (member.getDeclaringClass().equals(ctx)) {
          try {
            Class c = Class.forName(member.getDeclaringClass().getName() + "$" + property);

            throw new CompileException("name collision between innerclass: " + c.getCanonicalName()
                + "; and bean accessor: " + property + " (" + member.toString() + ")", expr, tkStart);
          }
          catch (ClassNotFoundException e2) {
            //fallthru
          }
        }
        throw e;
      }
      currType = toNonPrimitiveType(((Method) member).getReturnType());
      return o;
    }
    else if (member != null) {
      Field f = (Field) member;

      if ((f.getModifiers() & Modifier.STATIC) != 0) {
        o = f.get(null);

        if (hasNullPropertyHandler()) {
          addAccessorNode(new StaticVarAccessorNH((Field) member, getNullMethodHandler()));
          if (o == null) o = getNullMethodHandler().getProperty(member.getName(), ctx, variableFactory);
        }
        else {
          addAccessorNode(new StaticVarAccessor((Field) member));
        }
      }
      else {
        o = ctx != null ? f.get(ctx) : null;
        if (hasNullPropertyHandler()) {
          addAccessorNode(new FieldAccessorNH((Field) member, getNullMethodHandler()));
          if (o == null) o = getNullMethodHandler().getProperty(member.getName(), ctx, variableFactory);
        }
        else {
          addAccessorNode(new FieldAccessor((Field) member));
        }
      }
      currType = toNonPrimitiveType(f.getType());
      return o;
    }
    else if (ctx instanceof Map && (((Map) ctx).containsKey(property) || nullSafe)) {
      addAccessorNode(new MapAccessor(property));
      return ((Map) ctx).get(property);
    }
    else if (ctx != null && "length".equals(property) && ctx.getClass().isArray()) {
      addAccessorNode(new ArrayLength());
      return getLength(ctx);
    }
    else if (LITERALS.containsKey(property)) {
      addAccessorNode(new StaticReferenceAccessor(ctx = LITERALS.get(property)));
      return ctx;
    }
    else {
      Object tryStaticMethodRef = tryStaticAccess();
      staticAccess = true;
      if (tryStaticMethodRef != null) {
        if (tryStaticMethodRef instanceof Class) {
          addAccessorNode(new StaticReferenceAccessor(tryStaticMethodRef));
          return tryStaticMethodRef;
        }
        else if (tryStaticMethodRef instanceof Field) {
          addAccessorNode(new StaticVarAccessor((Field) tryStaticMethodRef));
          return ((Field) tryStaticMethodRef).get(null);
        }
        else {
          addAccessorNode(new StaticReferenceAccessor(tryStaticMethodRef));
          return tryStaticMethodRef;
        }
      }
      else if (ctx instanceof Class) {
        Class c = (Class) ctx;
        for (Method m : c.getMethods()) {
          if (property.equals(m.getName())) {
            if (pCtx!=null&& pCtx.getParserConfiguration()!=null?pCtx.getParserConfiguration().isAllowNakedMethCall():MVEL.COMPILER_OPT_ALLOW_NAKED_METH_CALL) {
              o = m.invoke(null, EMPTY_OBJ_ARR);
              if (hasNullMethodHandler()) {
                addAccessorNode(new MethodAccessorNH(m, new ExecutableStatement[0], getNullMethodHandler()));
                if (o == null)
                  o = getNullMethodHandler().getProperty(m.getName(), ctx, variableFactory);
              }
              else {
                addAccessorNode(new MethodAccessor(m, new ExecutableStatement[0]));
              }
              return o;
            }
            else {
              addAccessorNode(new StaticReferenceAccessor(m));
              return m;
            }
          }
        }

        try {
          Class subClass = findClass(variableFactory, c.getName() + "$" + property, pCtx);
          addAccessorNode(new StaticReferenceAccessor(subClass));
          return subClass;
        }
        catch (ClassNotFoundException cnfe) {
          // fall through.
        }
      }
      else if (pCtx!=null&& pCtx.getParserConfiguration()!=null?pCtx.getParserConfiguration().isAllowNakedMethCall():MVEL.COMPILER_OPT_ALLOW_NAKED_METH_CALL) {
        return getMethod(ctx, property);
      }


      if (ctx == null) {
        throw new PropertyAccessException("unresolvable property or identifier: " + property, expr, start, pCtx);
      }
      else {
        throw new PropertyAccessException("could not access: " + property + "; in class: "
            + ctx.getClass().getName(), expr, start, pCtx);
      }
    }
  }

  /**
   * Handle accessing a property embedded in a collections, map, or array
   *
   * @param ctx  -
   * @param prop -
   * @return -
   * @throws Exception -
   */
  private Object getCollectionProperty(Object ctx, String prop) throws Exception {
    if (prop.length() > 0) {
      ctx = getBeanProperty(ctx, prop);
    }

    currType = null;
    if (ctx == null) return null;

    int start = ++cursor;

    skipWhitespace();

    if (cursor == end)
      throw new CompileException("unterminated '['", this.expr, this.start);

    String item;

    if (scanTo(']'))
      throw new CompileException("unterminated '['", this.expr, this.start);

    item = new String(expr, start, cursor - start);

    boolean itemSubExpr = true;

    Object idx = null;

    try {
      idx = parseInt(item);
      itemSubExpr = false;
    }
    catch (Exception e) {
      // not a number;
    }

    ExecutableStatement itemStmt = null;
    if (itemSubExpr) {
      try {
        idx = (itemStmt = (ExecutableStatement) subCompileExpression(item.toCharArray(), pCtx))
            .getValue(thisRef, thisRef, variableFactory);
      }
      catch (CompileException e) {
        e.setExpr(this.expr);
        e.setCursor(start);
        throw e;
      }
    }

    ++cursor;

    if (ctx instanceof Map) {
      if (itemSubExpr) {
        addAccessorNode(new MapAccessorNest(itemStmt, null));
      }
      else {
        addAccessorNode(new MapAccessor(parseInt(item)));
      }

      return ((Map) ctx).get(idx);
    }
    else if (ctx instanceof List) {
      if (itemSubExpr) {
        addAccessorNode(new ListAccessorNest(itemStmt, null));
      }
      else {
        addAccessorNode(new ListAccessor(parseInt(item)));
      }

      return ((List) ctx).get((Integer) idx);
    }
    else if (ctx.getClass().isArray()) {
      if (itemSubExpr) {
        addAccessorNode(new ArrayAccessorNest(itemStmt));
      }
      else {
        addAccessorNode(new ArrayAccessor(parseInt(item)));
      }

      return Array.get(ctx, (Integer) idx);
    }
    else if (ctx instanceof CharSequence) {
      if (itemSubExpr) {
        addAccessorNode(new IndexedCharSeqAccessorNest(itemStmt));
      }
      else {
        addAccessorNode(new IndexedCharSeqAccessor(parseInt(item)));
      }

      return ((CharSequence) ctx).charAt((Integer) idx);
    }
    else if (ctx instanceof Class) {
      TypeDescriptor tDescr = new TypeDescriptor(expr, this.start, length, 0);
      if (tDescr.isArray()) {
        Class cls = getClassReference((Class) ctx, tDescr, variableFactory, pCtx);
        rootNode = new StaticReferenceAccessor(cls);
        return cls;
      }
    }
    throw new CompileException("illegal use of []: unknown type: "
            + ctx.getClass().getName(), this.expr, this.start);
  }


  private Object getCollectionPropertyAO(Object ctx, String prop) throws Exception {
    if (prop.length() > 0) {
      ctx = getBeanPropertyAO(ctx, prop);
    }

    currType = null;
    if (ctx == null) return null;

    int _start = ++cursor;

    skipWhitespace();

    if (cursor == end)
      throw new CompileException("unterminated '['", this.expr, this.start);

    String item;

    if (scanTo(']'))
      throw new CompileException("unterminated '['", this.expr, this.start);

    item = new String(expr, _start, cursor - _start);

    boolean itemSubExpr = true;

    Object idx = null;

    try {
      idx = parseInt(item);
      itemSubExpr = false;
    }
    catch (Exception e) {
      // not a number;
    }

    ExecutableStatement itemStmt = null;
    if (itemSubExpr) {
      idx = (itemStmt = (ExecutableStatement) subCompileExpression(item.toCharArray(), pCtx))
          .getValue(thisRef, thisRef, variableFactory);
    }

    ++cursor;

    if (ctx instanceof Map) {
      if (hasPropertyHandler(Map.class)) {
        return propHandler(item, ctx, Map.class);
      }
      else {
        if (itemSubExpr) {
          addAccessorNode(new MapAccessorNest(itemStmt, null));
        }
        else {
          addAccessorNode(new MapAccessor(parseInt(item)));
        }

        return ((Map) ctx).get(idx);
      }
    }
    else if (ctx instanceof List) {
      if (hasPropertyHandler(List.class)) {
        return propHandler(item, ctx, List.class);
      }
      else {
        if (itemSubExpr) {
          addAccessorNode(new ListAccessorNest(itemStmt, null));
        }
        else {
          addAccessorNode(new ListAccessor(parseInt(item)));
        }

        return ((List) ctx).get((Integer) idx);
      }
    }
    else if (ctx.getClass().isArray()) {
      if (hasPropertyHandler(Array.class)) {
        return propHandler(item, ctx, Array.class);
      }
      else {
        if (itemSubExpr) {
          addAccessorNode(new ArrayAccessorNest(itemStmt));
        }
        else {
          addAccessorNode(new ArrayAccessor(parseInt(item)));
        }

        return Array.get(ctx, (Integer) idx);
      }
    }
    else if (ctx instanceof CharSequence) {
      if (hasPropertyHandler(CharSequence.class)) {
        return propHandler(item, ctx, CharSequence.class);
      }
      else {
        if (itemSubExpr) {
          addAccessorNode(new IndexedCharSeqAccessorNest(itemStmt));
        }
        else {
          addAccessorNode(new IndexedCharSeqAccessor(parseInt(item)));
        }

        return ((CharSequence) ctx).charAt((Integer) idx);
      }
    }
    else {
      TypeDescriptor tDescr = new TypeDescriptor(expr, this.start, end - this.start, 0);
      if (tDescr.isArray()) {
        Class cls = getClassReference((Class) ctx, tDescr, variableFactory, pCtx);
        rootNode = new StaticReferenceAccessor(cls);
        return cls;
      }

      throw new CompileException("illegal use of []: unknown type: "
          + ctx.getClass().getName(), this.expr, this.st);
    }
  }

  /**
   * Find an appropriate method, execute it, and return it's response.
   *
   * @param ctx  -
   * @param name -
   * @return -
   * @throws Exception -
   */
  @SuppressWarnings({"unchecked"})
  private Object getMethod(Object ctx, String name) throws Exception {
    int st = cursor;
    String tk = cursor != end
        && expr[cursor] == '(' && ((cursor = balancedCapture(expr, cursor, '(')) - st) > 1 ?
        new String(expr, st + 1, cursor - st - 1) : "";
    cursor++;

    Object[] args;
    Class[] argTypes;
    ExecutableStatement[] es;

    if (tk.length() == 0) {
      args = ParseTools.EMPTY_OBJ_ARR;
      argTypes = ParseTools.EMPTY_CLS_ARR;
      es = null;
    }
    else {
      List<char[]> subtokens = parseParameterList(tk.toCharArray(), 0, -1);
      es = new ExecutableStatement[subtokens.size()];
      args = new Object[subtokens.size()];
      argTypes = new Class[subtokens.size()];

      for (int i = 0; i < subtokens.size(); i++) {
        try {
          args[i] = (es[i] = (ExecutableStatement) subCompileExpression(subtokens.get(i), pCtx))
              .getValue(this.thisRef, thisRef, variableFactory);
        }
        catch (CompileException e) {
          throw ErrorUtil.rewriteIfNeeded(e, this.expr, this.start);
        }

        if (es[i].isExplicitCast()) argTypes[i] = es[i].getKnownEgressType();
      }

      if (pCtx.isStrictTypeEnforcement()) {
        for (int i = 0; i < args.length; i++) {
          argTypes[i] = es[i].getKnownEgressType();
          if (es[i] instanceof ExecutableLiteral && ((ExecutableLiteral)es[i]).getLiteral() == null) {
            argTypes[i] = NullType.class;
          }
        }
      }
      else {
        for (int i = 0; i < args.length; i++) {
          if (argTypes[i] != null) continue;

          if (es[i].getKnownEgressType() == Object.class) {
            argTypes[i] = args[i] == null ? null : args[i].getClass();
          }
          else {
            argTypes[i] = es[i].getKnownEgressType();
          }
        }
      }
    }

    return getMethod(ctx, name, args, argTypes, es);
  }

  private Object getMethod(Object ctx, String name, Object[] args, Class[] argTypes, ExecutableStatement[] es) throws Exception {
    if (first && variableFactory != null && variableFactory.isResolveable(name)) {
      Object ptr = variableFactory.getVariableResolver(name).getValue();
      if (ptr instanceof Method) {
        ctx = ((Method) ptr).getDeclaringClass();
        name = ((Method) ptr).getName();
      }
      else if (ptr instanceof MethodStub) {
        ctx = ((MethodStub) ptr).getClassReference();
        name = ((MethodStub) ptr).getMethodName();
      }
      else if (ptr instanceof FunctionInstance) {
        FunctionInstance func = (FunctionInstance) ptr;
        if (!name.equals(func.getFunction().getName())) {
          getBeanProperty(ctx, name);
          addAccessorNode(new DynamicFunctionAccessor(es));
        }
        else {
          addAccessorNode(new FunctionAccessor(func, es));
        }
        return func.call(ctx, thisRef, variableFactory, args);
      }
      else {
        throw new OptimizationFailure("attempt to optimize a method call for a reference that does not point to a method: "
            + name + " (reference is type: " + (ctx != null ? ctx.getClass().getName() : null) + ")");
      }

      first = false;
    }

    if (ctx == null && currType == null) {
      throw new PropertyAccessException("null pointer or function not found: " + name, this.expr, this.start, pCtx);
    }

    boolean classTarget = false;
    Class<?> cls = currType != null ? currType : ((classTarget = ctx instanceof Class) ? (Class<?>) ctx : ctx.getClass());
    currType = null;

    Method m;
    Class[] parameterTypes = null;

    /**
     * If we have not cached the method then we need to go ahead and try to resolve it.
     */

    /**
     * Try to find an instance method from the class target.
     */
    if ((m = getBestCandidate(argTypes, name, cls, cls.getMethods(), false, classTarget)) != null) {
      parameterTypes = m.getParameterTypes();
    }

    if (m == null && classTarget) {
      /**
       * If we didn't find anything, maybe we're looking for the actual java.lang.Class methods.
       */
      if ((m = getBestCandidate(argTypes, name, cls, Class.class.getMethods(), false)) != null) {
        parameterTypes = m.getParameterTypes();
      }
    }

    // If we didn't find anything and the declared class is different from the actual one try also with the actual one
    if (m == null && ctx != null && cls != ctx.getClass() && !(ctx instanceof Class)) {
      cls = ctx.getClass();
      if ((m = getBestCandidate(argTypes, name, cls, cls.getMethods(), false, classTarget)) != null) {
        parameterTypes = m.getParameterTypes();
      }
    }

    if (m == null) {
      StringAppender errorBuild = new StringAppender();
      if ("size".equals(name) && args.length == 0 && cls.isArray()) {
        addAccessorNode(new ArrayLength());
        return getLength(ctx);
      }

      for (int i = 0; i < args.length; i++) {
        errorBuild.append(args[i] != null ? args[i].getClass().getName() : null);
        if (i < args.length - 1) errorBuild.append(", ");
      }

      throw new PropertyAccessException("unable to resolve method: " + cls.getName() + "."
          + name + "(" + errorBuild.toString() + ") [arglength=" + args.length + "]", this.expr, this.st, pCtx);
    }

    if (es != null) {
      ExecutableStatement cExpr;
      for (int i = 0; i < es.length; i++) {
        cExpr = es[i];
        if (cExpr.getKnownIngressType() == null) {
          cExpr.setKnownIngressType(paramTypeVarArgsSafe(parameterTypes, i, m.isVarArgs()));
          cExpr.computeTypeConversionRule();
        }
        if (!cExpr.isConvertableIngressEgress()) {
          args[i] = convert(args[i], paramTypeVarArgsSafe(parameterTypes, i, m.isVarArgs()));
        }
      }
    }
    else {
      /**
       * Coerce any types if required.
       */
      for (int i = 0; i < args.length; i++)
        args[i] = convert(args[i], paramTypeVarArgsSafe(parameterTypes, i, m.isVarArgs()));
    }

    Method method = getWidenedTarget(cls, m);
    Object o = ctx != null ? method.invoke(ctx, normalizeArgsForVarArgs(parameterTypes, args, m.isVarArgs())) : null;

    if (hasNullMethodHandler()) {
      addAccessorNode(new MethodAccessorNH(method, (ExecutableStatement[]) es, getNullMethodHandler()));
      if (o == null) o = getNullMethodHandler().getProperty(m.getName(), ctx, variableFactory);
    }
    else {
      addAccessorNode(new MethodAccessor(method, (ExecutableStatement[]) es));
    }

    /**
     * return the response.
     */
    currType = toNonPrimitiveType(method.getReturnType());
    return o;
  }

  public Object getValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory) throws Exception {
    return rootNode.getValue(ctx, elCtx, variableFactory);
  }

  private Accessor _getAccessor(Object o, Class type) {
    if (o instanceof List) {
      Accessor[] a = new Accessor[((List) o).size()];
      int i = 0;

      for (Object item : (List) o) {
        a[i++] = _getAccessor(item, type);
      }

      returnType = List.class;

      return new ListCreator(a);
    }
    else if (o instanceof Map) {
      Accessor[] k = new Accessor[((Map) o).size()];
      Accessor[] v = new Accessor[k.length];
      int i = 0;

      for (Object item : ((Map) o).keySet()) {
        k[i] = _getAccessor(item, type); // key
        v[i++] = _getAccessor(((Map) o).get(item), type); // value
      }

      returnType = Map.class;

      return new MapCreator(k, v);
    }
    else if (o instanceof Object[]) {
      Accessor[] a = new Accessor[((Object[]) o).length];
      int i = 0;
      int dim = 0;

      if (type != null) {
        String nm = type.getName();
        while (nm.charAt(dim) == '[') dim++;
      }
      else {
        type = Object[].class;
        dim = 1;
      }

      try {
        Class base = getBaseComponentType(type);
        Class cls = dim > 1 ? findClass(null, repeatChar('[', dim - 1) + "L" + base.getName() + ";", pCtx)
            : type;

        for (Object item : (Object[]) o) {
          expectType(pCtx, a[i++] = _getAccessor(item, cls), base, true);
        }

        return new ArrayCreator(a, getSubComponentType(type));
      }
      catch (ClassNotFoundException e) {
        throw new RuntimeException("this error should never throw:" + getBaseComponentType(type).getName(), e);
      }
    }
    else {
      if (returnType == null) returnType = Object.class;
      if (type.isArray()) {
        return new ExprValueAccessor((String) o, type, ctx, variableFactory, pCtx);
      }
      else {
        return new ExprValueAccessor((String) o, Object.class, ctx, variableFactory, pCtx);
      }
    }
  }


  public Accessor optimizeCollection(ParserContext pCtx, Object o, Class type, char[] property, int start, int offset,
                                     Object ctx, Object thisRef, VariableResolverFactory factory) {
    this.start = this.cursor = start;
    this.length = start + offset;
    this.returnType = type;
    this.ctx = ctx;
    this.variableFactory = factory;
    this.pCtx = pCtx;

    Accessor root = _getAccessor(o, returnType);

    if (property != null && length > start) {
      return new Union(pCtx, root, property, cursor, offset);
    }
    else {
      return root;
    }
  }


  public Accessor optimizeObjectCreation(ParserContext pCtx, char[] property, int start, int offset,
                                         Object ctx, Object thisRef, VariableResolverFactory factory) {
    this.length = start + offset;
    this.cursor = this.start = start;
    this.pCtx = pCtx;

    try {
      return compileConstructor(property, ctx, factory);
    }
    catch (CompileException e) {
      throw ErrorUtil.rewriteIfNeeded(e, property, this.start);
    }
    catch (ClassNotFoundException e) {
      throw new CompileException("could not resolve class: " + e.getMessage(), property, this.start, e);
    }
    catch (Exception e) {
      throw new CompileException("could not create constructor: " + e.getMessage(), property, this.start, e);
    }
  }


  private void setRootNode(AccessorNode rootNode) {
    this.rootNode = this.currNode = rootNode;
  }

  private AccessorNode getRootNode() {
    return rootNode;
  }

  public Object getResultOptPass() {
    return val;
  }

  @SuppressWarnings({"WeakerAccess"})
  private AccessorNode compileConstructor(char[] expression, Object ctx, VariableResolverFactory vars) throws
      InstantiationException, IllegalAccessException, InvocationTargetException,
      ClassNotFoundException, NoSuchMethodException {

    String[] cnsRes = captureContructorAndResidual(expression, start, length);
    List<char[]> constructorParms = parseMethodOrConstructor(cnsRes[0].toCharArray());

    if (constructorParms != null) {
      String s = new String(subset(expression, 0, ArrayTools.findFirst('(', start, length, expression)));
      Class cls = ParseTools.findClass(vars, s, pCtx);

      ExecutableStatement[] cStmts = new ExecutableStatement[constructorParms.size()];

      for (int i = 0; i < constructorParms.size(); i++) {
        cStmts[i] = (ExecutableStatement) subCompileExpression(constructorParms.get(i), pCtx);
      }

      Object[] parms = new Object[constructorParms.size()];
      for (int i = 0; i < constructorParms.size(); i++) {
        parms[i] = cStmts[i].getValue(ctx, vars);
      }

      Constructor cns = getBestConstructorCandidate(parms, cls, pCtx.isStrongTyping());

      if (cns == null) {
        StringBuilder error = new StringBuilder();
        for (int i = 0; i < parms.length; i++) {
          error.append(parms[i].getClass().getName());
          if (i + 1 < parms.length) error.append(", ");
        }

        throw new CompileException("unable to find constructor: " + cls.getName()
            + "(" + error.toString() + ")", this.expr, this.start);
      }
      for (int i = 0; i < parms.length; i++) {
        //noinspection unchecked
        parms[i] = convert(parms[i], paramTypeVarArgsSafe(cns.getParameterTypes(), i, cns.isVarArgs()));
      }
      parms = normalizeArgsForVarArgs(cns.getParameterTypes(), parms, cns.isVarArgs());

      AccessorNode ca = new ConstructorAccessor(cns, cStmts);

      if (cnsRes.length > 1) {
        ReflectiveAccessorOptimizer compiledOptimizer
            = new ReflectiveAccessorOptimizer(pCtx, cnsRes[1].toCharArray(), 0, cnsRes[1].length(),
            cns.newInstance(parms), ctx, vars);
        compiledOptimizer.ingressType = cns.getDeclaringClass();


        compiledOptimizer.setRootNode(ca);
        compiledOptimizer.compileGetChain();
        ca = compiledOptimizer.getRootNode();

        this.val = compiledOptimizer.getResultOptPass();
      }

      return ca;
    }
    else {
      ClassLoader classLoader = pCtx != null ? pCtx.getClassLoader() : currentThread().getContextClassLoader();
      Constructor<?> cns = Class.forName(new String(expression), true, classLoader ).getConstructor(EMPTYCLS);
      AccessorNode ca = new ConstructorAccessor(cns, null);

      if (cnsRes.length > 1) {
        //noinspection NullArgumentToVariableArgMethod
        ReflectiveAccessorOptimizer compiledOptimizer
            = new ReflectiveAccessorOptimizer(pCtx, cnsRes[1].toCharArray(), 0, cnsRes[1].length(), cns.newInstance(null), ctx, vars);
        compiledOptimizer.setRootNode(ca);
        compiledOptimizer.compileGetChain();
        ca = compiledOptimizer.getRootNode();

        this.val = compiledOptimizer.getResultOptPass();
      }

      return ca;
    }
  }

  public Class getEgressType() {
    return returnType;
  }

  public boolean isLiteralOnly() {
      return false;
  }

  private Object propHandler(String property, Object ctx, Class handler) {
    PropertyHandler ph = getPropertyHandler(handler);
    addAccessorNode(new PropertyHandlerAccessor(property, handler, ph));
    return ph.getProperty(property, ctx, variableFactory);
  }

  public void propHandlerSet(String property, Object ctx, Class handler, Object value) {
    PropertyHandler ph = getPropertyHandler(handler);
    addAccessorNode(new PropertyHandlerAccessor(property, handler, ph));
    ph.setProperty(property, ctx, variableFactory, value);
  }
}
