/**
 * 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 java.lang.reflect.Array;

import org.mvel2.CompileException;
import org.mvel2.DataConversion;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.integration.impl.DefaultLocalVariableResolverFactory;
import org.mvel2.integration.impl.ItemResolverFactory;
import org.mvel2.util.ParseTools;

import static org.mvel2.util.ParseTools.createStringTrimmed;
import static org.mvel2.util.ParseTools.getBaseComponentType;
import static org.mvel2.util.ParseTools.subCompileExpression;

/**
 * @author Christopher Brock
 */
public class ForEachNode extends BlockNode {
  protected String item;
  protected Class itemType;

  protected ExecutableStatement condition;

  private static final int ITERABLE = 0;
  private static final int ARRAY = 1;
  private static final int CHARSEQUENCE = 2;
  private static final int INTEGER = 3;

  private int type = -1;

  public ForEachNode(char[] expr, int start, int offset, int blockStart, int blockOffset, int fields, ParserContext pCtx) {
    super(pCtx);

    handleCond(this.expr = expr, this.start = start, this.offset = offset, this.fields = fields, pCtx);
    this.blockStart = blockStart;
    this.blockOffset = blockOffset;

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      if (pCtx.isStrictTypeEnforcement() && itemType != null) {
        pCtx = pCtx.createSubcontext();
        pCtx.addInput(item, itemType);
      }

      pCtx.pushVariableScope();
      pCtx.makeVisible(item);

      this.compiledBlock = (ExecutableStatement) subCompileExpression(expr, blockStart, blockOffset, pCtx);

      pCtx.popVariableScope();
    }
  }

  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    ItemResolverFactory.ItemResolver itemR = new ItemResolverFactory.ItemResolver(item);
    ItemResolverFactory itemFactory = new ItemResolverFactory(itemR, new DefaultLocalVariableResolverFactory(factory));

    Object iterCond = condition.getValue(ctx, thisValue, factory);

    if (type == -1) {
      determineIterType(iterCond.getClass());
    }

    Object v;
    switch (type) {
      case ARRAY:
        int len = Array.getLength(iterCond);
        for (int i = 0; i < len; i++) {
          itemR.setValue(Array.get(iterCond, i));
          v = compiledBlock.getValue(ctx, thisValue, itemFactory);
          if (itemFactory.tiltFlag()) return v;
        }
        break;
      case CHARSEQUENCE:
        for (Object o : iterCond.toString().toCharArray()) {
          itemR.setValue(o);
          v = compiledBlock.getValue(ctx, thisValue, itemFactory);
          if (itemFactory.tiltFlag()) return v;
        }
        break;
      case INTEGER:
        int max = (Integer) iterCond + 1;
        for (int i = 1; i != max; i++) {
          itemR.setValue(i);
          v = compiledBlock.getValue(ctx, thisValue, itemFactory);
          if (itemFactory.tiltFlag()) return v;
        }
        break;

      case ITERABLE:
        for (Object o : (Iterable) iterCond) {
          itemR.setValue(o);
          v = compiledBlock.getValue(ctx, thisValue, itemFactory);
          if (itemFactory.tiltFlag()) return v;
        }

        break;
    }

    return null;
  }

  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    ItemResolverFactory.ItemResolver itemR = new ItemResolverFactory.ItemResolver(item);
    ItemResolverFactory itemFactory = new ItemResolverFactory(itemR, new DefaultLocalVariableResolverFactory(factory));

    Object iterCond = MVEL.eval(expr, start, offset, thisValue, factory);

    if (itemType != null && itemType.isArray())
      enforceTypeSafety(itemType, getBaseComponentType(iterCond.getClass()));

    this.compiledBlock = (ExecutableStatement) subCompileExpression(expr, blockStart, blockOffset, pCtx);

    Object v;
    if (iterCond instanceof Iterable) {
      for (Object o : (Iterable) iterCond) {
        itemR.setValue(o);
        v = compiledBlock.getValue(ctx, thisValue, itemFactory);
        if (itemFactory.tiltFlag()) return v;
      }
    }
    else if (iterCond != null && iterCond.getClass().isArray()) {
      int len = Array.getLength(iterCond);
      for (int i = 0; i < len; i++) {
        itemR.setValue(Array.get(iterCond, i));
        v = compiledBlock.getValue(ctx, thisValue, itemFactory);
        if (itemFactory.tiltFlag()) return v;
      }
    }
    else if (iterCond instanceof CharSequence) {
      for (Object o : iterCond.toString().toCharArray()) {
        itemR.setValue(o);
        v = compiledBlock.getValue(ctx, thisValue, itemFactory);
        if (itemFactory.tiltFlag()) return v;
      }
    }
    else if (iterCond instanceof Integer) {
      int max = (Integer) iterCond + 1;
      for (int i = 1; i != max; i++) {
        itemR.setValue(i);
        v = compiledBlock.getValue(ctx, thisValue, itemFactory);
        if (itemFactory.tiltFlag()) return v;
      }
    }
    else {
      throw new CompileException("non-iterable type: "
          + (iterCond != null ? iterCond.getClass().getName() : "null"), expr, start);
    }

    return null;
  }

  private void handleCond(char[] condition, int start, int offset, int fields, ParserContext pCtx) {
    int cursor = start;
    int end = start + offset;
    while (cursor < end && condition[cursor] != ':') cursor++;

    if (cursor == end || condition[cursor] != ':')
      throw new CompileException("expected : in foreach", condition, cursor);

    int x;
    if ((x = (item = createStringTrimmed(condition, start, cursor - start)).indexOf(' ')) != -1) {
      String tk = item.substring(0, x);
      try {
        itemType = ParseTools.findClass(null, tk, pCtx);
        item = item.substring( item.lastIndexOf( ' ' )+1, item.length() );
      }
      catch (ClassNotFoundException e) {
        throw new CompileException("cannot resolve identifier: " + tk, condition, start);
      }
    }

    // this.start = ++cursor;

    this.start = cursor + 1;
    this.offset = offset - (cursor - start) - 1;

    if ((fields & COMPILE_IMMEDIATE) != 0) {
      Class egress = (this.condition = (ExecutableStatement) subCompileExpression(expr, this.start, this.offset, pCtx)).getKnownEgressType();

      if (itemType != null && egress.isArray()) {
        enforceTypeSafety(itemType, getBaseComponentType(this.condition.getKnownEgressType()));
      }
      else if (pCtx.isStrongTyping()) {
        determineIterType(egress);
      }
    }
  }

  private void determineIterType(Class t) {
    if (Iterable.class.isAssignableFrom(t)) {
      type = ITERABLE;
    }
    else if (t.isArray()) {
      type = ARRAY;
    }
    else if (CharSequence.class.isAssignableFrom(t)) {
      type = CHARSEQUENCE;
    }
    else if (Integer.class.isAssignableFrom(t)) {
      type = INTEGER;
    }
    else {
      throw new CompileException("non-iterable type: " + t.getName(), expr, start);
    }
  }

  private void enforceTypeSafety(Class required, Class actual) {
    if (!required.isAssignableFrom(actual) && !DataConversion.canConvert(actual, required)) {
      throw new CompileException("type mismatch in foreach: expected: "
          + required.getName() + "; but found: " + getBaseComponentType(actual), expr, start);
    }
  }
}
