/*
 * Copyright 2003-2014 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.generator.impl.interpreted;

import jetbrains.mps.generator.impl.GenerationFailureException;
import jetbrains.mps.generator.impl.GeneratorUtil;
import jetbrains.mps.generator.impl.GeneratorUtilEx;
import jetbrains.mps.generator.impl.RuleUtil;
import jetbrains.mps.generator.impl.query.CallArgumentQuery;
import jetbrains.mps.generator.impl.query.GeneratorQueryProvider;
import jetbrains.mps.generator.impl.query.QueryKey;
import jetbrains.mps.generator.impl.query.QueryKeyImpl;
import jetbrains.mps.generator.runtime.TemplateContext;
import jetbrains.mps.generator.template.TemplateArgumentContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Runtime presentation of a template invocation. Handles arguments, prepares template context for a call.
 * @author Artem Tikhomirov
 */
public class TemplateCall {
  private final ArgumentExpression[] myArguments;
  private final String[] myParameters;
  // true to indicate no-op context, either no args/params, or their count doesn't match
  private final boolean myNoArgs;

  /**
   * At the moment, we handle ITemplateCall only, the reference to TemplateDeclaration with accompanying arguments.
   * Elements with references to TemplateDeclaration without arguments (like WeaveEach) shall not get here (although it's not a big deal
   * to handle it here, just keep myArguments empty, and return outerContext unchanged). The reason it's not done here as this knowledge
   * (which consequence is ITemplateCall) is static, and I don't want runtime checks for static knowledge
   * @param templateCall ITemplateCall node
   */
  public TemplateCall(@NotNull SNode templateCall) {
    final List<SNode> args = RuleUtil.getTemplateCall_Arguments(templateCall);
    myArguments = toExpressionRuntime(args);
    final SNode template = RuleUtil.getTemplateCall_Template(templateCall);
    String[] paramNames = RuleUtil.getTemplateDeclarationParameterNames(template);
    myParameters = paramNames == null ? new String[0] : paramNames;
    myNoArgs = myArguments.length == 0 || myArguments.length != myParameters.length;
  }

  /**
   * @return <code>true</code> iff there are arguments or parameters, but their count doesn't match
   */
  public boolean argumentsMismatch() {
    return myArguments.length != myParameters.length;
  }

  @NotNull
  public TemplateContext prepareCallContext(@NotNull TemplateContext outerContext) throws GenerationFailureException{
    if (myNoArgs) {
      return outerContext;
    }
    final Map<String, Object> vars = new HashMap<String, Object>(myArguments.length * 2);
    for (int i = 0; i < myArguments.length; i++) {
      Object value = myArguments[i].evaluate(outerContext);
      vars.put(myParameters[i], value);
    }
    // variables drop mapping label, hence need to reinstall it
    return outerContext.subContext(vars).subContext(outerContext.getInputName());
  }

  private static ArgumentExpression[] toExpressionRuntime(List<SNode> args) {
    final ArrayList<ArgumentExpression> ae = new ArrayList<ArgumentExpression>(args.size());
    int i = 1;
    for (SNode argExpr : args) {
      final SConcept argConcept = argExpr.getConcept();
      if (argConcept.isSubConceptOf(RuleUtil.concept_TemplateArgumentParameterExpression)) {
        ae.add(new TemplateParameterExpr(argExpr, i));
      } else if (argConcept.isSubConceptOf(RuleUtil.concept_TemplateArgumentPatternRef)) {
        ae.add(new PatternRefExpr(argExpr, i));
      } else if (argConcept.isSubConceptOf(RuleUtil.concept_TemplateArgumentQueryExpression)) {
        ae.add(new QueryExpr(argExpr));
      } else if (argConcept.isSubConceptOf(RuleUtil.concept_TemplateArgumentVarRefExpression)) {
        ae.add(new VarRefExpr(argExpr));
      } else if(GeneratorUtilEx.shallGenerateFunctionToEvaluate(argExpr)) {
        ae.add(new GeneratedExpr(argExpr));
      } else {
        ae.add(new OtherExpr(argExpr, i));
      }
      i++;
    }
    return ae.toArray(new ArgumentExpression[ae.size()]);
  }

  interface ArgumentExpression {
    public Object evaluate(TemplateContext context) throws GenerationFailureException;
  }

  // TemplateArgumentParameterExpression
  private static class TemplateParameterExpr implements ArgumentExpression {
    private final String myParameterName;
    private final SNode myParameterExpr;
    private final int myArgIndex;

