/**
 * 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.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.compiler.AbstractParser;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.optimizers.AccessorOptimizer;
import org.mvel2.optimizers.OptimizerFactory;
import org.mvel2.util.CollectionParser;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.mvel2.util.ParseTools.*;

/**
 * @author Christopher Brock
 */
public class InlineCollectionNode extends ASTNode {
  private Object collectionGraph;
  int trailingStart;
  int trailingOffset;

  public InlineCollectionNode(char[] expr, int start, int end, int fields, ParserContext pctx) {
    super(expr, start, end, fields | INLINE_COLLECTION);

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      parseGraph(true, null, pctx);
      try {
        AccessorOptimizer ao = OptimizerFactory.getThreadAccessorOptimizer();
        accessor = ao.optimizeCollection(pctx, collectionGraph, egressType, expr, trailingStart, trailingOffset, null, null, null);
        egressType = ao.getEgressType();
      }
      finally {
        OptimizerFactory.clearThreadAccessorOptimizer();
      }
    }
  }

  public InlineCollectionNode(char[] expr, int start, int end, int fields, Class type, ParserContext pctx) {
    super(expr, start, end, fields | INLINE_COLLECTION);

    this.egressType = type;

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      try {
        parseGraph(true, type, pctx);
        AccessorOptimizer ao = OptimizerFactory.getThreadAccessorOptimizer();
        accessor = ao.optimizeCollection(pctx, collectionGraph, egressType, expr, this.trailingStart, trailingOffset, null, null, null);
        egressType = ao.getEgressType();
      }
      finally {
        OptimizerFactory.clearThreadAccessorOptimizer();
      }
    }
  }

  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    if (accessor != null) {
      return accessor.getValue(ctx, thisValue, factory);
    }
    else {
      try {
        AccessorOptimizer ao = OptimizerFactory.getThreadAccessorOptimizer();
        if (collectionGraph == null) parseGraph(true, null, null);

        accessor = ao.optimizeCollection(AbstractParser.getCurrentThreadParserContext(), collectionGraph,
            egressType, expr, trailingStart, trailingOffset, ctx, thisValue, factory);
        egressType = ao.getEgressType();

        return accessor.getValue(ctx, thisValue, factory);
      }
      finally {
        OptimizerFactory.clearThreadAccessorOptimizer();
      }
    }

  }

  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    parseGraph(false, egressType, AbstractParser.getCurrentThreadParserContext());

    return execGraph(collectionGraph, egressType, ctx, factory);
  }

  private void parseGraph(boolean compile, Class type, ParserContext pCtx) {
    CollectionParser parser = new CollectionParser();

    if (type == null) {
      collectionGraph = ((List) parser.parseCollection(expr, start, offset, compile, pCtx)).get(0);
    }
    else {
      collectionGraph = ((List) parser.parseCollection(expr, start, offset, compile, type, pCtx)).get(0);
    }

    trailingStart = parser.getCursor() + 2;
    trailingOffset = offset - (trailingStart - start);

    if (this.egressType == null) this.egressType = collectionGraph.getClass();
  }

  private Object execGraph(Object o, Class type, Object ctx, VariableResolverFactory factory) {
    if (o instanceof List) {
      ArrayList list = new ArrayList(((List) o).size());

      for (Object item : (List) o) {
        list.add(execGraph(item, type, ctx, factory));
      }

      return list;
    }
    else if (o instanceof Map) {
      HashMap map = new HashMap();

      for (Object item : ((Map) o).keySet()) {
        map.put(execGraph(item, type, ctx, factory), execGraph(((Map) o).get(item), type, ctx, factory));
      }

      return map;
    }
    else if (o instanceof Object[]) {
      int dim = 0;

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

      Object newArray = Array.newInstance(getSubComponentType(type), ((Object[]) o).length);

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

        int c = 0;
        for (Object item : (Object[]) o) {
          Array.set(newArray, c++, execGraph(item, cls, ctx, factory));
        }

        return newArray;
      }
      catch (IllegalArgumentException e) {
        throw new CompileException("type mismatch in array", expr, start, e);
      }
      catch (ClassNotFoundException e) {
        throw new RuntimeException("this error should never throw:" + getBaseComponentType(type).getName(), e);
      }
    }
    else {
      if (type.isArray()) {
        return MVEL.eval((String) o, ctx, factory, getBaseComponentType(type));
      }
      else {
        return MVEL.eval((String) o, ctx, factory);
      }
    }
  }
}
