/*
 * Copyright 2000-2009 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.openapi.vcs.ex;

import com.intellij.diff.comparison.ByLine;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.comparison.TrimUtil;
import com.intellij.diff.comparison.iterables.DiffIterableUtil;
import com.intellij.diff.comparison.iterables.FairDiffIterable;
import com.intellij.diff.util.DiffUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.DumbProgressIndicator;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.ex.Range.InnerRange;
import com.intellij.util.diff.FilesTooBigForDiffException;
import org.jetbrains.annotations.NotNull;

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

public class RangesBuilder {
  private static final Logger LOG = Logger.getInstance(RangesBuilder.class);

  @NotNull
  public static List<Range> createRanges(@NotNull Document current, @NotNull Document vcs) throws FilesTooBigForDiffException {
    return createRanges(current, vcs, false);
  }

  @NotNull
  public static List<Range> createRanges(@NotNull Document current, @NotNull Document vcs, boolean innerWhitespaceChanges)
    throws FilesTooBigForDiffException {
    return createRanges(DiffUtil.getLines(current), DiffUtil.getLines(vcs), 0, 0, innerWhitespaceChanges);
  }

  @NotNull
  public static List<Range> createRanges(@NotNull List<String> current,
                                         @NotNull List<String> vcs,
                                         int currentShift,
                                         int vcsShift,
                                         boolean innerWhitespaceChanges) throws FilesTooBigForDiffException {
    if (innerWhitespaceChanges) {
      return createRangesSmart(current, vcs, currentShift, vcsShift);
    }
    else {
      return createRangesSimple(current, vcs, currentShift, vcsShift);
    }
  }

  @NotNull
  private static List<Range> createRangesSimple(@NotNull List<String> current,
                                                @NotNull List<String> vcs,
                                                int currentShift,
                                                int vcsShift) throws FilesTooBigForDiffException {
    FairDiffIterable iterable = ByLine.compare(vcs, current, ComparisonPolicy.DEFAULT, DumbProgressIndicator.INSTANCE);

    List<Range> result = new ArrayList<>();
    for (com.intellij.diff.util.Range range : iterable.iterateChanges()) {
      int vcsLine1 = vcsShift + range.start1;
      int vcsLine2 = vcsShift + range.end1;
      int currentLine1 = currentShift + range.start2;
      int currentLine2 = currentShift + range.end2;

      result.add(new Range(currentLine1, currentLine2, vcsLine1, vcsLine2));
    }
    return result;
  }

  @NotNull
  private static List<Range> createRangesSmart(@NotNull List<String> current,
                                               @NotNull List<String> vcs,
                                               int currentShift,
                                               int vcsShift) throws FilesTooBigForDiffException {
    FairDiffIterable iwIterable = ByLine.compare(vcs, current, ComparisonPolicy.IGNORE_WHITESPACES, DumbProgressIndicator.INSTANCE);

    RangeBuilder rangeBuilder = new RangeBuilder(current, vcs, currentShift, vcsShift);

    for (com.intellij.diff.util.Range range : iwIterable.iterateUnchanged()) {
      int count = range.end1 - range.start1;
      for (int i = 0; i < count; i++) {
        int vcsIndex = range.start1 + i;
        int currentIndex = range.start2 + i;
        if (vcs.get(vcsIndex).equals(current.get(currentIndex))) {
          rangeBuilder.markEqual(vcsIndex, currentIndex);
        }
      }
    }

    return rangeBuilder.finish();
  }

  private static class RangeBuilder extends DiffIterableUtil.ChangeBuilderBase {
    @NotNull private final List<String> myCurrent;
    @NotNull private final List<String> myVcs;
    private final int myCurrentShift;
    private final int myVcsShift;

    @NotNull private final List<Range> myResult = new ArrayList<>();

    public RangeBuilder(@NotNull List<String> current,
                        @NotNull List<String> vcs,
                        int currentShift,
                        int vcsShift) {
      super(vcs.size(), current.size());
      myCurrent = current;
      myVcs = vcs;
      myCurrentShift = currentShift;
      myVcsShift = vcsShift;
    }

    @NotNull
    public List<Range> finish() {
      doFinish();
      return myResult;
    }

    @Override
    protected void addChange(int vcsStart, int currentStart, int vcsEnd, int currentEnd) {
      com.intellij.diff.util.Range range = TrimUtil.expand(myVcs, myCurrent, vcsStart, currentStart, vcsEnd, currentEnd);
      if (range.isEmpty()) return;

      List<InnerRange> innerRanges = calcInnerRanges(range);
      Range newRange = new Range(range.start2, range.end2, range.start1, range.end1, innerRanges);
      newRange.shift(myCurrentShift);
      newRange.vcsShift(myVcsShift);

      myResult.add(newRange);
    }

    @NotNull
    private List<InnerRange> calcInnerRanges(@NotNull com.intellij.diff.util.Range blockRange) {
      List<String> vcs = myVcs.subList(blockRange.start1, blockRange.end1);
      List<String> current = myCurrent.subList(blockRange.start2, blockRange.end2);

      ArrayList<InnerRange> result = new ArrayList<>();
      FairDiffIterable iwIterable = ByLine.compare(vcs, current, ComparisonPolicy.IGNORE_WHITESPACES, DumbProgressIndicator.INSTANCE);
      for (Pair<com.intellij.diff.util.Range, Boolean> pair : DiffIterableUtil.iterateAll(iwIterable)) {
        com.intellij.diff.util.Range range = pair.first;
        Boolean equals = pair.second;

        byte type = equals ? Range.EQUAL : getChangeType(range.start1, range.end1, range.start2, range.end2);
        result.add(new InnerRange(range.start2 + blockRange.start2, range.end2 + blockRange.start2,
                                  type));
      }
      result.trimToSize();
      return result;
    }
  }

  private static byte getChangeType(int vcsStart, int vcsEnd, int currentStart, int currentEnd) {
    int deleted = vcsEnd - vcsStart;
    int inserted = currentEnd - currentStart;
    if (deleted > 0 && inserted > 0) return Range.MODIFIED;
    if (deleted > 0) return Range.DELETED;
    if (inserted > 0) return Range.INSERTED;
    LOG.error("Unknown change type");
    return Range.EQUAL;
  }
}
