package jetbrains.mps.vcs.history;

/*Generated by MPS */

import jetbrains.mps.annotations.GeneratedClass;
import java.util.Deque;
import jetbrains.mps.internal.collections.runtime.DequeSequence;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import jetbrains.mps.internal.collections.runtime.MapSequence;
import java.util.HashMap;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.annotations.NotNull;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import jetbrains.mps.smodel.DefaultSModelDescriptor;
import jetbrains.mps.smodel.InvalidSModel;
import org.jetbrains.mps.openapi.model.SModel;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import jetbrains.mps.internal.collections.runtime.SetSequence;
import java.util.List;
import jetbrains.mps.internal.collections.runtime.CollectionSequence;
import java.util.Collection;
import java.util.HashSet;

@GeneratedClass(node = "r:2897a5d4-aed7-4a4e-ac07-fbc830f9ed9b(jetbrains.mps.vcs.history)/4498559492711807995", model = "r:2897a5d4-aed7-4a4e-ac07-fbc830f9ed9b(jetbrains.mps.vcs.history)")
public final class RootCommitsGraphTraverser {

  private final Deque<CommitsGraphNode> myBranches = DequeSequence.fromDeque(new LinkedList<CommitsGraphNode>());
  private final Map<CommitsGraphNode, Set<CommitsGraphNode>> myForkCommits = MapSequence.fromMap(new HashMap<CommitsGraphNode, Set<CommitsGraphNode>>());
  private final CommitsGraphNodeConsumer myCommitConsumer;
  private final VirtualFile myFile;
  private final SNodeId myRootId;
  private final boolean myTolerateReadModelFailure = true;
  private boolean myIsStopped = false;
  private ModelReadException myException;
  @NotNull
  private final CommitsGraphNode myStartNode;


  public RootCommitsGraphTraverser(@NotNull CommitsGraphNode startNode, SNodeId rootId, VirtualFile file, CommitsGraphNodeConsumer commitConsumer) {
    myRootId = rootId;
    myFile = file;
    myCommitConsumer = commitConsumer;
    myStartNode = startNode;
  }


  public void run() {
    try {
      myStartNode.loadModel(null, myFile.getExtension());
    } catch (Throwable e) {
      myException = new ModelReadException(myStartNode.getRevision(), e.getMessage());
    }
    if (!(myStartNode.isModelLoaded())) {
      return;
    }

    DequeSequence.fromDequeNew(myBranches).pushElement(myStartNode);

    while (hasNotIgnoredBranch()) {
      CommitsGraphNode node = getNextNode();
      if (!(node.isIgnored())) {
        try {
          loadParentsModels(node);
        } catch (ModelReadException e) {
          myException = e;
        }
        processNode(node);
      }
      node.unloadModel();
      if (myIsStopped) {
        break;
      }
      for (CommitsGraphNode parent : ListSequence.fromList(node.getParents())) {
        pushParentNode(node, parent);
      }
    }
  }

  public ModelReadException getException() {
    return myException;
  }

  private void loadParentsModels(CommitsGraphNode node) throws ModelReadException {
    for (CommitsGraphNode parent : node.getParents()) {
      loadParentModel(node, parent);
    }
  }

  private void loadParentModel(@NotNull CommitsGraphNode node, @NotNull CommitsGraphNode parent) throws ModelReadException {
    if (parent.getLoadedModel() != null) {
      return;
    }
    try {
      parent.loadModel(node, myFile.getExtension());
    } catch (Throwable e) {
      if (!(myTolerateReadModelFailure)) {
        throw new ModelReadException(parent.getRevision(), e.getMessage());
      }
    }
    if (parent.getLoadedModel().getNode(myRootId) == null) {
      parent.setIgnored();
    }
    if (!(myTolerateReadModelFailure) && !(parent.isOwnModelLoaded())) {
      if (parent.getModel() instanceof DefaultSModelDescriptor) {
        DefaultSModelDescriptor dd = (DefaultSModelDescriptor) parent.getModel();
        if (dd.getModelData() instanceof InvalidSModel) {
          InvalidSModel ism = (InvalidSModel) dd.getModelData();
          for (SModel.Problem problem : ism.getProblems()) {
            throw new ModelReadException(parent.getRevision(), problem.getText());
          }
        }
      }
    }
  }

  public static class ModelReadException extends Exception {

    private final VcsFileRevision myRevision;
    private final String myText;

