/*
 * Copyright 2003-2017 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;

import jetbrains.mps.generator.GenerationCanceledException;
import jetbrains.mps.generator.GenerationSessionContext;
import jetbrains.mps.generator.GenerationTrace;
import jetbrains.mps.generator.GenerationTracerUtil;
import jetbrains.mps.generator.IGeneratorLogger;
import jetbrains.mps.generator.impl.MapSrcProcessor.MapSrcMacroProcessorInterpreted;
import jetbrains.mps.generator.impl.RoleValidation.RoleValidator;
import jetbrains.mps.generator.impl.RoleValidation.Status;
import jetbrains.mps.generator.impl.interpreted.TemplateCall;
import jetbrains.mps.generator.impl.query.GeneratorQueryProvider;
import jetbrains.mps.generator.impl.query.IfMacroCondition;
import jetbrains.mps.generator.impl.query.InsertMacroQuery;
import jetbrains.mps.generator.impl.query.MapNodeQuery;
import jetbrains.mps.generator.impl.query.MapPostProcessor;
import jetbrains.mps.generator.impl.query.QueryKeyImpl;
import jetbrains.mps.generator.impl.query.QueryProviderBase;
import jetbrains.mps.generator.impl.query.SourceNodeQuery;
import jetbrains.mps.generator.impl.query.SourceNodesQuery;
import jetbrains.mps.generator.impl.query.VariableValueQuery;
import jetbrains.mps.generator.impl.query.WeaveAnchorQuery;
import jetbrains.mps.generator.impl.template.QueryExecutor;
import jetbrains.mps.generator.runtime.GenerationException;
import jetbrains.mps.generator.runtime.TemplateContext;
import jetbrains.mps.generator.runtime.TemplateExecutionEnvironment;
import jetbrains.mps.generator.runtime.TemplateSwitchMapping;
import jetbrains.mps.generator.runtime.WeavingWithAnchor;
import jetbrains.mps.generator.template.ITemplateProcessor;
import jetbrains.mps.generator.template.IfMacroContext;
import jetbrains.mps.generator.template.InsertMacroContext;
import jetbrains.mps.generator.template.SourceSubstituteMacroNodeContext;
import jetbrains.mps.generator.template.SourceSubstituteMacroNodesContext;
import jetbrains.mps.generator.template.TemplateVarContext;
import jetbrains.mps.generator.template.WeavingAnchorContext;
import jetbrains.mps.smodel.NodeReadEventsCaster;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.textgen.trace.TracingUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Applies template to input node.
 * Generally, there's single template processor per generation step as it keeps runtime presentation of template model being applied.
 * Rule that need to apply template might access instance of {@link jetbrains.mps.generator.impl.TemplateProcessor} via
 * {@link jetbrains.mps.generator.runtime.TemplateExecutionEnvironment#getTemplateProcessor()}
 * TODO make MacroNode aware of container TemplateNode and don't pass both arguments when only one is sufficient
 */
public final class TemplateProcessor implements ITemplateProcessor {
  private final TemplateGenerator myGenerator;
  private final SModel myOutputModel;
  private final MacroImplFactory myImplFactory;
  private final Map<SNode, TemplateNode> myTemplateRuntimeMap = new ConcurrentHashMap<SNode, TemplateNode>();

  public TemplateProcessor(@NotNull TemplateGenerator generator) {
    myGenerator = generator;
    myOutputModel = myGenerator.getOutputModel();
    myImplFactory = new MacroImplFactory(this);
  }

  /*package*/ TemplateGenerator getGenerator() {
    return myGenerator;
  }
  /*package*/ GeneratorQueryProvider getQueryProvider(SNodeReference templateNode) {
    return myGenerator.getQueryProvider(templateNode);
  }

  @Override
  @NotNull
  public List<SNode> apply(@NotNull SNode templateNode, @NotNull TemplateContext context) throws DismissTopMappingRuleException, GenerationFailureException,
      GenerationCanceledException {
    if (myGenerator.isIncremental()) {
      // turn off tracing
      NodeReadEventsCaster.setNodesReadListener(null);
    }
    try {
      final TemplateNode rtTemplateNode = getTemplateNodeRuntime(templateNode);
      if (rtTemplateNode.getFirstMacro() != null) {
        return applyMacro(rtTemplateNode.getFirstMacro(), context);
      } else {
        return applyTemplate(rtTemplateNode, context);
      }
    } finally {
      if (myGenerator.isIncremental()) {
        // restore tracing
        NodeReadEventsCaster.removeNodesReadListener();
      }
    }
  }

