/*
 * Copyright 2003-2016 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.impl.FastRuleFinder.BlockedReductionsData;
import jetbrains.mps.generator.runtime.GenerationException;
import jetbrains.mps.generator.runtime.TemplateExecutionEnvironment;
import jetbrains.mps.generator.runtime.TemplateWeavingRule;
import jetbrains.mps.generator.template.ITemplateGenerator;
import jetbrains.mps.generator.template.QueryExecutionContext;
import jetbrains.mps.smodel.FastNodeFinder;
import jetbrains.mps.smodel.FastNodeFinderManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.ArrayList;
import java.util.List;

/**
 * Evgeny Gryaznov, Feb 15, 2010
 */
public class WeavingProcessor {
  private final TemplateGenerator myGenerator;
  private final List<ArmedWeavingRule> myReadyRules;

  public WeavingProcessor(TemplateGenerator generator) {
    myGenerator = generator;
    myReadyRules = new ArrayList<ArmedWeavingRule>();
  }

  public void prepareWeavingRules(SModel inputModel) throws GenerationCanceledException, GenerationFailureException {
    Iterable<TemplateWeavingRule> rules = myGenerator.getRuleManager().getWeaving_MappingRules();
    myReadyRules.clear();
    final BlockedReductionsData ruleBlocks = myGenerator.getBlockedReductionsData();
    final FastNodeFinder nodeFinder = FastNodeFinderManager.get(inputModel);
    for (TemplateWeavingRule rule : rules) {
      boolean includeInheritors = rule.applyToInheritors();
      for (SNode applicableNode : nodeFinder.getNodes(rule.getApplicableConcept(), includeInheritors)) {
        if (ruleBlocks.isWeavingBlocked(applicableNode, rule)) {
          continue;
        }
        QueryExecutionContext executionContext = myGenerator.getExecutionContext(applicableNode);
        if (executionContext == null) {
          continue;
        }
        TemplateExecutionEnvironment environment = new TemplateExecutionEnvironmentImpl(new TemplateProcessor(myGenerator), executionContext, new ReductionTrack(myGenerator.getBlockedReductionsData()));
        DefaultTemplateContext context = new DefaultTemplateContext(environment, applicableNode, null);
        if (executionContext.isApplicable(rule, context)) {
          // if there are too many ArmedWeavingRule instances (i.e. a lot of applicable SNode),
          // it's easy to refactor AWR to keep list of applicable nodes and to recreate TEE on demand
          myReadyRules.add(new ArmedWeavingRule(rule, environment, applicableNode));
          ruleBlocks.blockWeaving(applicableNode, rule);
        }
      }
    }
  }

  public boolean hasWeavingRulesToApply() {
    return !myReadyRules.isEmpty();
  }

  public void apply() throws GenerationFailureException, GenerationCanceledException {
    for (ArmedWeavingRule rule : myReadyRules) {
      if (rule.apply()) {
        myGenerator.setChanged();
      }
    }
  }

  private static class ArmedWeavingRule {
    @NotNull
    private final TemplateWeavingRule myRule;
    @NotNull
    private final TemplateExecutionEnvironment myEnv;
    @NotNull
    private final SNode myApplicableNode;

    public ArmedWeavingRule(@NotNull TemplateWeavingRule rule, @NotNull TemplateExecutionEnvironment env, @NotNull SNode applicableNode) {
      myRule = rule;
      myEnv = env;
      myApplicableNode = applicableNode;
    }

    public boolean apply() throws GenerationFailureException, GenerationCanceledException {
      try {
        DefaultTemplateContext context = new DefaultTemplateContext(myEnv, myApplicableNode, null);
        final QueryExecutionContext queryExecutor = myEnv.getQueryExecutor();
        SNode outputContextNode = queryExecutor.getContextNode(myRule, context);
        if (!checkContext(outputContextNode)) {
          return false;
        }

        try {
          queryExecutor.applyRule(myRule, context, outputContextNode);
        } catch (DismissTopMappingRuleException e) {
          myEnv.getLogger().error(myRule.getRuleNode(), "wrong template: dismiss in weaving rule is not supported", GeneratorUtil.describeInput(context));
        } catch (TemplateProcessingFailureException e) {
          myEnv.getLogger().error(myRule.getRuleNode(), "weaving rule: error processing template fragment", GeneratorUtil.describeInput(context));
        }
      } catch (GenerationCanceledException ex) {
        throw ex;
      } catch (GenerationFailureException ex) {
        throw ex;
      } catch (GenerationException e) {
        myEnv.getLogger().error(myRule.getRuleNode(), "internal error: " + e.toString());
      }
      return true; // original code did myGenerator.setChanged once checkContext had passed.
    }

    private boolean checkContext(SNode contextNode) {
      TemplateGenerator generator = myEnv.getGenerator();

      if (contextNode == null) {
        myEnv.getLogger().error(myRule.getRuleNode(), "weaving rule: cannot find context node", GeneratorUtil.describe(myApplicableNode, "input node"));
        return false;
      }
      // Additional check - context should be generated from the same root
      SNode contextRoot = contextNode.getContainingRoot();
      SModel model = contextRoot.getModel();
      if (model == null) {
        return reportErrorIfStrict("bad context for weaving rule: no root for context " + contextNode);
      }

      if (model != generator.getOutputModel()) {
        String msg = "Bad context for weaving rule: context node shall belong to output model.";
        if (model == generator.getInputModel()) {
          msg += "Context specified comes from the input model. You may use 'genContext.get copied output for' operation to obtain its output model counterpart.";
        }
        myEnv.getLogger().error(myRule.getRuleNode(), msg, GeneratorUtil.describe(myApplicableNode, "input node"));
        return false;
      }

      SNode originalContextRoot = generator.getOriginalRootByGenerated(contextRoot);
      if (originalContextRoot == null) {
        return reportErrorIfStrict(String.format("bad context for weaving rule: %s is generated by 'create root' rule", contextRoot));
      }

      if (myApplicableNode.getModel() == null) return true;

      SNode inputRoot = myApplicableNode.getContainingRoot();
      if (originalContextRoot != inputRoot) {
        String msg = "bad context for weaving rule: %s is generated from %s , while input node is from %s";
        return reportErrorIfStrict(String.format(msg, contextRoot, originalContextRoot, inputRoot));
      }

      return true;
    }

    private boolean reportErrorIfStrict(String msg) {
      ITemplateGenerator generator = myEnv.getGenerator();
      if (generator.isStrict()) {
        generator.getLogger().error(myRule.getRuleNode(), String.format("Strict generation mode failure: %s", msg));
        return false;
      } else {
        generator.getLogger().warning(myRule.getRuleNode(), msg);
        return true;
      }
    }

    @Override
    public String toString() {
      return String.format("waving rule for: %s; node: %s", myRule.getApplicableConcept().getQualifiedName(), myApplicableNode);
    }
  }
 }
