/*
 * Copyright 2000-2015 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 com.intellij.diff.tools.fragmented;

import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.util.LineRange;
import com.intellij.diff.util.Side;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import org.jetbrains.annotations.NotNull;

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

class UnifiedFragmentBuilder {
  @NotNull private final List<? extends LineFragment> myFragments;
  @NotNull private final Document myDocument1;
  @NotNull private final Document myDocument2;
  @NotNull private final Side myMasterSide;

  @NotNull private final StringBuilder myBuilder = new StringBuilder();
  @NotNull private final List<ChangedBlock> myBlocks = new ArrayList<>();
  @NotNull private final List<HighlightRange> myRanges = new ArrayList<>();
  @NotNull private final LineNumberConvertor.Builder myConvertor1 = new LineNumberConvertor.Builder();
  @NotNull private final LineNumberConvertor.Builder myConvertor2 = new LineNumberConvertor.Builder();
  @NotNull private final List<LineRange> myChangedLines = new ArrayList<>();

  UnifiedFragmentBuilder(@NotNull List<? extends LineFragment> fragments,
                                @NotNull Document document1,
                                @NotNull Document document2,
                                @NotNull Side masterSide) {
    myFragments = fragments;
    myDocument1 = document1;
    myDocument2 = document2;
    myMasterSide = masterSide;
  }

  private boolean myEqual = false;

  private int lastProcessedLine1 = -1;
  private int lastProcessedLine2 = -1;
  private int totalLines = 0;

  public void exec() {
    if (myFragments.isEmpty()) {
      myEqual = true;
      appendTextMaster(0, 0, getLineCount(myDocument1) - 1, getLineCount(myDocument2) - 1);
      return;
    }

    for (LineFragment fragment : myFragments) {
      processEquals(fragment.getStartLine1() - 1, fragment.getStartLine2() - 1);
      processChanged(fragment);
    }
    processEquals(getLineCount(myDocument1) - 1, getLineCount(myDocument2) - 1);
  }

  private void processEquals(int endLine1, int endLine2) {
    int startLine1 = lastProcessedLine1 + 1;
    int startLine2 = lastProcessedLine2 + 1;

    appendTextMaster(startLine1, startLine2, endLine1, endLine2);
  }

  @SuppressWarnings("UnnecessaryLocalVariable")
  private void processChanged(@NotNull LineFragment fragment) {
    int startLine1 = fragment.getStartLine1();
    int endLine1 = fragment.getEndLine1() - 1;
    int lines1 = endLine1 - startLine1;

    int startLine2 = fragment.getStartLine2();
    int endLine2 = fragment.getEndLine2() - 1;
    int lines2 = endLine2 - startLine2;

    int linesBefore = totalLines;
    int linesAfter;

    if (lines1 >= 0) {
      int startOffset1 = myDocument1.getLineStartOffset(startLine1);
      int endOffset1 = myDocument1.getLineEndOffset(endLine1);
      appendTextSide(Side.LEFT, startOffset1, endOffset1, lines1, lines2, startLine1, -1);
    }

    int linesBetween = totalLines;

    if (lines2 >= 0) {
      int startOffset2 = myDocument2.getLineStartOffset(startLine2);
      int endOffset2 = myDocument2.getLineEndOffset(endLine2);
      appendTextSide(Side.RIGHT, startOffset2, endOffset2, lines2, lines2, -1, startLine2);
    }

    linesAfter = totalLines;

    int blockStartLine1 = linesBefore;
    int blockEndLine1 = linesBetween;
    int blockStartLine2 = linesBetween;
    int blockEndLine2 = linesAfter;

    myBlocks.add(new ChangedBlock(new LineRange(blockStartLine1, blockEndLine1),
                                  new LineRange(blockStartLine2, blockEndLine2),
                                  fragment));

    lastProcessedLine1 = endLine1;
    lastProcessedLine2 = endLine2;
  }

  private void appendTextMaster(int startLine1, int startLine2, int endLine1, int endLine2) {
    // The slave-side line matching might be incomplete for non-fair line fragments (@see FairDiffIterable)
    // If it ever became an issue, it could be fixed by explicit fair by-line comparing of "equal" regions

    int lines1 = endLine1 - startLine1;
    int lines2 = endLine2 - startLine2;
    if (myMasterSide.select(lines1, lines2) >= 0) {
      int startOffset = myMasterSide.isLeft() ? myDocument1.getLineStartOffset(startLine1) : myDocument2.getLineStartOffset(startLine2);
      int endOffset = myMasterSide.isLeft() ? myDocument1.getLineEndOffset(endLine1) : myDocument2.getLineEndOffset(endLine2);

      appendText(myMasterSide, startOffset, endOffset, lines1, lines2, startLine1, startLine2);
    }
  }

  private void appendTextSide(@NotNull Side side, int offset1, int offset2, int lines1, int lines2, int startLine1, int startLine2) {
    int linesBefore = totalLines;
    appendText(side, offset1, offset2, lines1, lines2, startLine1, startLine2);
    int linesAfter = totalLines;

    if (linesBefore != linesAfter) myChangedLines.add(new LineRange(linesBefore, linesAfter));
  }

  private void appendText(@NotNull Side side, int offset1, int offset2, int lines1, int lines2, int startLine1, int startLine2) {
    int lines = side.select(lines1, lines2);
    boolean notEmpty = lines >= 0;

    int appendix = notEmpty ? 1 : 0;
    if (startLine1 != -1) {
      myConvertor1.put(totalLines, startLine1, lines + appendix, lines1 + appendix);
    }
    if (startLine2 != -1) {
      myConvertor2.put(totalLines, startLine2, lines + appendix, lines2 + appendix);
    }

    if (notEmpty) {
      Document document = side.select(myDocument1, myDocument2);

      int newline = document.getTextLength() > offset2 + 1 ? 1 : 0;
      TextRange base = new TextRange(myBuilder.length(), myBuilder.length() + offset2 - offset1 + newline);
      TextRange changed = new TextRange(offset1, offset2 + newline);
      myRanges.add(new HighlightRange(side, base, changed));

      myBuilder.append(document.getCharsSequence().subSequence(offset1, offset2));
      myBuilder.append('\n');

      totalLines += lines + 1;
    }
  }

  private static int getLineCount(@NotNull Document document) {
    return Math.max(document.getLineCount(), 1);
  }

  //
  // Result
  //


  public boolean isEqual() {
    return myEqual;
  }

  @NotNull
  public CharSequence getText() {
    return myBuilder;
  }

  @NotNull
  public List<ChangedBlock> getBlocks() {
    return myBlocks;
  }

  @NotNull
  public List<HighlightRange> getRanges() {
    return myRanges;
  }

  @NotNull
  public LineNumberConvertor getConvertor1() {
    return myConvertor1.build();
  }

  @NotNull
  public LineNumberConvertor getConvertor2() {
    return myConvertor2.build();
  }

  @NotNull
  public List<LineRange> getChangedLines() {
    return myChangedLines;
  }
}