  @NotNull
  /*package*/List<SNode> applyMacro(@NotNull MacroNode rtMacro, @NotNull TemplateContext context)
      throws DismissTopMappingRuleException, GenerationFailureException, GenerationCanceledException {

    return rtMacro.apply(context.subContext(rtMacro.getMappingLabel()));
  }

  @NotNull
  /*package*/List<SNode> applyTemplate(@NotNull TemplateNode rtTemplateNode, @NotNull TemplateContext context)
      throws DismissTopMappingRuleException, GenerationFailureException, GenerationCanceledException {

    final TemplateExecutionEnvironment env = context.getEnvironment();
    SNode outputNode = env.createOutputNode(rtTemplateNode.getConcept());

    // use same env method as reduce_TemplateNode does
    // XXX reduce_TemplateNode looks into incoming references now, not to save template node id if there are no
    //     references inside template model (and hence no attempt to restore the reference using template node id)
    //     Would be great to do smth similar here
    env.nodeCopied(context, outputNode, rtTemplateNode.getTemplateNodeId());
    env.registerLabel(context.getInput(), outputNode, context.getInputName()); // XXX reduce_TemplateNode doesn't do that

    rtTemplateNode.apply(context, outputNode);

    // process children
    context = context.subContext(); // drop label
    for (SNode templateChildNode : rtTemplateNode.getChildTemplates()) {
      TemplateNode rtTemplateChildNode = getTemplateNodeRuntime(templateChildNode);
      final List<SNode> outputChildNodes;
      if(rtTemplateChildNode.getFirstMacro() != null) {
        outputChildNodes = applyMacro(rtTemplateChildNode.getFirstMacro(), context);
      } else {
        outputChildNodes = applyTemplate(rtTemplateChildNode, context);
      }
      SContainmentLink role = rtTemplateChildNode.getRoleInParent();
      RoleValidator validator = myGenerator.getChildRoleValidator(outputNode, role);
      for (SNode outputChildNode : outputChildNodes) {
        // check child
        Status status = validator.validate(outputChildNode);
        if (status != null) {
          myGenerator.getLogger().warning(rtTemplateChildNode.getTemplateNodeReference(), status.getMessage("apply template"), status.describe(
              GeneratorUtil.describe(context.getInput(), "input"),
              GeneratorUtil.describe(outputNode, "output"),
              GeneratorUtil.describe(rtTemplateNode.getTemplateNodeReference(), "template node")
          ));
        }
        outputNode.addChild(role, outputChildNode);
      }
    }
    return Collections.singletonList(outputNode);
  }

  private TemplateNode getTemplateNodeRuntime(SNode templateNode) {
    // template nodes may belong to different models, hence can't use anything simpler than and identity that includes model
    // and since template models don't change during generation, Object identity is enough as a key.
    @SuppressWarnings("redundant")
    final SNode key = templateNode;
    TemplateNode rv = myTemplateRuntimeMap.get(key);
    if (rv == null) {
      rv = new TemplateNode(templateNode, myImplFactory);
      myTemplateRuntimeMap.put(key, rv);
    }
    return rv;
   }

  private static class MacroImplFactory implements MacroNode.Factory {
    private final TemplateProcessor myTemplateProcessor;
    private final Map<SConcept, Integer> macroImplMap = new HashMap<SConcept, Integer>(32);

    MacroImplFactory(@NotNull TemplateProcessor templateProcessor) {
      myTemplateProcessor = templateProcessor;
      macroImplMap.put(RuleUtil.concept_NodeMacro, 0);
      macroImplMap.put(RuleUtil.concept_LoopMacro, 1);
      macroImplMap.put(RuleUtil.concept_CopySrcNodeMacro, 2);
      macroImplMap.put(RuleUtil.concept_CopySrcListMacro, 3);
      macroImplMap.put(RuleUtil.concept_InsertMacro, 4);
      macroImplMap.put(RuleUtil.concept_WeaveMacro, 5);
      macroImplMap.put(RuleUtil.concept_LabelMacro, 6);
      macroImplMap.put(RuleUtil.concept_VarMacro, 7);
      macroImplMap.put(RuleUtil.concept_IfMacro, 8);
      macroImplMap.put(RuleUtil.concept_MapSrcNodeMacro, 9);
      macroImplMap.put(RuleUtil.concept_MapSrcListMacro, 10);
      macroImplMap.put(RuleUtil.concept_TemplateSwitchMacro, 12);
      macroImplMap.put(RuleUtil.concept_IncludeMacro, 13);
      macroImplMap.put(RuleUtil.concept_TemplateCallMacro, 14);
      macroImplMap.put(RuleUtil.concept_TraceMacro, 15);
      macroImplMap.put(RuleUtil.concept_ExportMacro, 16);
    }

