/*
 * Copyright 2000-2014 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.util.diff;

import java.util.BitSet;

class PatienceIntLCS {
  private final int[] myFirst;
  private final int[] mySecond;

  private final int myStart1;
  private final int myStart2;
  private final int myCount1;
  private final int myCount2;

  private final BitSet myChanges1;
  private final BitSet myChanges2;

  public PatienceIntLCS(int[] first, int[] second) {
    this(first, second, 0, first.length, 0, second.length, new BitSet(first.length), new BitSet(second.length));
  }

  public PatienceIntLCS(int[] first, int[] second, int start1, int count1, int start2, int count2, BitSet changes1, BitSet changes2) {
    myFirst = first;
    mySecond = second;
    myStart1 = start1;
    myStart2 = start2;
    myCount1 = count1;
    myCount2 = count2;

    myChanges1 = changes1;
    myChanges2 = changes2;
  }

  public void execute() throws FilesTooBigForDiffException {
    execute(false);
  }

  public void execute(boolean failOnSmallReduction) throws FilesTooBigForDiffException {
    int thresholdCheckCounter = failOnSmallReduction ? 2 : -1;
    execute(myStart1, myCount1, myStart2, myCount2, thresholdCheckCounter);
  }

  private void execute(int start1, int count1, int start2, int count2, int thresholdCheckCounter) throws FilesTooBigForDiffException {
    if (count1 == 0 && count2 == 0) {
      return;
    }

    if (count1 == 0 || count2 == 0) {
      addChange(start1, count1, start2, count2);
      return;
    }

    int startOffset = matchForward(start1, count1, start2, count2);
    start1 += startOffset;
    start2 += startOffset;
    count1 -= startOffset;
    count2 -= startOffset;

    int endOffset = matchBackward(start1, count1, start2, count2);
    count1 -= endOffset;
    count2 -= endOffset;

    if (count1 == 0 || count2 == 0) {
      addChange(start1, count1, start2, count2);
    }
    else {
      if (thresholdCheckCounter == 0) checkReduction(count1, count2);
      thresholdCheckCounter = Math.max(-1, thresholdCheckCounter - 1);

      UniqueLCS uniqueLCS = new UniqueLCS(myFirst, mySecond, start1, count1, start2, count2);
      int[][] matching = uniqueLCS.execute();

      if (matching == null) {
        if (thresholdCheckCounter >= 0) checkReduction(count1, count2);
        IntLCS intLCS = new IntLCS(myFirst, mySecond, start1, count1, start2, count2, myChanges1, myChanges2);
        intLCS.execute();
      }
      else {
        int s1, s2, c1, c2;
        int matched = matching[0].length;
        assert matched > 0;

        c1 = matching[0][0];
        c2 = matching[1][0];

        execute(start1, c1, start2, c2, thresholdCheckCounter);

        for (int i = 1; i < matching[0].length; i++) {
          s1 = matching[0][i - 1] + 1;
          s2 = matching[1][i - 1] + 1;

          c1 = matching[0][i] - s1;
          c2 = matching[1][i] - s2;

          if (c1 > 0 || c2 > 0) {
            execute(start1 + s1, c1, start2 + s2, c2, thresholdCheckCounter);
          }
        }

        if (matching[0][matched - 1] == count1 - 1) {
          s1 = count1 - 1;
          c1 = 0;
        }
        else {
          s1 = matching[0][matched - 1] + 1;
          c1 = count1 - s1;
        }
        if (matching[1][matched - 1] == count2 - 1) {
          s2 = count2 - 1;
          c2 = 0;
        }
        else {
          s2 = matching[1][matched - 1] + 1;
          c2 = count2 - s2;
        }

        execute(start1 + s1, c1, start2 + s2, c2, thresholdCheckCounter);
      }
    }
  }

  private int matchForward(int start1, int count1, int start2, int count2) {
    final int size = Math.min(count1, count2);
    int idx = 0;
    for (int i = 0; i < size; i++) {
      if (!(myFirst[start1 + i] == mySecond[start2 + i])) break;
      ++idx;
    }
    return idx;
  }

  private int matchBackward(int start1, int count1, int start2, int count2) {
    final int size = Math.min(count1, count2);
    int idx = 0;
    for (int i = 1; i <= size; i++) {
      if (!(myFirst[start1 + count1 - i] == mySecond[start2 + count2 - i])) break;
      ++idx;
    }
    return idx;
  }

  private void addChange(int start1, int count1, int start2, int count2) {
    myChanges1.set(start1, start1 + count1);
    myChanges2.set(start2, start2 + count2);
  }

  public BitSet[] getChanges() {
    return new BitSet[]{myChanges1, myChanges2};
  }

  private void checkReduction(int count1, int count2) throws FilesTooBigForDiffException {
    if (count1 * 2 < myCount1) return;
    if (count2 * 2 < myCount2) return;
    throw new FilesTooBigForDiffException(0);
  }
}
