/*
 * Copyright 2003-2012 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.newTypesystem.context.component;

import gnu.trove.THashSet;
import jetbrains.mps.errors.IErrorReporter;
import jetbrains.mps.lang.typesystem.runtime.InferenceRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.IsApplicableStatus;
import jetbrains.mps.lang.typesystem.runtime.SubstituteType_Runtime;
import jetbrains.mps.newTypesystem.context.typechecking.BaseTypechecking;
import jetbrains.mps.newTypesystem.state.State;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeSubstitution;
import jetbrains.mps.typesystemEngine.util.TypeSystemUtil;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.SNodeOperations;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.model.SNode;
import jetbrains.mps.typesystem.inference.TypeChecker;
import jetbrains.mps.util.Pair;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * User: fyodor
 * Date: 11/12/12
 */
/*package*/ public class SimpleTypecheckingComponent<STATE extends State> {

  private final STATE myState;
  protected Queue<SNode> myQueue = new LinkedList<SNode>();
  protected boolean myIsChecked = false;
  protected BaseTypechecking<?, ?> myTypechecking;
  protected Set<SNode> myNodes = new THashSet<SNode>();
  protected Set<SNode> myFullyCheckedNodes = new THashSet<SNode>(); //nodes which are checked with their children
  protected Set<SNode> myPartlyCheckedNodes = new THashSet<SNode>(); // nodes which are checked themselves but not children


  public SimpleTypecheckingComponent(STATE state, BaseTypechecking component) {
    myState = state;
    myTypechecking = component;
  }

  protected boolean isIncrementalMode() {
    return false;
  }

  //returns true if something was invalidated
  protected boolean doInvalidate() {
    return false;
  }

  protected void invalidateNodeTypeSystem(SNode node, boolean typeWillBeRecalculated) {
    myPartlyCheckedNodes.remove(node);
    myFullyCheckedNodes.remove(node);
    myState.clearNode(node);
  }

  protected void performActionsAfterChecking() {
    // do nothing
  }

  public SNode computeTypesForNode_special(SNode initialNode, Collection<SNode> givenAdditionalNodes) {
    return computeTypesForNode_special_(initialNode, givenAdditionalNodes);
  }

  protected void setTargetNode(SNode initialNode) {
    assert false;
  }


  public Map<SNode, List<IErrorReporter>> getNodesToErrorsMap() {
    return Collections.emptyMap();
  }

  protected void clearState() {
    myState.clear(true);
  }

  public void addNodeToFrontier(SNode node) {
    if (node == null) return;
    myQueue.add(node);
  }

  public void computeTypes(boolean refreshTypes) {
    computeTypes(getTypechecking().getNode(), refreshTypes, true, Collections.<SNode>emptyList(), true, null);
  }

  protected void computeTypes(SNode nodeToCheck, boolean refreshTypes, boolean forceChildrenCheck, Collection<SNode> additionalNodes, boolean finalExpansion, SNode initialNode) {
    if (refreshTypes) {
      clear();
      if (initialNode != null) {
        setTargetNode(initialNode);
      }
    } else {
      myState.clearStateObjects();
      doInvalidate();
      myPartlyCheckedNodes.addAll(myFullyCheckedNodes);
      myFullyCheckedNodes.clear();
    }
    computeTypesSpecial(nodeToCheck, forceChildrenCheck, additionalNodes, finalExpansion, initialNode);
    performActionsAfterChecking();
    myState.performActionsAfterChecking();  }

  public void setChecked() {
    myIsChecked = true;
  }

  public boolean isChecked() {
    return myIsChecked && !doInvalidate();
  }

  protected BaseTypechecking getTypechecking() {
    return myTypechecking;
  }

  public void solveInequalitiesAndExpandTypes(boolean finalExpansion) {
    myState.solveInequalities();
    myState.expandAll(myNodes, finalExpansion);
    myNodes.clear();
  }

  protected AccessTracking createAccessTracking() {
    return new AccessTracking();
  }

  protected void applyRulesToNode(SNode node, List<Pair<InferenceRule_Runtime, IsApplicableStatus>> newRules) {
    for (Pair<InferenceRule_Runtime, IsApplicableStatus> rule : newRules) {
      myState.applyRuleToNode(node, rule.o1, rule.o2);
    }
  }

  /**
   * Search for and apply the inference rules to the given node.
   *
   * In case the node has node attributes, also search for inference rules for these attributes and apply them *before* rules for the
   * node given.
   *
   * @param node
   * @return
   */
  protected boolean applyRulesToNode(SNode node) {
    final List<Pair<SNode, List<Pair<InferenceRule_Runtime, IsApplicableStatus>>>> nodesAndRules = new ArrayList<Pair<SNode, List<Pair<InferenceRule_Runtime, IsApplicableStatus>>>>();

    if (!collectNodesAndRules(node, nodesAndRules)) return false;

    for (Pair<SNode, List<Pair<InferenceRule_Runtime, IsApplicableStatus>>> pair : nodesAndRules) {
      applyRulesToNode(pair.o1, pair.o2);
    }

    return true;
  }

  @NotNull
  protected SConcept getNodeAttributeConcept() {
    return MetaAdapterFactory.getConcept(0xceab519525ea4f22L, 0x9b92103b95ca8c0cL, 0x2eb1ad060897da54L, "jetbrains.mps.lang.core.structure.NodeAttribute");
  }

  @NotNull
  protected SContainmentLink getSmodelAttributeRole() {
    return MetaAdapterFactory.getContainmentLink(0xceab519525ea4f22L, 0x9b92103b95ca8c0cL, 0x10802efe25aL, 0x47bf8397520e5942L, "smodelAttribute");
  }

  private boolean isNodeAttribute(SNode sNode) {
    boolean conceptMatches = sNode.getConcept().isSubConceptOf(getNodeAttributeConcept());
    return conceptMatches && getSmodelAttributeRole().equals(sNode.getContainmentLink());
  }

  protected boolean collectNodesAndRules(SNode node, List<Pair<SNode, List<Pair<InferenceRule_Runtime, IsApplicableStatus>>>> nodesAndRules) {
    for (SNode nodeOrAttr : myTypechecking.nodesToApplyRulesTo(node)) {
      List<Pair<InferenceRule_Runtime, IsApplicableStatus>> rules = TypeChecker.getInstance().getRulesManager().getInferenceRules(nodeOrAttr);
      if (rules != null && !rules.isEmpty()) {
        nodesAndRules.add(new Pair<SNode, List<Pair<InferenceRule_Runtime, IsApplicableStatus>>>(nodeOrAttr, rules));

        // check if the last rule applicable to an attribute supercedes the rules that may follow (last one wins)
        // this has no effect if we're looking at the attributed node
        Pair<InferenceRule_Runtime, IsApplicableStatus> lastPair = rules.get(rules.size() - 1);
        if (lastPair.o1.supercedesAttributed(nodeOrAttr, lastPair.o2)) {
          break;
        }
      }
    }

    return !nodesAndRules.isEmpty();
  }

  public SNode getType(SNode node) {
    if (myFullyCheckedNodes.contains(node)) {
      return getRawTypeFromContext(node);
    }
    return null;
  }

  private SNode getRawTypeFromContext(SNode node) {
    return myState.getTypeCheckingContext().getTypeDontCheck(node);
//    synchronized (TYPECHECKING_LOCK) {
//      return myState.getNodeMaps().getType(node);
//    }
  }

  private void drainQueue(boolean forceChildrenCheck, SNode targetNode, AccessTracking accessTracking) {
    for (SNode sNode = myQueue.poll(); sNode != null; sNode = myQueue.poll()) {
      if (myFullyCheckedNodes.contains(sNode) || !TypeSystemUtil.shouldApplyTypeSystemRules(sNode)) {
        continue;
      }
      Set<SNode> candidatesForFrontier = new LinkedHashSet<SNode>();
      if (forceChildrenCheck) {
        candidatesForFrontier.addAll(IterableUtil.asCollection(sNode.getChildren()));
      }
      for (SNode candidate : candidatesForFrontier) {
        if (candidate == null || myFullyCheckedNodes.contains(candidate)) continue;
        myQueue.add(candidate);
      }
      if (isNodeAttribute(sNode)) {
        // attributes are processed together with the attributed nodes
        myQueue.add(sNode.getParent());

      } else if (!myPartlyCheckedNodes.contains(sNode)) {
        applyRulesAndTrackAccess(accessTracking, sNode);
        myPartlyCheckedNodes.add(sNode);
      }
      myFullyCheckedNodes.add(sNode);
      if (typeCalculated(targetNode) != null) return;
    }
  }

  private void applyRulesAndTrackAccess(AccessTracking accessTracking, SNode sNode) {
    accessTracking.installReadListeners();
    boolean typeAffected = false;
    try {
      myNodes.add(sNode);
      typeAffected = applyRulesToNode(sNode);
    } finally {
      accessTracking.removeReadListeners();
    }
    accessTracking.postProcess(sNode, typeAffected);
  }

  protected SNode typeCalculated(SNode initialNode) {
    return null;
  }

  protected void computeTypesSpecial(SNode nodeToCheck, boolean forceChildrenCheck, Collection<SNode> additionalNodes, boolean finalExpansion, SNode initialNode) {
    AccessTracking accessTracking = createAccessTracking();
    if (nodeToCheck != null) {
      myQueue.add(nodeToCheck);
      myQueue.addAll(additionalNodes);
    }
    while (!myQueue.isEmpty()) {
      drainQueue(forceChildrenCheck, initialNode, accessTracking);
      if (typeCalculated(initialNode) != null) return;
      solveInequalitiesAndExpandTypes(finalExpansion);
    }
  }

  protected SNode computeTypesForNode_special_(SNode initialNode, Collection<SNode> givenAdditionalNodes) {
    assert myFullyCheckedNodes.isEmpty();
    return computeTypesForNode_special__(initialNode, givenAdditionalNodes);
  }

  protected SNode computeTypesForNode_special__(SNode initialNode, Collection<SNode> givenAdditionalNodes) {
    long start = System.currentTimeMillis();
    setTarget(initialNode);
    if (initialNode == null) return null;

    computeTypesSpecial(initialNode, false, givenAdditionalNodes, false, initialNode);
    SNode type = typeCalculated(initialNode);
    if (type != null) return type;

    if (initialNode.getModel() != null && initialNode.getParent() == null) {
      computeTypes(initialNode, initialNode);
      return getType(initialNode);
    }

    TypeSystemComponent.LOG.debug("No typesystem rule for " + SNodeOperations.getDebugText(initialNode) + " in root " + initialNode.getContainingRoot() + ": type calculation took " + (System.currentTimeMillis() - start) + " ms", new Throwable(), new jetbrains.mps.smodel.SNodePointer(initialNode));
    return null;
  }

  protected void computeTypes(SNode initialNode, SNode node) {
    /*do nothing*/
  }

  protected void setTarget(SNode initialNode) {
    /*do nothing*/
  }

  public void clear() {

  }

  /*package*/ STATE getState() {
    return myState;
  }

  protected static class AccessTracking {
    protected void installReadListeners() {}

    protected void removeReadListeners() {}

    protected void postProcess(SNode sNode, boolean typeAffected){}
  }

  public TypeSubstitution lookupSubstitution(SNode origNode, TypeCheckingContext typeCheckingContext) {
    for (SNode ruleNode : myTypechecking.nodesToApplyRulesTo(origNode)) {
      for (Pair<SubstituteType_Runtime, IsApplicableStatus> rule_status : substituteTypeRules(ruleNode)) {
        if(rule_status.o2.isApplicable()) {
          TypeSubstitution subs = rule_status.o1.substitution(ruleNode, origNode, typeCheckingContext, rule_status.o2);

          if (subs != null && subs.isValid()) {
            return subs;
          }
        }
      }
    }
    return null;
  }

  private List<Pair<SubstituteType_Runtime, IsApplicableStatus>> substituteTypeRules(SNode test) {
    return TypeChecker.getInstance().getRulesManager().getSubstituteTypeRules(test);
  }

}
