/**
 * 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.ast;

import org.mvel2.CompileException;
import org.mvel2.ErrorDetail;
import org.mvel2.ParserContext;
import org.mvel2.PropertyAccessor;
import org.mvel2.compiler.Accessor;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.compiler.PropertyVerifier;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.optimizers.AccessorOptimizer;
import org.mvel2.optimizers.OptimizerFactory;
import org.mvel2.util.ArrayTools;
import org.mvel2.util.ErrorUtil;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;

import static java.lang.reflect.Array.newInstance;
import static org.mvel2.DataConversion.convert;
import static org.mvel2.MVEL.analyze;
import static org.mvel2.MVEL.eval;
import static org.mvel2.optimizers.OptimizerFactory.getThreadAccessorOptimizer;
import static org.mvel2.util.ArrayTools.findFirst;
import static org.mvel2.util.CompilerTools.getInjectedImports;
import static org.mvel2.util.ParseTools.*;
import static org.mvel2.util.ReflectionUtil.toPrimitiveArrayType;

/**
 * @author Christopher Brock
 */
@SuppressWarnings({"ManualArrayCopy"})
public class NewObjectNode extends ASTNode {
  private transient Accessor newObjectOptimizer;
  private TypeDescriptor typeDescr;
  private char[] name;

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

  public NewObjectNode(TypeDescriptor typeDescr, int fields, ParserContext pCtx) {
    super(pCtx);
    this.typeDescr = typeDescr;
    this.fields = fields;
    this.expr = typeDescr.getExpr();
    this.start = typeDescr.getStart();
    this.offset = typeDescr.getOffset();

    if (offset < expr.length) {
      this.name = subArray(expr, start, start + offset);
    }
    else {
      this.name = expr;
    }

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      if (pCtx != null && pCtx.hasImport(typeDescr.getClassName())) {
        pCtx.setAllowBootstrapBypass(false);
        egressType = pCtx.getImport(typeDescr.getClassName());
      }
      else {
        try {
          egressType = Class.forName(typeDescr.getClassName(), true, getClassLoader());
        }
        catch (ClassNotFoundException e) {
          if (pCtx.isStrongTyping())
            pCtx.addError(new ErrorDetail(expr, start, true, "could not resolve class: " + typeDescr.getClassName()));
          return;
          // do nothing.
        }
      }

      if (egressType != null) {
        rewriteClassReferenceToFQCN(fields);
        if (typeDescr.isArray()) {
          try {
            egressType = egressType.isPrimitive() ?
                toPrimitiveArrayType(egressType) :
                findClass(null, repeatChar('[', typeDescr.getArrayLength()) + "L" + egressType.getName() + ";", pCtx);
          }
          catch (Exception e) {
            e.printStackTrace();
            // for now, don't handle this.
          }
        }
      }

      if (pCtx != null) {
        if (egressType == null) {
          pCtx.addError(new ErrorDetail(expr, start, true, "could not resolve class: " + typeDescr.getClassName()));
          return;
        }

        if (!typeDescr.isArray()) {
          String[] cnsResid = captureContructorAndResidual(expr, start, offset);

          final List<char[]> constructorParms
              = parseMethodOrConstructor(cnsResid[0].toCharArray());

          final Class[] parms = new Class[constructorParms.size()];
          for (int i = 0; i < parms.length; i++) {
            parms[i] = analyze(constructorParms.get(i), pCtx);
          }

          if (getBestConstructorCandidate(parms, egressType, true) == null) {
            if (pCtx.isStrongTyping())
              pCtx.addError(new ErrorDetail(expr, start, pCtx.isStrongTyping(), "could not resolve constructor " + typeDescr.getClassName()
                  + Arrays.toString(parms)));
          }

          if (cnsResid.length == 2) {
            String residualProperty =
                cnsResid[1].trim();

            if (residualProperty.length() == 0) return;

            this.egressType = new PropertyVerifier(residualProperty, pCtx, egressType).analyze();
          }
        }
      }
    }
  }

  private void rewriteClassReferenceToFQCN(int fields) {
    String FQCN = egressType.getName();

    if (typeDescr.getClassName().indexOf('.') == -1) {
      int idx = ArrayTools.findFirst('(', 0, name.length, name);

      char[] fqcn = FQCN.toCharArray();

      if (idx == -1) {
        this.name = new char[idx = fqcn.length];
        for (int i = 0; i < idx; i++)
          this.name[i] = fqcn[i];
      }
      else {
        char[] newName = new char[fqcn.length + (name.length - idx)];

        for (int i = 0; i < fqcn.length; i++)
          newName[i] = fqcn[i];

        int i0 = name.length - idx;
        int i1 = fqcn.length;
        for (int i = 0; i < i0; i++)
          newName[i + i1] = name[i + idx];

        this.name = newName;
      }

      this.typeDescr.updateClassName(name, 0, name.length, fields);
    }
  }

  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    if (newObjectOptimizer == null) {
      if (egressType == null) {
        /**
         * This means we couldn't resolve the type at the time this AST node was created, which means
         * we have to attempt runtime resolution.
         */

        if (factory != null && factory.isResolveable(typeDescr.getClassName())) {
          try {
            egressType = (Class) factory.getVariableResolver(typeDescr.getClassName()).getValue();
            rewriteClassReferenceToFQCN(COMPILE_IMMEDIATE);

            if (typeDescr.isArray()) {
              try {
                egressType = findClass(factory,
                    repeatChar('[', typeDescr.getArrayLength()) + "L" + egressType.getName() + ";", pCtx);
              }
              catch (Exception e) {
                // for now, don't handle this.
              }
            }

          }
          catch (ClassCastException e) {
            throw new CompileException("cannot construct object: " + typeDescr.getClassName()
                + " is not a class reference", expr, start, e);
          }
        }
      }

      if (typeDescr.isArray()) {
        return (newObjectOptimizer = new NewObjectArray(getBaseComponentType(egressType.getComponentType()), typeDescr.getCompiledArraySize()))
            .getValue(ctx, thisValue, factory);
      }

      try {
        AccessorOptimizer optimizer = getThreadAccessorOptimizer();

        ParserContext pCtx = this.pCtx;
        if (pCtx == null) {
          pCtx = new ParserContext();
          pCtx.getParserConfiguration().setAllImports(getInjectedImports(factory));
        }

        newObjectOptimizer = optimizer.optimizeObjectCreation(pCtx, name, 0, name.length, ctx, thisValue, factory);

        /**
         * Check to see if the optimizer actually produced the object during optimization.  If so,
         * we return that value now.
         */
        if (optimizer.getResultOptPass() != null) {
          egressType = optimizer.getEgressType();
          return optimizer.getResultOptPass();
        }
      }
      catch (CompileException e) {
        throw ErrorUtil.rewriteIfNeeded(e, expr, start);
      }
      finally {
        OptimizerFactory.clearThreadAccessorOptimizer();
      }
    }

    return newObjectOptimizer.getValue(ctx, thisValue, factory);
  }


  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    try {
      if (typeDescr.isArray()) {
        Class cls = findClass(factory, typeDescr.getClassName(), pCtx);

        int[] s = new int[typeDescr.getArrayLength()];
        ArraySize[] arraySize = typeDescr.getArraySize();

        for (int i = 0; i < s.length; i++) {
          s[i] = convert(eval(arraySize[i].value, ctx, factory), Integer.class);
        }

        return newInstance(cls, s);
      }
      else {
        String[] cnsRes = captureContructorAndResidual(name, 0, name.length);
        List<char[]> constructorParms = parseMethodOrConstructor(cnsRes[0].toCharArray());

        if (constructorParms != null) {
          Class cls = findClass(factory, new String(subset(name, 0, findFirst('(', 0, name.length, name))).trim(), pCtx);

          Object[] parms = new Object[constructorParms.size()];
          for (int i = 0; i < constructorParms.size(); i++) {
            parms[i] = eval(constructorParms.get(i), ctx, factory);
          }

          Constructor cns = getBestConstructorCandidate(parms, cls, false);

          if (cns == null)
            throw new CompileException("unable to find constructor for: " + cls.getName(), expr, start);

          for (int i = 0; i < parms.length; i++) {
            //noinspection unchecked
            parms[i] = convert(parms[i], cns.getParameterTypes()[i]);
          }

          if (cnsRes.length > 1) {
            return PropertyAccessor.get(cnsRes[1], cns.newInstance(parms), factory, thisValue, pCtx);
          }
          else {
            return cns.newInstance(parms);
          }
        }
        else {
          Constructor<?> cns = Class.forName(typeDescr.getClassName(), true, pCtx.getParserConfiguration().getClassLoader())
              .getConstructor(EMPTYCLS);

          if (cnsRes.length > 1) {
            return PropertyAccessor.get(cnsRes[1], cns.newInstance(), factory, thisValue, pCtx);
          }
          else {
            return cns.newInstance();
          }
        }
      }
    }
    catch (CompileException e) {
      throw e;
    }
    catch (ClassNotFoundException e) {
      throw new CompileException("unable to resolve class: " + e.getMessage(), expr, start, e);
    }
    catch (NoSuchMethodException e) {
      throw new CompileException("cannot resolve constructor: " + e.getMessage(), expr, start, e);
    }
    catch (Exception e) {
      throw new CompileException("could not instantiate class: " + e.getMessage(), expr, start, e);
    }
  }

  private boolean isPrototypeFunction() {
    return pCtx.getFunctions().containsKey(typeDescr.getClassName());
  }

  private Object createPrototypalObject(Object ctx, Object thisRef, VariableResolverFactory factory) {
    final Function function = pCtx.getFunction(typeDescr.getClassName());
    return function.getReducedValueAccelerated(ctx, thisRef, factory);
  }

  public static class NewObjectArray implements Accessor, Serializable {
    private ExecutableStatement[] sizes;
    private Class arrayType;

    public NewObjectArray(Class arrayType, ExecutableStatement[] sizes) {
      this.arrayType = arrayType;
      this.sizes = sizes;
    }

    public Object getValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory) {
      int[] s = new int[sizes.length];
      for (int i = 0; i < s.length; i++) {
        s[i] = convert(sizes[i].getValue(ctx, elCtx, variableFactory), Integer.class);
      }

      return newInstance(arrayType, s);
    }

    public Object setValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory, Object value) {
      return null;
    }

    public Class getKnownEgressType() {
      try {
        return Class.forName("[L" + arrayType.getName() + ";");
      }
      catch (ClassNotFoundException cne) {
        return null;
      }
    }
  }

  public TypeDescriptor getTypeDescr() {
    return typeDescr;
  }

  public Accessor getNewObjectOptimizer() {
    return newObjectOptimizer;
  }
}