    @Override
    public MacroNode create(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next) {
      Integer k = macroImplMap.get(macro.getConcept());
      if (k == null) {
        return new NoMacro(macro, templateNode, next, myTemplateProcessor);
      }
      switch(k) {
        case 0 : return new NoMacro(macro, templateNode, next, myTemplateProcessor);
        case 1 : return new LoopMacro(macro, templateNode, next, myTemplateProcessor);
        case 2 : return new CopySrcMacros(macro, templateNode, next, myTemplateProcessor, true);
        case 3 : return new CopySrcMacros(macro, templateNode, next, myTemplateProcessor, false);
        case 4 : return new InsertMacro(macro, templateNode, next, myTemplateProcessor);
        case 5 : return new WeaveMacro(macro, templateNode, next, myTemplateProcessor);
        case 6 : return new LabelMacro(macro, templateNode, next, myTemplateProcessor);
        case 7 : return new VarMacro(macro, templateNode, next, myTemplateProcessor);
        case 8 : return new IfMacro(macro, templateNode, next, myTemplateProcessor);
        case 9 : return new MapSrcMacros(macro, templateNode, next, myTemplateProcessor, true);
        case 10 : return new MapSrcMacros(macro, templateNode, next, myTemplateProcessor, false);
        case 12 : return new SwitchMacro(macro, templateNode, next, myTemplateProcessor);
        case 13 : return new IncludeMacro(macro, templateNode, next, myTemplateProcessor);
        case 14 : return new CallMacro(macro, templateNode, next, myTemplateProcessor);
        case 15 : return new TraceMacro(macro, templateNode, next, myTemplateProcessor);
        case 16 : return new ExportMacro(macro, templateNode, next, myTemplateProcessor);
        default: return new NoMacro(macro, templateNode, next, myTemplateProcessor);
      }
    }
  }

