/*
 * 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.newTypesystem.context.typechecking;

import gnu.trove.THashMap;
import gnu.trove.THashSet;
import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.MPSClassesListener;
import jetbrains.mps.classloading.MPSClassesListenerAdapter;
import jetbrains.mps.errors.IErrorReporter;
import jetbrains.mps.lang.typesystem.runtime.ICheckingRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.IsApplicableStatus;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.newTypesystem.context.component.ITypeErrorComponent;
import jetbrains.mps.newTypesystem.context.component.IncrementalTypecheckingComponent;
import jetbrains.mps.newTypesystem.context.component.NonTypeSystemComponent;
import jetbrains.mps.newTypesystem.context.component.TypeSystemComponent;
import jetbrains.mps.newTypesystem.state.State;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.SModelAdapter;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.event.SModelChildEvent;
import jetbrains.mps.smodel.event.SModelEvent;
import jetbrains.mps.smodel.event.SModelEventVisitorAdapter;
import jetbrains.mps.smodel.event.SModelPropertyEvent;
import jetbrains.mps.smodel.event.SModelReferenceEvent;
import jetbrains.mps.typesystem.inference.TypeChecker;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeRecalculatedListener;
import jetbrains.mps.util.Cancellable;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.WeakSet;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class IncrementalTypechecking extends BaseTypechecking<State, TypeSystemComponent> {

  private List<SModelEvent> myEvents = new ArrayList<SModelEvent>();
  private List<SModel> myReplacedModels = new ArrayList<SModel>();

  private MPSClassesListener myClassesListener = new MPSClassesListenerAdapter() {
    @Override
    public void beforeClassesUnloaded(Set<? extends ReloadableModuleBase> unloadedModules) {
      myNonTypeSystemComponent.clear();
    }
  };

  private Map<SModel, Set<SNode>> mySModelNodes = new THashMap<SModel, Set<SNode>>();

  private MyTypeRecalculatedListener myTypeRecalculatedListener = new MyTypeRecalculatedListener();

  private MyModelListener myModelListener = new MyModelListener();

  private MyModelListenerManager myModelListenerManager = new MyModelListenerManager();

  private MySmodelListener mySModelListener = new MySmodelListener();

  private NonTypeSystemComponent myNonTypeSystemComponent;

  private static final Logger LOG = Logger.wrap(LogManager.getLogger(IncrementalTypechecking.class));

  private NodeTypeAccess myNodeTypeAccess = new NodeTypeAccess();

  private ITypeErrorComponent myTypeErrorComponent;

  public IncrementalTypechecking(SNode node, State state) {
    super(node, state);
    myNonTypeSystemComponent = new NonTypeSystemComponent(TypeChecker.getInstance(), state, this);
    myModelListenerManager.track(myRootNode);
    ClassLoaderManager.getInstance().addClassesHandler(myClassesListener);
  }

  @Override
  protected TypeSystemComponent createTypecheckingComponent() {
    return new TypeSystemComponent(TypeChecker.getInstance(), getState(), this);
  }

  public void clear() {
    clearNodesTypes();
    myNonTypeSystemComponent.clear();
    getTypecheckingComponent().clear();
  }

  public MyModelListenerManager getModelListenerManager() {
    return myModelListenerManager;
  }

  public MyTypeRecalculatedListener getTypeRecalculatedListener() {
    return myTypeRecalculatedListener;
  }

  private void clearNodesTypes() {
    getTypecheckingComponent().clearNodeTypes();
    myNonTypeSystemComponent.clearNodeTypes();
  }

  private void putError(SNode node, IErrorReporter reporter) {
    getTypeErrorComponent().addError(node, reporter);
  }

  private ITypeErrorComponent getTypeErrorComponent() {
    return myTypeErrorComponent != null ? myTypeErrorComponent : getTypecheckingComponent();
  }

  public void reportTypeError(SNode nodeWithError, IErrorReporter errorReporter) {
    if (nodeWithError != null) {
      putError(nodeWithError, errorReporter);
    }
  }

  public void applyRuleToNode(SNode node, ICheckingRule_Runtime rule, IsApplicableStatus status, TypeCheckingContext typeCheckingContext) {
    try {
      rule.applyRule(node, typeCheckingContext, status);
    } catch (Throwable t) {
      LOG.error("an error occurred while applying rule to node " + node, t, node);
    }
  }

  /**
   * Returns true if the node's type is affected.
   */
  public boolean runApplyRulesTo(SNode node, Runnable run) {
    myNodeTypeAccess.pushNode(node);
    try {
      run.run();
    } finally {
      return myNodeTypeAccess.popNode();
    }
  }

  @Override
  public void dispose() {
    ClassLoaderManager.getInstance().removeClassesHandler(myClassesListener);
    if (myModelListenerManager != null) {
      myModelListenerManager.dispose();
      myModelListenerManager = null;
    }
    TypeChecker.getInstance().removeTypeRecalculatedListener(myTypeRecalculatedListener);
    if (myNonTypeSystemComponent != null) {
      myNonTypeSystemComponent = null;
    }
    mySModelListener = null;
    super.dispose();
  }


  public void setCheckedNonTypesystem() {
    myNonTypeSystemComponent.setChecked();
  }

  public void typeOfNodeCalled(SNode node) {
    myNodeTypeAccess.nodeTypeAccessed(node);
  }

  public void addDependencyOnCurrent(SNode node, boolean typeAffected) {
    addDependencyOnCurrent_(node, typeAffected);
  }

  public void addDependencyOnCurrent(SNode node) {
    addDependencyOnCurrent_(node, true);
  }

  //"type affected" means that *type* of this node depends on current
  // used to decide whether call "type will be recalculated" if current invalidated
  private void addDependencyOnCurrent_(SNode node, boolean typeAffected) {
    if (node == null) {
      LOG.error("Typesystem dependency not tracked. ");
      return;
    }

    Set<SNode> hashSet = new THashSet<SNode>(1);
    hashSet.add(myNodeTypeAccess.peekNode());
    getTypecheckingComponent().addDependentNodesTypeSystem(node, hashSet, typeAffected);
  }

  public void addDependencyForCurrent(SNode node) {
    SNode current = myNodeTypeAccess.peekNode();
    if (current == null) {
      LOG.error("Typesystem dependency not tracked. ");
      return;
    }

    Set<SNode> hashSet = new THashSet<SNode>(1);
    hashSet.add(node);
    getTypecheckingComponent().addDependentNodesTypeSystem(current, hashSet, true);
  }

  @Override
  public boolean applyNonTypesystemRulesToRoot(TypeCheckingContext typeCheckingContext, Cancellable c) {
    ITypeErrorComponent oldTypeErrorComponent = myTypeErrorComponent;
    myTypeErrorComponent = myNonTypeSystemComponent;
    try {
      return myNonTypeSystemComponent.applyNonTypeSystemRulesToRoot(typeCheckingContext, getNode(), c);
    } finally {
      myTypeErrorComponent = oldTypeErrorComponent;
    }
  }

  public SNode getType(SNode node) {
    return getTypecheckingComponent().getType(node);
  }

  @Override
  @NotNull
  public List<IErrorReporter> getErrors(SNode node) {
    List<IErrorReporter> result = new ArrayList<IErrorReporter>(super.getErrors(node));
    Map<SNode, List<IErrorReporter>> nodesToErrorsMapNT = myNonTypeSystemComponent.getNodesToErrorsMap();
    List<IErrorReporter> iErrorReporters = nodesToErrorsMapNT.get(node);
    if (iErrorReporters != null) {
      result.addAll(iErrorReporters);
    }
    return result;
  }

  //--------------------------------------------------
  @Override
  public Set<Pair<SNode, List<IErrorReporter>>> getNodesWithErrors(boolean typesystemErrors) {
    if (typesystemErrors) {
      return new THashSet<Pair<SNode, List<IErrorReporter>>>(super.getNodesWithErrors(typesystemErrors));
    }

    Set<Pair<SNode, List<IErrorReporter>>> result = new THashSet<Pair<SNode, List<IErrorReporter>>>();
    Map<SNode, List<IErrorReporter>> nodesToErrorsMapNT = myNonTypeSystemComponent.getNodesToErrorsMap();
    Set<SNode> keySet = new THashSet<SNode>(nodesToErrorsMapNT.keySet());
    keySet.addAll(nodesToErrorsMapNT.keySet());

    for (SNode key : keySet) {
      List<IErrorReporter> reporters = nodesToErrorsMapNT.get(key);
      if (reporters.isEmpty()) continue;
      if (key.getModel() == null) {
        LOG.warning("Type system reports error for node without containing root. Node: " + key);
        for (IErrorReporter reporter : reporters) {
          LOG.warning("This error was reported from: " + reporter.getRuleNode(), reporter.getRuleNode());
        }
        continue;
      }
      result.add(new Pair<SNode, List<IErrorReporter>>(key, reporters));
    }
    return result;
  }


  public void markNodeAsAffectedByRule(SNode node, String ruleModel, String ruleId) {
    getTypecheckingComponent().markNodeAsAffectedByRule(node, ruleModel, ruleId);
  }

  public Set<Pair<String, String>> getRulesWhichAffectNodeType(SNode node) {
    return getTypecheckingComponent().getRulesWhichAffectNodeType(node);
  }

  public boolean isCheckedNonTypesystem() {
    return myNonTypeSystemComponent.isChecked();
  }

  @Override
  public boolean isChecked(boolean considerNonTypeSystemRules) {
    processPendingEvents();
    boolean typesChecked = super.isChecked(considerNonTypeSystemRules);
    if (considerNonTypeSystemRules) {
      return typesChecked && myNonTypeSystemComponent.isChecked();
    } else {
      return typesChecked;
    }
  }

  private void processPendingEvents() {
    final MySModelEventVisitorAdapter visitor = new MySModelEventVisitorAdapter();
    for (SModelEvent event : myEvents) {
      event.accept(visitor);
    }
    for (SModel replacedModel : myReplacedModels) {
      for (SNode node : mySModelNodes.get(replacedModel)) {
        visitor.markInvalid(node);
      }
    }
    myReplacedModels.clear();
    myEvents.clear();
  }

  public void track(SNode node) {
    myModelListenerManager.track(node);
  }

  public void updateGCedNodes() {
    getModelListenerManager().updateGCedNodes();
  }

  private class MyModelListener extends SModelAdapter {
    @Override
    public void eventFired(SModelEvent event) {
      myEvents.add(event);
    }
  }

  private class MySmodelListener extends SRepositoryContentAdapter {
    @Override
    public void modelReplaced(SModel model) {
      myReplacedModels.add(model);
    }
  }

  private class MySModelEventVisitorAdapter extends SModelEventVisitorAdapter {
    @Override
    public void visitChildEvent(SModelChildEvent event) {
      final SNode child = event.getChild();
      final SNode parent = event.getParent();

      if (jetbrains.mps.smodel.SNodeUtil.isSideTransformInfo(child) &&
          (event.isRemoved() || jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute.equals(child.getContainmentLink()))) {
        return;
      }

      markInvalid(child);
      markInvalid(parent);

      List<SNode> childWithDescendants = IterableUtil.copyToList(SNodeUtil.getDescendants(child, null, true));
      if (event.isRemoved()) {
        Iterator<SNode> it = childWithDescendants.iterator();
        it.next(); // skip child, we've marked it as invalid already
        while (it.hasNext()) {
          SNode descendant = it.next();
          //invalidate nodes which are removed
          markDependentNodesForInvalidation(descendant, myNonTypeSystemComponent);
          markDependentNodesForInvalidation(descendant, getTypecheckingComponent());
        }
      }

      List<SReference> references = new ArrayList<SReference>();
      for (SNode descendant : childWithDescendants) {
        references.addAll(IterableUtil.asCollection(descendant.getReferences()));
      }
      markReferenceTargetsInvalid(references);
    }

    private void markReferenceTargetsInvalid(List<SReference> references) {
      for (SReference reference : references) {
        // MPS-18585 IncrementalTypecheking doesn't invalidate target nodes of dynamic refs if source node has been detached from model
        if (reference instanceof DynamicReference) {
          // the problem was in a more strict case:
          // dynamic reference from a detached node (its getTargetNode() seems to be non-sensible)
          // but I skip all DynamicReferences
          continue;
        }
        SNode targetNode = jetbrains.mps.util.SNodeOperations.getTargetNodeSilently(reference);
        if (targetNode != null) {
          markDependentNodesForInvalidation(targetNode, myNonTypeSystemComponent);
        }
      }
    }

    @Override
    public void visitReferenceEvent(SModelReferenceEvent event) {
      SReference ref = event.getReference();
      markInvalid(ref.getSourceNode());
      // A heuristic: always invalidate the node's parent (MPS-21481)
      if (ref.getSourceNode().getParent() != null) {
        markInvalid(ref.getSourceNode().getParent());
      }
      if (!event.isAdded()) return;
      // MPS-18585 IncrementalTypecheking doesn't invalidate target nodes of dynamic refs if source node has been detached from model
      if (ref instanceof DynamicReference && ref.getSourceNode().getModel() == null) {
        return;
      }
      SNode node = jetbrains.mps.util.SNodeOperations.getTargetNodeSilently(event.getReference());
      if (node == null) return;
      markDependentNodesForInvalidation(node, myNonTypeSystemComponent);
    }

    @Override
    public void visitPropertyEvent(SModelPropertyEvent event) {
      markDependentOnPropertyNodesForInvalidation(event.getNode(), event.getPropertyName());
    }

    private void markInvalid(SNode node) {
      markDependentNodesForInvalidation(node, getTypecheckingComponent());
      markDependentNodesForInvalidation(node, myNonTypeSystemComponent);
    }

    private void markDependentNodesForInvalidation(SNode eventNode, IncrementalTypecheckingComponent component) {
      component.addNodeToInvalidate(eventNode);
    }

    private void markDependentOnPropertyNodesForInvalidation(SNode eventNode, String propertyName) {
      myNonTypeSystemComponent.addPropertyToInvalidate(eventNode, propertyName);
      getTypecheckingComponent().addNodeToInvalidate(eventNode);
    }
  }

  private class MyTypeRecalculatedListener implements TypeRecalculatedListener {
    MyTypeRecalculatedListener() {
    }

    @Override
    public void typeWillBeRecalculatedForTerm(SNode term) {
      myNonTypeSystemComponent.typeWillBeRecalculatedForTerm(term);
    }
  }

  private class MyModelListenerManager {
    private ReferenceQueue<SNode> myReferenceQueue = new ReferenceQueue<SNode>();
    private Map<SModel, Integer> myNodesCount = new THashMap<SModel, Integer>();
    private Map<WeakReference, SModel> myDescriptors = new THashMap<WeakReference, SModel>();

    /**
     * Warning: this method should be called only once for each node
     * We do not check for duplicated nodes
     */
    void track(SNode node) {
      if (!SNodeUtil.isAccessible(node, MPSModuleRepository.getInstance())) return;

      SModel sm = node.getModel();
      if (!myNodesCount.containsKey(sm)) {
        ((SModelInternal) sm).addModelListener(myModelListener);
        sm.addModelListener(mySModelListener);
        myNodesCount.put(sm, 1);
        mySModelNodes.put(sm, new WeakSet<SNode>());
      } else {
        Integer oldValue = myNodesCount.get(sm);
        myNodesCount.put(sm, oldValue + 1);
      }

      WeakReference<SNode> ref = new WeakReference<SNode>(node, myReferenceQueue);
      myDescriptors.put(ref, sm);

      mySModelNodes.get(sm).add(node);
    }


    void updateGCedNodes() {
      while (true) {
        WeakReference<SNode> ref = (WeakReference<SNode>) myReferenceQueue.poll();
        if (ref == null) return;

        SModel sm = myDescriptors.get(ref);
        Integer count = myNodesCount.get(sm);
        if (count == 1) {
          ((SModelInternal) sm).removeModelListener(myModelListener);
          sm.removeModelListener(mySModelListener);
          myNodesCount.remove(sm);
          mySModelNodes.remove(sm);
        } else {
          myNodesCount.put(sm, count - 1);
        }

        myDescriptors.remove(ref);
      }
    }

    void dispose() {
      for (SModel sm : Collections.unmodifiableCollection(myNodesCount.keySet())) {
        ((SModelInternal) sm).removeModelListener(myModelListener);
        sm.removeModelListener(mySModelListener);
      }
    }
  }

  private static class NodeTypeAccess {
    private LinkedList<Pair<SNode, Boolean>> myStack = new LinkedList<Pair<SNode, Boolean>>();

    private void pushNode(SNode node) {
      myStack.push(new Pair<SNode, Boolean>(node, false));
    }

    private boolean popNode() {
      return myStack.pop().o2;
    }

    private void nodeTypeAccessed(SNode node) {
      for (Pair<SNode, Boolean> p : myStack) {
        if (p.o1 == node) {
          p.o2 = true;
        }
      }
    }

    private SNode peekNode() {
      if (myStack.isEmpty()) return null;
      return myStack.peek().o1;
    }
  }
}

