package jetbrains.mps.vcs.diff.ui;

/*Generated by MPS */

import jetbrains.mps.annotations.GeneratedClass;
import jetbrains.mps.logging.Logger;
import org.jetbrains.mps.openapi.model.SNodeId;
import java.util.List;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import java.util.ArrayList;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.Arrays;
import jetbrains.mps.smodel.persistence.lines.LineContent;
import jetbrains.mps.vcspersistence.VCSPersistenceSupport;
import com.intellij.openapi.progress.ProcessCanceledException;
import java.util.Map;
import java.util.HashMap;
import jetbrains.mps.smodel.persistence.lines.NodeLineContent;

@GeneratedClass(node = "r:df1b052a-af27-4b87-80fc-1492fa2192be(jetbrains.mps.vcs.diff.ui)/6427926084137231861", model = "r:df1b052a-af27-4b87-80fc-1492fa2192be(jetbrains.mps.vcs.diff.ui)")
/*package*/ final class RootFileHistoryExtractor implements RevisionsExtractor {
  private static final Logger LOG = Logger.getLogger(RootFileHistoryExtractor.class);
  private final SNodeId myRoot;
  private final List<VcsFileRevision> myFilteredRevisions = new ArrayList<VcsFileRevision>();
  private final List<VcsFileRevision> myInitialRevisions;
  private boolean myLoading;
  private final Runnable myOnUpdate;
  private final int myTotalRevisions;
  private int myProcessedRevisions;
  private boolean myIsStopped;


  /*package*/ RootFileHistoryExtractor(List<VcsFileRevision> initialRevisions, SNodeId root, Runnable onUpdate) {
    myInitialRevisions = initialRevisions;
    myRoot = root;
    myOnUpdate = onUpdate;
    myTotalRevisions = initialRevisions.size();
  }

  @Override
  @NotNull
  public List<VcsFileRevision> getRevisionParents(VcsFileRevision revision) {
    int index = myInitialRevisions.indexOf(revision);
    if (index + 1 >= myInitialRevisions.size()) {
      return Collections.emptyList();
    }
    return Arrays.asList(myInitialRevisions.get(index + 1));
  }

  @Override
  public boolean isLoading() {
    return myLoading;
  }

  @Override
  public List<VcsFileRevision> getRevisions() {
    synchronized (myFilteredRevisions) {
      return new ArrayList<VcsFileRevision>(myFilteredRevisions);
    }
  }

  @Override
  public int getTotalRevisionsNumber() {
    return myTotalRevisions;
  }

  @Override
  public int getProcessedRevisionsNumber() {
    return myProcessedRevisions;
  }

  @Override
  public boolean stop() {
    myIsStopped = true;
    return true;
  }

  @Override
  public void run() {
    myLoading = true;
    try {
      NodeSet ns = new NodeSet(myRoot);
      // revision with the last revision we've analyzed content for
      VcsFileRevision prevRevision = null;
      ContentRange prevContentRange = null;
      // I assume first revision to be CurrentRevision with actual model/file content and myRoot node present
      for (VcsFileRevision rev : myInitialRevisions) {
        if (myIsStopped) {
          break;
        }
        myProcessedRevisions++;
        try {
          byte[] revContent = rev.loadContent();
          List<LineContent> lineToContentMap = VCSPersistenceSupport.getLineToContentMap(revContent, true);
          if (lineToContentMap == null) {
            // FIXME report somehow
            continue;
          }
          ContentRange contentRange = buildRanges(ns, lineToContentMap);
          if (prevRevision != null && contentRange.changedAgainst(prevContentRange)) {
            // prevRevision == null when we process the very first revision, aka CurrentRevision. All we need to do then is to record state for further steps.
            // contentRange might be isEmpty, which indicates node addition/removal. Shall record the revision and go on, just in case the node is back in earlier revisions.
            synchronized (myFilteredRevisions) {
              myFilteredRevisions.add(prevRevision);
            }
            // fall-through
          }
          // if no changes, just skip this revision. Through there's no need to update prevContentRange as it's the same, do it for code simplicity
          prevContentRange = contentRange;
          // prevRevision, however, shall always point to the processed revision
          prevRevision = rev;
          myOnUpdate.run();

        } catch (ProcessCanceledException ex) {
          // re-throw, it's handled further in the outside catch, just don't log. See MPS-30613
          throw ex;
        } catch (Exception ex) {
          if (LOG.isErrorLevel()) {
            LOG.error("Error processing revision " + rev, ex);
          }
          // FIXME report the exception in UI
        }
      }
      if (prevContentRange != null && !(prevContentRange.isEmpty())) {
        // last revision we've checked has some content that corresponds to node (e.g. node introduced in the very first revision of the file)
        // XXX perhaps, shall do it for any prevRevision != null just to get revision with no node (aka 'base') to give diff in any case (think of a single revision file with the node)
        synchronized (myFilteredRevisions) {
          myFilteredRevisions.add(prevRevision);
        }
      }
    } catch (ProcessCanceledException ex) {
      // Though this exception is not manifested, chances are ProgressManager.checkCanceled() call somewhere deep down the call stack may cause it,
      // just ignore the exception and stop annotating.
    } finally {
      myLoading = false;
    }
    myOnUpdate.run();
  }

  private static ContentRange buildRanges(NodeSet ns, List<LineContent> lineToContentMap) {
    Map<SNodeId, Integer> firstLines = new HashMap<SNodeId, Integer>();
    ContentRange contentRange = new ContentRange();
    int line = 0;
    for (LineContent lc : lineToContentMap) {
      if (lc instanceof NodeLineContent) {
        SNodeId lineNodeId = lc.getNodeId();
        if (ns.isOfInterest(lineNodeId)) {
          Integer start = firstLines.remove(lineNodeId);
          if (start != null) {
            // assume it's the closing NodeLineContent for the same node id
            // FIXME instead of this assumptions, may want to introduce "closing tag" flag into NodeLineContent to detect start/end of a node range properly
            contentRange.recordRange(lineNodeId, start, line, lineToContentMap);
          } else {
            // first time we encountered a node of interest, record its start
            firstLines.put(lineNodeId, line);
          }
        }
      }
      line++;
    }
    // if there were no closing NodeLineContent, assume these records are one-liners
    for (SNodeId nid : firstLines.keySet()) {
      Integer l = firstLines.get(nid);
      contentRange.recordRange(nid, l, l, lineToContentMap);
    }
    return contentRange;
  }

  /**
   * Abstraction of a set of nodes we are interested. At the moment we are limited to a root node only
   */
  private static class NodeSet {
    private final SNodeId myRoot;

    /*package*/ NodeSet(SNodeId single) {
      myRoot = single;
    }

    public boolean isOfInterest(SNodeId nid) {
      return myRoot.equals(nid);
    }
  }

  /**
   * I don't care about line numbers but changes inside a node's range from version to version
   */
  private static class ContentRange {
    private final List<SNodeId> myNodes;
    private final List<LineContent[]> myRanges;

    /*package*/ ContentRange() {
      myNodes = new ArrayList<SNodeId>();
      myRanges = new ArrayList<LineContent[]>();
    }

    public void recordRange(SNodeId nid, int fromLine, int toLine, List<LineContent> modelContent) {
      LineContent[] arr = new LineContent[toLine - fromLine + 1];
      for (int i = 0; i < arr.length; i++) {
        arr[i] = modelContent.get(fromLine + i);
      }
      myNodes.add(nid);
      myRanges.add(arr);
    }

    public boolean isEmpty() {
      return myNodes.isEmpty();
    }

    public boolean changedAgainst(ContentRange other) {
      if (!(myNodes.equals(other.myNodes))) {
        return true;
      }
      assert myRanges.size() == other.myRanges.size();
      for (int i = 0, x = myRanges.size(); i < x; i++) {
        LineContent[] a1 = myRanges.get(i);
        LineContent[] a2 = other.myRanges.get(i);
        if (a1.length != a2.length) {
          return true;
        }
        for (int j = 0; j < a1.length; j++) {
          if (!(a1[j].matches(a2[j]))) {
            return true;
          }
        }
      }
      return false;
    }
  }
}