  /**
   * Record value with name
   */
  private static class ExportMacro extends MacroImpl {
    public ExportMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      final List<SNode> value = nextMacro(templateContext);
      final GenerationSessionContext ctx = myTemplateProcessor.getGenerator().getGeneratorSessionContext();
      ctx.getExports().record(templateContext, macro.getReferenceTarget("label"), value);
      return value;
    }
  }

  private static abstract class MacroImpl implements MacroNode {
    protected final TemplateProcessor myTemplateProcessor; // XXX now, with macro and tn as fields, perhaps shall pass TP as an argument to apply?
    protected final SNode macro;
    protected final TemplateNode templateNode;
    private final SNodeReference myMacroNodeRef;
    private final String myMappingLabel;
    private final MacroNode myNextMacro;

    protected MacroImpl(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      this.macro = macro;
      this.templateNode = templateNode;
      myNextMacro = next;
      myTemplateProcessor = templateProcessor;
      myMacroNodeRef = new SNodePointer(macro);
      myMappingLabel = GeneratorUtilEx.getMappingName_NodeMacro(macro, null);
    }

    @Override
    public final MacroNode getNextMacro() {
      return myNextMacro;
    }

    @NotNull
    @Override
    public final SNodeReference getMacroNodeRef() {
      return myMacroNodeRef;
    }

    @Nullable
    @Override
    public final String getMappingLabel() {
      // instead of this accessor may implement #apply here, and delegate to another method in subclass with updated context
      return myMappingLabel;
    }

    protected final List<SNode> nextMacro(TemplateContext context)
        throws GenerationFailureException, DismissTopMappingRuleException, GenerationCanceledException {
      if (getNextMacro() != null) {
        return myTemplateProcessor.applyMacro(getNextMacro(), context);
      } else {
        return myTemplateProcessor.applyTemplate(templateNode, context);
      }
    }

    protected final IGeneratorLogger getLogger() {
      return myTemplateProcessor.getGenerator().getLogger();
    }
  }

  // could be standalone facility, not an element in class hierarchy
  private static abstract class MacroWithInput extends MacroImpl {
    private volatile SourceNodeQuery mySourceNodeQuery;
    private volatile SourceNodesQuery mySourceNodesQuery;

    protected MacroWithInput(SNode macro, TemplateNode templateNode, MacroNode next, TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    protected final SNode getNewInputNode(@NotNull TemplateContext context) throws GenerationFailureException {
      SourceNodeQuery q = mySourceNodeQuery;
      if (q == null) {
        synchronized (this) {
          if ((q = mySourceNodeQuery) == null) {
            q = mySourceNodeQuery = createNodeQuery();
          }
        }
      }
      final QueryExecutor qe = context.getEnvironment().getQueryExecutor();
      return qe.evaluate(q, new SourceSubstituteMacroNodeContext(context, getMacroNodeRef()));
    }

    protected final Collection<SNode> getNewInputNodes(@NotNull TemplateContext context) throws GenerationFailureException {
      SourceNodesQuery q = mySourceNodesQuery;
      if (q == null) {
        synchronized (this) {
          if ((q = mySourceNodesQuery) == null) {
            q = mySourceNodesQuery = createNodesQuery();
          }
        }
      }
      if (q == null) {
        getLogger().error(getMacroNodeRef(), "couldn't get input nodes", GeneratorUtil.describeInput(context));
        throw new GenerationFailureException("couldn't get input nodes");
      }
      final QueryExecutor qe = context.getEnvironment().getQueryExecutor();
      final Collection<SNode> result = qe.evaluate(q, new SourceSubstituteMacroNodesContext(context, getMacroNodeRef()));
      checkInputNodesForNulls(context, result);
      return result;
    }

    static <T> List<T> wrapAsList(T node) {
      return node == null ? Collections.<T>emptyList() : Collections.singletonList(node);
    }

    private void checkInputNodesForNulls(TemplateContext context, Iterable<SNode> result) throws GenerationFailureException {
      for (SNode n : result) {
        if (n == null) {
          final String msg = String.format("Unexpected null value among new input nodes in %s macro", macro.getPresentation());
          context.getEnvironment().getLogger().error(getMacroNodeRef(), msg, GeneratorUtil.describeInput(context));
          throw new GenerationFailureException(msg);
        }
      }
    }

    private SourceNodeQuery createNodeQuery() {
      SNode query = RuleUtil.getSourceNodeQuery(macro);
      if (query != null) {
        return myTemplateProcessor.getQueryProvider(getMacroNodeRef()).getSourceNodeQuery(new QueryKeyImpl(getMacroNodeRef(), query.getNodeId(), query));
      } else {
        // <default> : propagate  current input node
        return new SourceNodeQuery() {
          @Nullable
          @Override
          public SNode evaluate(@NotNull SourceSubstituteMacroNodeContext context) throws GenerationFailureException {
            return context.getInputNode();
          }
        };
      }
    }

    // return null iff there's no sourceNodesQuery but macro does require input
    private SourceNodesQuery createNodesQuery() {
      SNode nodesQuery = RuleUtil.getSourceNodesQuery(macro);
      if (nodesQuery != null) {
        return myTemplateProcessor.getQueryProvider(getMacroNodeRef()).getSourceNodesQuery(new QueryKeyImpl(getMacroNodeRef(), nodesQuery.getNodeId(), nodesQuery));
      }
      if (RuleUtil.doesMacroRequireInput(macro)) {
        return null;
      }
      // <default> : propagate  current input node
      return new SourceNodesQuery() {
        @NotNull
        @Override
        public Collection<SNode> evaluate(@NotNull SourceSubstituteMacroNodesContext context) throws GenerationFailureException {
          return wrapAsList(context.getInputNode());
        }
      };
    }
  }

  // $LOOP$
  private static class LoopMacro extends MacroWithInput {
    LoopMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      Collection<SNode> newInputNodes = getNewInputNodes(templateContext);
      if (newInputNodes.isEmpty()) {
        return Collections.emptyList();
      }
      final GenerationTrace trace = templateContext.getEnvironment().getTrace();
      String counterVarName = RuleUtil.getLoopMacroCounterVarName(macro);
      ArrayList<SNode> outputNodes = new ArrayList<SNode>();
      int i = 0;
      for (SNode newInputNode : newInputNodes) {
        TemplateContext ctx = templateContext;
        if (counterVarName != null) {
          ctx = ctx.withVariable("cv:" + counterVarName, i);
        }
        ctx = ctx.subContext(newInputNode);
        List<SNode> _outputNodes = nextMacro(ctx);
        outputNodes.addAll(_outputNodes);
        i++;
        trace.trace(newInputNode.getNodeId(), GenerationTracerUtil.translateOutput(_outputNodes), getMacroNodeRef());
      }
      return outputNodes;
    }
  }

  // $COPY-SRC$ / $COPY-SRCL$
  private static class CopySrcMacros extends MacroWithInput {
    private final boolean myIsSoleInput;

    protected CopySrcMacros(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor, boolean soleInput) {
      super(macro, templateNode, next, templateProcessor);
      myIsSoleInput = soleInput;
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      final Collection<SNode> newInputNodes;
      if (myIsSoleInput) {
        newInputNodes = wrapAsList(getNewInputNode(templateContext));
      } else {
        newInputNodes = getNewInputNodes(templateContext);
      }
      SNodeReference templateNodeRef = templateNode.getTemplateNodeReference();
      String tempNodeId = templateNode.getTemplateNodeId();
      return templateContext.getEnvironment().copyNodes(newInputNodes, templateNodeRef, tempNodeId, templateContext);
    }
  }

  // $INSERT$
  private static class InsertMacro extends MacroImpl {
    private final InsertMacroQuery myQuery;

    protected InsertMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
      QueryKeyImpl qk = new QueryKeyImpl(getMacroNodeRef(), RuleUtil.getInsertMacro_Query(macro).getNodeId());
      myQuery = templateProcessor.getQueryProvider(getMacroNodeRef()).getInsertMacroQuery(qk);
    }

    private SNode getNodeToInsert(TemplateContext context) throws GenerationFailureException {
      return context.getEnvironment().getQueryExecutor().evaluate(myQuery, new InsertMacroContext(context, getMacroNodeRef()));
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      SNode child = getNodeToInsert(templateContext);
      if (child != null) {
        child = templateContext.getEnvironment().insertNode(child, getMacroNodeRef(), templateContext);
        // XXX TEEI.insertNode doesn't register ML, perhaps shall behave the same as this code? Or it's done in generated code?
        // label
        myTemplateProcessor.getGenerator().registerMappingLabel(templateContext.getInput(), templateContext.getInputName(), child);
        return Collections.singletonList(child);
      }
      return Collections.emptyList();
    }
  }

  // $WEAVE$
  private static class WeaveMacro extends MacroWithInput implements WeavingWithAnchor {
    private WeaveAnchorQuery myAnchorQuery;

    protected WeaveMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);

    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      List<SNode> _outputNodes = nextMacro(templateContext);
      if (_outputNodes.isEmpty()) {
        return Collections.emptyList();
      }
      if (_outputNodes.size() > 1) {
        getLogger().error(macro.getReference(), "cannot apply $WEAVE$ to a list of nodes",
            GeneratorUtil.describe(templateContext.getInput(), "input"));
        return _outputNodes;
      }
      SNode consequence = RuleUtil.getWeaveMacro_Consequence(macro);
      if (consequence == null) {
        getLogger().error(macro.getReference(), "couldn't evaluate weave macro: no consequence",
            GeneratorUtil.describeIfExists(templateContext.getInput(), "input"));
        return _outputNodes;
      }

      SNode template = RuleUtil.getTemplateDeclarationReference_Template(consequence);
      ////
      if (template == null) {
        getLogger().error(macro.getReference(), "couldn't evaluate weave macro: no template",
            GeneratorUtil.describeIfExists(templateContext.getInput(), "input"));
        return _outputNodes;
      }
      WeaveTemplateContainer wtc = new WeaveTemplateContainer(template, this);
      wtc.initialize(getLogger());

      SNode contextNode = _outputNodes.get(0);
      for (SNode node : getNewInputNodes(templateContext)) {
        wtc.apply(contextNode, templateContext.subContext(node));
      }
      return _outputNodes;
    }

    @Nullable
    @Override
    public SNode getAnchorNode(@NotNull TemplateContext context, @NotNull SNode outputParent, @NotNull SNode outputNode) throws GenerationFailureException {
      if (myAnchorQuery == null) {
        myAnchorQuery = context.getEnvironment().getQueryProvider(getMacroNodeRef()).getWeaveAnchorQuery(macro);
      }
      return myAnchorQuery.anchorNode(new WeavingAnchorContext(context, getMacroNodeRef(), outputParent, outputNode));
    }
  }

  // $LABEL$
  private static class LabelMacro extends MacroImpl {
    protected LabelMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      return nextMacro(templateContext);
    }
  }

  // $VAR$
  private static class VarMacro extends MacroImpl {
    private final VariableValueQuery myValueQuery;

    protected VarMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
      QueryKeyImpl qk = new QueryKeyImpl(getMacroNodeRef(), RuleUtil.getVarMacro_Query(macro).getNodeId());
      myValueQuery = templateProcessor.getQueryProvider(getMacroNodeRef()).getVariableValueQuery(qk);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      String varName = RuleUtil.getVarMacro_Name(macro);
      Object varValue = templateContext.getEnvironment().getQueryExecutor().evaluate(myValueQuery, new TemplateVarContext(templateContext, getMacroNodeRef()));
      TemplateContext newContext = templateContext.subContext(Collections.singletonMap(varName, varValue));

      // tc.subContext(Map props) doesn't save mapping label, so "LABEL aaa VAR bb <templateNode>" fails to
      // establish mapping aaa:templateNode. However, instead of passing ML here once again, shall consider updating subContext(Map)
      // contract to preserve mapping label. Can't do it without thorough check of the method usage in generated templates
      return nextMacro(newContext.subContext(templateContext.getInputName()));
    }
  }

  // $IF$
  private static class IfMacro extends MacroImpl {
    private final IfMacroCondition myCondition;
    private final RuleConsequenceProcessor myAlternativeConsequence;

    protected IfMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
      SNode alternativeConsequence = RuleUtil.getIfMacro_AlternativeConsequence(macro);
      myAlternativeConsequence = alternativeConsequence == null ? null : RuleConsequenceProcessor.prepare(alternativeConsequence);
      SNode function = RuleUtil.getIfMacro_ConditionFunction(macro);
      if (function != null) {
        myCondition =
            templateProcessor.getQueryProvider(getMacroNodeRef()).getIfMacroCondition(new QueryKeyImpl(getMacroNodeRef(), function.getNodeId(), macro));
      } else {
        myCondition = new QueryProviderBase.Missing(macro);
      }
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext context)
        throws DismissTopMappingRuleException, GenerationFailureException, GenerationCanceledException {
      final QueryExecutor queryExecutor = context.getEnvironment().getQueryExecutor();
      if (queryExecutor.evaluate(myCondition, new IfMacroContext(context, getMacroNodeRef()))) {
        return nextMacro(context);
      } else {
        // alternative consequence
        if (myAlternativeConsequence == null) {
          return Collections.emptyList();
        }
        try {
          return myAlternativeConsequence.processRuleConsequence(context);
        } catch (AbandonRuleInputException ex) {
          // it's ok. just ignore
          return Collections.emptyList();
        }
      }
    }
  }

  // $MAP-SRC$ or $MAP-SRCL$
  private static class MapSrcMacros extends MacroWithInput {

    private final boolean myIsSoleInput;

    protected MapSrcMacros(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor, boolean soleInput) {
      super(macro, templateNode, next, templateProcessor);
      myIsSoleInput = soleInput;
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      final Collection<SNode> newInputNodes;
      if (myIsSoleInput) {
        newInputNodes = wrapAsList(getNewInputNode(templateContext));
      } else {
        newInputNodes = getNewInputNodes(templateContext);
      }
      if (newInputNodes.isEmpty()) {
        return Collections.emptyList();
      }
      GeneratorQueryProvider queryProvider = myTemplateProcessor.getQueryProvider(getMacroNodeRef());
      SNode mf = RuleUtil.getMapSrc_MapperFunction(macro);
      SNode ppf = RuleUtil.getMapSrc_PostMapperFunction(macro);
      MapNodeQuery mapNodeQuery = mf == null ? null : queryProvider.getMapNodeQuery(new QueryKeyImpl(getMacroNodeRef(), mf.getNodeId()));
      MapPostProcessor postProcessor = ppf == null ? null : queryProvider.getMapPostProcessor(new QueryKeyImpl(getMacroNodeRef(), ppf.getNodeId()));
      // it's perfectly legal to have neither mapNodeQuery nor postProcessor
      final TemplateExecutionEnvironment env = templateContext.getEnvironment();
      ArrayList<SNode> outputNodes = new ArrayList<SNode>(newInputNodes.size());
      final DelayedChanges delayedChanges = myTemplateProcessor.getGenerator().getDelayedChanges();
      for (SNode newInputNode : newInputNodes) {
        TemplateContext newcontext = templateContext.subContext(newInputNode);
        if (mapNodeQuery != null) {
          SNode childToReplaceLater = env.createOutputNode(templateNode.getConcept());
          outputNodes.add(childToReplaceLater);
          // execute the 'mapper' function later
          delayedChanges.add(new MapSrcMacroProcessorInterpreted(mapNodeQuery, postProcessor, getMacroNodeRef(), childToReplaceLater, newcontext));
        } else {
          List<SNode> _outputNodes = nextMacro(newcontext);
          outputNodes.addAll(_outputNodes);
          if (postProcessor != null) {
            for (SNode outputNode : _outputNodes) {
              delayedChanges.add(new MapSrcMacroProcessorInterpreted(postProcessor, getMacroNodeRef(), outputNode, newcontext));
            }
          }
        }
      }
      return outputNodes;
    }
  }

  // $SWITCH$
  private static class SwitchMacro extends MacroWithInput {
    private volatile TemplateCall myCallProcessor;

    protected SwitchMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    protected SNode getTemplateSwitch() {
      return RuleUtil.getTemplateSwitchMacro_TemplateSwitch(macro);
    }

    protected TemplateContext prepareContext(TemplateContext templateContext) throws GenerationFailureException {
      TemplateCall tc = myCallProcessor;
      if (tc == null) {
        tc = new TemplateCall(macro);
        if (tc.argumentsMismatch()) {
          getLogger().error(getMacroNodeRef(), "number of arguments doesn't match template", GeneratorUtil.describeInput(templateContext));
          // fall-through
        }
        myCallProcessor = tc;
      }
      return tc.prepareCallContext(templateContext);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      SNode templateSwitch = getTemplateSwitch();
      if (templateSwitch == null) {
        throw new TemplateProcessingFailureException(macro, "error processing $SWITCH$ - bad TemplateSwitch reference",
            GeneratorUtil.describeInput(templateContext));
      }
      final SNodeReference switchPtr = templateSwitch.getReference();
      SNode newInputNode = getNewInputNode(templateContext);
      if (newInputNode == null) {
        TemplateSwitchMapping tswitch = myTemplateProcessor.getGenerator().getSwitch(switchPtr);
        if (tswitch != null) {
          tswitch.processNull(templateContext.getEnvironment(), switchPtr, templateContext);
        }
        return Collections.emptyList(); // skip template
      }

      final TemplateContext switchContext = prepareContext(templateContext).subContext(newInputNode);

      Collection<SNode> collection = null;
      try {
        collection = templateContext.getEnvironment().trySwitch(switchPtr, switchContext);
      } catch (GenerationCanceledException | GenerationFailureException | DismissTopMappingRuleException e) {
        throw e;
      } catch (GenerationException e) {
        getLogger().error(switchPtr, "internal error in switch: " + e.toString(), GeneratorUtil.describe(macro, "macro"));
      }
      if (collection == null) {
        // XXX why tryDefault is part of trySwitch, and not here? For the sake of generated code, perhaps (not to generate conditions 'if nothing generated')?
        // no switch-case found for the inputNode - continue with templateNode under the $switch$
        // use initial context, not the one prepared (could be filled with switch arguments)
        collection = nextMacro(templateContext.subContext(newInputNode));
      }
      return new ArrayList<SNode>(collection);
    }
  }

  // subclass is responsible to initialize myInvokedTemplate field
  private static abstract class InvokeTemplateMacro extends MacroWithInput {
    private final String myName;
    protected SNode myInvokedTemplate;
    private volatile TemplateContainer myTemplates;

    protected InvokeTemplateMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor, String macroName) {
      super(macro, templateNode, next, templateProcessor);
      myName = macroName;
    }
    protected abstract TemplateContext prepareContext(TemplateContext templateContext) throws GenerationFailureException;

    private TemplateContainer getTemplates() {
      TemplateContainer rv = myTemplates;
      if (rv == null) {
        synchronized (this) {
          if ((rv = myTemplates) == null) {
            rv = new TemplateContainer(myInvokedTemplate);
            myTemplates = rv;
          }
        }
      }
      return rv;
    }

    @NotNull
    @Override
    public final List<SNode> apply(@NotNull TemplateContext templateContext)
        throws DismissTopMappingRuleException, GenerationFailureException, GenerationCanceledException {

      SNode newInputNode = getNewInputNode(templateContext);
      if (newInputNode == null) {
        return Collections.emptyList(); // skip template
      }

      SNode invokedTemplate = myInvokedTemplate;
      if (invokedTemplate == null) {
        throw new TemplateProcessingFailureException(macro, String.format("error processing %s : no template to invoke", myName));
      }

      TemplateContext newcontext = prepareContext(templateContext).subContext(newInputNode);
      if (newcontext == null) {
        throw new TemplateProcessingFailureException(macro, String.format("error processing %s : failed to prepare new context", myName),
            GeneratorUtil.describe(invokedTemplate, "invoked template"));
      }

      final TemplateContainer tc = getTemplates();
      final List<SNode> rv = tc.processRuleConsequence(newcontext);
      templateContext.getEnvironment().getTrace().trace(newInputNode.getNodeId(), GenerationTracerUtil.translateOutput(rv), getMacroNodeRef());
      return rv;
    }
  }

  // $INCLUDE$
  private static class IncludeMacro extends InvokeTemplateMacro {

    protected IncludeMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor, "$INCLUDE$");
      myInvokedTemplate = RuleUtil.getIncludeMacro_Template(macro);
    }

    @Override
    protected TemplateContext prepareContext(TemplateContext templateContext) throws GenerationFailureException {
      final String[] parameterNames = RuleUtil.getTemplateDeclarationParameterNames(myInvokedTemplate);
      if (parameterNames == null) {
        getLogger().error(getMacroNodeRef(), "error processing $INCLUDE$: target template is broken", GeneratorUtil.describeInput(templateContext));
        return null;
      }

      for (String name : parameterNames) {
        if (!templateContext.hasVariable(name)) {
          getLogger().error(getMacroNodeRef(), String.format("error processing $INCLUDE$: parameter '%s' is missing", name),
              GeneratorUtil.describeInput(templateContext));
        }
      }
      return templateContext;
    }
  }

  // $CALL$
  private static class CallMacro extends InvokeTemplateMacro {
    private volatile TemplateCall myCallProcessor;

    protected CallMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor, "$CALL$");
      myInvokedTemplate = RuleUtil.getCallMacro_Template(macro);
    }

    @Override
    protected TemplateContext prepareContext(TemplateContext templateContext) throws GenerationFailureException {
      TemplateCall tc = myCallProcessor;
      if (tc == null) {
        tc = new TemplateCall(macro);
        if (tc.argumentsMismatch()) {
          getLogger().error(getMacroNodeRef(), "number of arguments doesn't match template", GeneratorUtil.describeInput(templateContext));
          // fall-through
        }
        myCallProcessor = tc;
      }
      return tc.prepareCallContext(templateContext);
    }
  }

  // $TRACE$
  private static class TraceMacro extends MacroWithInput {

    protected TraceMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {
      List<SNode> _outputNodes = nextMacro(templateContext);
      if (!_outputNodes.isEmpty()) {
        SNode inputNode = getNewInputNode(templateContext);
        if (inputNode != null) {
          for (SNode outputNode : _outputNodes) {
            TracingUtil.fillOriginalNode(inputNode, outputNode, false);
          }
        }
      }
      return _outputNodes;
    }
  }

  // $$
  private static class NoMacro extends MacroWithInput {

    protected NoMacro(@NotNull SNode macro, @NotNull TemplateNode templateNode, @Nullable MacroNode next, @NotNull TemplateProcessor templateProcessor) {
      super(macro, templateNode, next, templateProcessor);
    }

    @NotNull
    @Override
    public List<SNode> apply(@NotNull TemplateContext templateContext) throws DismissTopMappingRuleException, GenerationFailureException,
        GenerationCanceledException {

      Collection<SNode> newInputNodes = getNewInputNodes(templateContext);
      if (newInputNodes.isEmpty()) {
        return Collections.emptyList();
      }
      ArrayList<SNode> outputNodes = new ArrayList<SNode>();
      for (SNode newInputNode : newInputNodes) {
        List<SNode> _outputNodes = nextMacro(templateContext.subContext(newInputNode));
        outputNodes.addAll(_outputNodes);
      }
      return outputNodes;
    }
  }
}
