/**
 * 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.ParserContext;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.compiler.ExpressionCompiler;
import org.mvel2.integration.VariableResolver;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.integration.impl.DefaultLocalVariableResolverFactory;
import org.mvel2.integration.impl.FunctionVariableResolverFactory;
import org.mvel2.integration.impl.MapVariableResolverFactory;
import org.mvel2.integration.impl.StackDemarcResolverFactory;

import java.util.Map;

import static org.mvel2.util.ParseTools.parseParameterDefList;
import static org.mvel2.util.ParseTools.subCompileExpression;


@SuppressWarnings({"unchecked"})
public class Function extends ASTNode implements Safe {
  protected String name;
  protected ExecutableStatement compiledBlock;

  protected String[] parameters;
  protected int parmNum;
  protected boolean compiledMode = false;
  protected boolean singleton;

  public Function(String name,
                  char[] expr,
                  int start,
                  int offset,
                  int blockStart,
                  int blockOffset,
                  int fields,
                  ParserContext pCtx) {

    super(pCtx);
    if ((this.name = name) == null || name.length() == 0) {
      this.name = null;
    }
    this.expr = expr;

    parmNum = (this.parameters = parseParameterDefList(expr, start, offset)).length;

    //pCtx.declareFunction(this);

    ParserContext ctx = new ParserContext(pCtx.getParserConfiguration(), pCtx, true);

    if (!pCtx.isFunctionContext()) {
      singleton = true;
      pCtx.declareFunction(this);
    }
    else {
      ctx.declareFunction(this);
    }

    /**
     * To prevent the function parameters from being counted as
     * external inputs, we must add them explicitly here.
     */
    for (String s : this.parameters) {
      ctx.addVariable(s, Object.class);
      ctx.addIndexedInput(s);
    }

    /**
     * Compile the expression so we can determine the input-output delta.
     */
    ctx.setIndexAllocation(false);
    ExpressionCompiler compiler = new ExpressionCompiler(expr, blockStart, blockOffset, ctx);
    compiler.setVerifyOnly(true);
    compiler.compile();

    ctx.setIndexAllocation(true);

    /**
     * Add globals as inputs
     */
    if (pCtx.getVariables() != null) {
      for (Map.Entry<String, Class> e : pCtx.getVariables().entrySet()) {
        ctx.getVariables().remove(e.getKey());
        ctx.addInput(e.getKey(), e.getValue());
      }

      ctx.processTables();
    }

    ctx.addIndexedInputs(ctx.getVariables().keySet());
    ctx.getVariables().clear();

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

    this.parameters = new String[ctx.getIndexedInputs().size()];

    int i = 0;
    for (String s : ctx.getIndexedInputs()) {
      this.parameters[i++] = s;
    }

    compiledMode = (fields & COMPILE_IMMEDIATE) != 0;

    this.egressType = this.compiledBlock.getKnownEgressType();

    pCtx.addVariable(name, Function.class);
  }

  public Object getReducedValueAccelerated(Object ctx, Object thisValue, VariableResolverFactory factory) {
    PrototypalFunctionInstance instance = new PrototypalFunctionInstance(this, new MapVariableResolverFactory());
    if (name != null) {
      if (!factory.isIndexedFactory() && factory.isResolveable(name))
        throw new CompileException("duplicate function: " + name, expr, start);

      factory.createVariable(name, instance);
    }
    return instance;
  }

  public Object getReducedValue(Object ctx, Object thisValue, VariableResolverFactory factory) {
    PrototypalFunctionInstance instance = new PrototypalFunctionInstance(this, new MapVariableResolverFactory());
    if (name != null) {
      if (!factory.isIndexedFactory() && factory.isResolveable(name))
        throw new CompileException("duplicate function: " + name, expr, start);
      factory.createVariable(name, instance);
    }
    return instance;
  }

  public Object call(Object ctx, Object thisValue, VariableResolverFactory factory, Object[] parms) {
    if (parms != null && parms.length != 0) {
      // detect tail recursion
      if (factory instanceof FunctionVariableResolverFactory
          && ((FunctionVariableResolverFactory) factory).getIndexedVariableResolvers().length == parms.length) {
        FunctionVariableResolverFactory fvrf = (FunctionVariableResolverFactory) factory;
        if (fvrf.getFunction().equals(this)) {
          VariableResolver[] swapVR = fvrf.getIndexedVariableResolvers();
          fvrf.updateParameters(parms);
          try {
            return compiledBlock.getValue(ctx, thisValue, fvrf);
          }
          finally {
            fvrf.setIndexedVariableResolvers(swapVR);
          }
        }
      }
      return compiledBlock.getValue(thisValue,
          new StackDemarcResolverFactory(new FunctionVariableResolverFactory(this, factory, parameters, parms)));
    }
    else if (compiledMode) {
      return compiledBlock.getValue(thisValue,
          new StackDemarcResolverFactory(new DefaultLocalVariableResolverFactory(factory, parameters)));
    }
    else {
      return compiledBlock.getValue(thisValue,
          new StackDemarcResolverFactory(new DefaultLocalVariableResolverFactory(factory, parameters)));
    }

  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String[] getParameters() {
    return parameters;
  }

  public boolean hasParameters() {
    return this.parameters != null && this.parameters.length != 0;
  }

  public void checkArgumentCount(int passing) {
    if (passing != parmNum) {
      throw new CompileException("bad number of arguments in function call: "
          + passing + " (expected: " + (parmNum == 0 ? "none" : parmNum) + ")", expr, start);
    }
  }

  public ExecutableStatement getCompiledBlock() {
    return compiledBlock;
  }

  public String toString() {
    return "FunctionDef:" + (name == null ? "Anonymous" : name);
  }
}