    public ModelReadException(VcsFileRevision myRevision, String myCause) {
      super();
      this.myRevision = myRevision;
      this.myText = myCause;
    }

    public VcsFileRevision getRevision() {
      return myRevision;
    }

    public String getText() {
      return myText;
    }
  }

  private CommitsGraphNode getNextNode() {
    if (DequeSequence.fromDequeNew(myBranches).isEmpty()) {
      // This can happen if we annotate some arbitrary revision rather than the head revision.
      MapSequence.fromMap(myForkCommits).visitAll((it) -> {
        CommitsGraphNode forkCommit = it.key();
        Set<CommitsGraphNode> processedChildren = it.value();
        if (SetSequence.fromSet(processedChildren).all((processedChild) -> processedChild.isIgnored())) {
          forkCommit.setIgnored();
        }
        DequeSequence.fromDequeNew(myBranches).pushElement(forkCommit);
      });
      MapSequence.fromMap(myForkCommits).clear();
    }
    CommitsGraphNode node = DequeSequence.fromDequeNew(myBranches).sort((a, b) -> a.compareTo(b), false).first();
    DequeSequence.fromDequeNew(myBranches).removeElement(node);
    return node;
  }

  private void processNode(CommitsGraphNode node) {

    if (myException != null) {
      stop();
      return;
    }
    myCommitConsumer.commitProcessingStarted(node);
    List<CommitsGraphNode> parents = ListSequence.fromList(node.getParents()).toList();
    assert ListSequence.fromList(parents).count() < 3;
    if (ListSequence.fromList(parents).isEmpty()) {
      myCommitConsumer.processLastCommit(node);
      stop();
    } else if (ListSequence.fromList(parents).count() == 1) {
      myCommitConsumer.processSimpleCommit(node);
      CommitsGraphNode parentNode = ListSequence.fromList(node.getParents()).first();
      if (parentNode.isIgnored()) {
        stop();
      }
    } else if (ListSequence.fromList(parents).count() == 2) {
      myCommitConsumer.processMergeCommit(node);
      if (ListSequence.fromList(parents).getElement(0).isIgnored() && ListSequence.fromList(parents).getElement(1).isIgnored()) {
        stop();
      }
    }
    myCommitConsumer.commitProcessingFinished(node);
  }

  private boolean hasNotIgnoredBranch() {
    return DequeSequence.fromDequeNew(myBranches).any((it) -> !(it.isIgnored())) || MapSequence.fromMap(myForkCommits).isNotEmpty();
  }

  public void stop() {
    myIsStopped = true;
  }

  private void pushParentNode(CommitsGraphNode node, CommitsGraphNode parentNode) {
    if (CollectionSequence.fromCollection(parentNode.getChildren()).count() == 1) {
      pushParent(node, parentNode);
    } else {
      tryPushForkParent(node, parentNode);
    }
  }

  private void pushParent(CommitsGraphNode node, CommitsGraphNode parentNode) {
    if (node.isIgnored()) {
      parentNode.setIgnored();
    }
    DequeSequence.fromDequeNew(myBranches).pushElement(parentNode);
  }

  private void tryPushForkParent(CommitsGraphNode node, CommitsGraphNode parentNode) {
    Set<CommitsGraphNode> forkProcessedChildren = getForkProcessedChildren(parentNode);
    SetSequence.fromSet(forkProcessedChildren).addElement(node);
    if (SetSequence.fromSet(forkProcessedChildren).count() == CollectionSequence.fromCollection(parentNode.getChildren()).count()) {
      pushForkParent(parentNode);
    }
  }

  private void pushForkParent(final CommitsGraphNode parentNode) {
    Collection<CommitsGraphNode> children = parentNode.getChildren();
    if (CollectionSequence.fromCollection(children).all((node) -> node.isIgnored() || parentNode.isIgnoredByChild(node))) {
      parentNode.setIgnored();
    }
    MapSequence.fromMap(myForkCommits).removeKey(parentNode);
    DequeSequence.fromDequeNew(myBranches).pushElement(parentNode);
  }

  private Set<CommitsGraphNode> getForkProcessedChildren(CommitsGraphNode forkNode) {
    Set<CommitsGraphNode> processedChildren = MapSequence.fromMap(myForkCommits).get(forkNode);
    if (processedChildren == null) {
      processedChildren = SetSequence.fromSet(new HashSet<CommitsGraphNode>());
      MapSequence.fromMap(myForkCommits).put(forkNode, processedChildren);
    }
    return processedChildren;
  }
}