    public TemplateParameterExpr(SNode parameterExpr, int index) {
      myParameterExpr = parameterExpr;
      myArgIndex = index;
      SNode parameter = RuleUtil.getTemplateArgumentParameterExpression_Parameter(parameterExpr);
      myParameterName = parameter == null ? null : parameter.getName();
    }
    @Override
    public Object evaluate(TemplateContext context) throws GenerationFailureException {
      if (myParameterName == null) {
        context.getEnvironment().getLogger().error(myParameterExpr.getReference(),
            "cannot evaluate template argument #" + (myArgIndex) + ": invalid parameter reference",
            GeneratorUtil.describeInput(context));
        return null;
      }
      return context.getVariable(myParameterName);
    }
  }

  // TemplateArgumentPatternRef
  private static class PatternRefExpr implements ArgumentExpression {
    private final SNode myPatternExpr;
    private final int myArgIndex;
    private final String myPatternVar;

    public PatternRefExpr(SNode patternExpr, int index) {
      myPatternExpr = patternExpr;
      myArgIndex = index;
      myPatternVar = GeneratorUtilEx.getPatternVariableName(patternExpr);
    }

    @Override
    public Object evaluate(TemplateContext context) throws GenerationFailureException {
      if (myPatternVar == null) {
        context.getEnvironment().getLogger().error(myPatternExpr.getReference(),
            "cannot evaluate template argument #" + (myArgIndex) + ": invalid pattern reference",
            GeneratorUtil.describeInput(context));
        return null;
      } else {
        // TODO FIXME using PatternVarsUtil directly, which is loaded by MPS
        return context.getPatternVariable(myPatternVar);
      }
    }
  }

  // TemplateArgumentQueryExpression
  private static class QueryExpr implements ArgumentExpression {
    private final QueryKey myQueryKey;
    private CallArgumentQuery myQuery;

    public QueryExpr(SNode queryExpr) {
      SNode query = RuleUtil.getTemplateArgumentQueryExpression_Query(queryExpr);
      myQueryKey = new QueryKeyImpl(queryExpr.getReference(), query.getNodeId());
    }

    protected QueryExpr(QueryKey key) {
      myQueryKey = key;
    }

    @Override
    public Object evaluate(TemplateContext context) throws GenerationFailureException {
      CallArgumentQuery q = myQuery;
      if (q == null) {
        // TODO perhaps, shall initialize it at construction time, rather than on first use?
        GeneratorQueryProvider queryProvider = context.getEnvironment().getQueryProvider(myQueryKey.getTemplateNode());
        q = myQuery = queryProvider.getTemplateCallArgumentQuery(myQueryKey);
      }
      return context.getEnvironment().getQueryExecutor().evaluate(q, new TemplateArgumentContext(context, myQueryKey.getTemplateNode()));
    }
  }

  // TemplateArgumentVariableRefExpression
  private static class VarRefExpr implements ArgumentExpression {
    private final String myMacroVarName;
    public VarRefExpr(SNode varRefExpression) {
      SNode varmacro = RuleUtil.getTemplateArgumentVarRef_VarMacro(varRefExpression);
      myMacroVarName = RuleUtil.getVarMacro_Name(varmacro);
    }
    @Override
    public Object evaluate(TemplateContext context) throws GenerationFailureException {
      return context.getVariable(myMacroVarName);
    }
  }

  // Expression, requires generated code according to GeneratorUtilEx.shallGenerateFunctionToEvaluate
  private static class GeneratedExpr extends QueryExpr implements ArgumentExpression {
    public GeneratedExpr(SNode expr) {
      // Here we utilize the fact we generate identical methods both for TemplateArgumentQueryExpression and plain Expressions,
      // and rely on the fact query provider needs only SNodeId to build a method name.
      super(new QueryKeyImpl(expr.getReference(), expr.getNodeId()));
    }
  }

  // Expression, primitive value. Unlike GeneratedExpr, doesn't require generated code to evaluate
  private static class OtherExpr implements ArgumentExpression {
    private final SNode myExpression;
    private final int myArgIndex;

    public OtherExpr(SNode expression, int index) {
      myExpression = expression;
      myArgIndex = index;
    }
    @Override
    public Object evaluate(TemplateContext context) throws GenerationFailureException {
      try {
        return GeneratorUtilEx.evaluateExpression(myExpression);
      } catch(IllegalArgumentException ex) {
        context.getEnvironment().getLogger().error(myExpression.getReference(),
            String.format("cannot evaluate template argument #%d: %s", myArgIndex, ex.toString()),
            GeneratorUtil.describeInput(context));
      }
      return null;
    }
  }
}
