/*
* 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.util.Side;
import com.intellij.util.SmartList;
import gnu.trove.TIntFunction;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class LineNumberConvertor {
// Oneside -> Twoside
@NotNull private final TreeMap<Integer, Integer> myFragments1;
@NotNull private final TreeMap<Integer, Integer> myFragments2;
// Twoside -> Oneside
@NotNull private final TreeMap<Integer, Integer> myInvertedFragments1;
@NotNull private final TreeMap<Integer, Integer> myInvertedFragments2;
@NotNull private final Corrector myCorrector = new Corrector();
public LineNumberConvertor(@NotNull TreeMap<Integer, Integer> fragments1,
@NotNull TreeMap<Integer, Integer> fragments2,
@NotNull TreeMap<Integer, Integer> invertedFragments1,
@NotNull TreeMap<Integer, Integer> invertedFragments2) {
myFragments1 = fragments1;
myFragments2 = fragments2;
myInvertedFragments1 = invertedFragments1;
myInvertedFragments2 = invertedFragments2;
}
public int convert1(int value) {
return convert(value, Side.LEFT, true, false);
}
public int convert2(int value) {
return convert(value, Side.RIGHT, true, false);
}
public int convertInv1(int value) {
return convert(value, Side.LEFT, false, false);
}
public int convertInv2(int value) {
return convert(value, Side.RIGHT, false, false);
}
public int convertApproximate1(int value) {
return convert(value, Side.LEFT, true, true);
}
public int convertApproximate2(int value) {
return convert(value, Side.RIGHT, true, true);
}
public int convertApproximateInv1(int value) {
return convert(value, Side.LEFT, false, true);
}
public int convertApproximateInv2(int value) {
return convert(value, Side.RIGHT, false, true);
}
//
// Impl
//
@NotNull
public TIntFunction createConvertor1() {
return this::convert1;
}
@NotNull
public TIntFunction createConvertor2() {
return this::convert2;
}
private int convert(int value, @NotNull Side side, boolean fromOneside, boolean approximate) {
return myCorrector.convertCorrected(value, side, fromOneside, approximate);
}
/*
* Both oneside and one of the sides were changed in a same way. We should update converters, because of changed shift.
*/
public void handleOnesideChange(int startLine, int endLine, int shift, @NotNull Side masterSide) {
myCorrector.handleOnesideChange(startLine, endLine, shift, masterSide);
}
private static int convert(@NotNull final TreeMap<Integer, Integer> fragments, int value, boolean approximate) {
return approximate ? convertApproximate(fragments, value) : convert(fragments, value);
}
/*
* This convertor returns exact matching between lines, and -1 if it's impossible
*/
private static int convert(@NotNull final TreeMap<Integer, Integer> fragments, int value) {
Map.Entry<Integer, Integer> floor = fragments.floorEntry(value);
if (floor == null || floor.getValue() == -1) return -1;
return floor.getValue() - floor.getKey() + value;
}
/*
* This convertor returns 'good enough' position, even if exact matching is impossible
*/
private static int convertApproximate(@NotNull final TreeMap<Integer, Integer> fragments, int value) {
Map.Entry<Integer, Integer> floor = fragments.floorEntry(value);
if (floor == null) return 0;
if (floor.getValue() != -1) return floor.getValue() - floor.getKey() + value;
Map.Entry<Integer, Integer> floorHead = fragments.floorEntry(floor.getKey() - 1);
assert floorHead != null && floorHead.getValue() != -1;
return floorHead.getValue() - floorHead.getKey() + floor.getKey();
}
@NotNull
private TreeMap<Integer, Integer> getFragments(@NotNull Side side, boolean fromOneside) {
return fromOneside ? side.select(myFragments1, myFragments2)
: side.select(myInvertedFragments1, myInvertedFragments2);
}
public static class Builder {
@NotNull private final TreeMap<Integer, Integer> myFragments1 = new TreeMap<>();
@NotNull private final TreeMap<Integer, Integer> myFragments2 = new TreeMap<>();
@NotNull private final TreeMap<Integer, Integer> myInvertedFragments1 = new TreeMap<>();
@NotNull private final TreeMap<Integer, Integer> myInvertedFragments2 = new TreeMap<>();
public void put1(int start, int newStart, int length) {
myFragments1.put(start, newStart);
myFragments1.put(start + length, -1);
myInvertedFragments1.put(newStart, start);
myInvertedFragments1.put(newStart + length, -1);
}
public void put2(int start, int newStart, int length) {
myFragments2.put(start, newStart);
myFragments2.put(start + length, -1);
myInvertedFragments2.put(newStart, start);
myInvertedFragments2.put(newStart + length, -1);
}
@NotNull
public LineNumberConvertor build() {
return new LineNumberConvertor(myFragments1, myFragments2, myInvertedFragments1, myInvertedFragments2);
}
}
/*
* myFragments allow to convert between Sm-So-Su, Mm-Mo-Mu, Em-Eo-Eu.
*
* Corrector processes information about synchronous modification B -> B' (when masterSide and oneside are modified the same way),
* and allows to convert between Sm'-So'-Su', Mm'-Mo'-Mu', Em'-Eo'-Eu'.
*
*
* Before After
*
* masterSide unchangedSide
* oneside
*
* Sm + + So + Su Sm' + + So' + Su'
* | | | | | |
* | | | | | |
* | | | | | |
* Am +-------+ Ao + Au Am' +-------+ Ao' + Au'
* | | | | | |
* Mm + B + Mo + Mu Mm' + + Mo' + Mu'
* | | | | B' | |
* Bm +-------+ Bo | | | |
* | | | | | |
* | | | Bm' +-------+ Bo' |
* | | | | | |
* Em + + Eo + Eu | | + Eu'
* | |
* Em' + + Eo'
*
* Su == Su'; Mu == Mu'; Eu == Eu'; Au == Au'
* Sm == Sm'; So == So'; Am == Am'; Ao == Ao'
* Bo - Ao == Bm - Am; Bo' - Ao' == Bm' - Am'
*
* change.startOneside == Ao == Ao'
* change.startTwoside == Am == Am'
*
* change.oldLength == Bo - Ao
* change.newLength == Bo' - Ao'
*
* change.side == masterSide
*
* In case of multiple changes - we process them in reverse order (from new to old).
*
*/
private class Corrector {
private final List<CorrectedChange> myChanges = new SmartList<>();
@SuppressWarnings("UnnecessaryLocalVariable")
public void handleOnesideChange(int startLine, int endLine, int shift, @NotNull Side masterSide) {
int oldOnesideStart = startLine;
int oldTwosideStart = convert(startLine, masterSide, true, false);
assert oldTwosideStart != -1;
int oldLength = endLine - startLine;
int newLength = oldLength + shift;
myChanges.add(new CorrectedChange(oldOnesideStart, oldTwosideStart, oldLength, newLength, masterSide));
}
public int convertCorrected(int value, @NotNull Side side, boolean fromOneside, boolean approximate) {
if (fromOneside) {
return convertFromOneside(value, side, approximate, myChanges.size() - 1);
}
else {
return convertFromTwoside(value, side, approximate, myChanges.size() - 1);
}
}
private int convertFromTwoside(int value, @NotNull Side side, boolean approximate, int index) {
if (index < 0) {
return convert(getFragments(side, false), value, approximate);
}
CorrectedChange change = myChanges.get(index);
int shift = change.newLength - change.oldLength;
if (change.side != side) { // ?u' -> ?o'
int converted = convertFromTwoside(value, side, approximate, index - 1);
if (converted < change.startOneside) { // Su' -> So'
// Su' == Su; So' == So
// value == Su'; converted == So
return converted;
}
if (converted >= change.startOneside + change.oldLength) { // Eu' -> Eo'
// Eo' == Eo + shift; Eu' == Eu
// value == Eu'; converted == Eo
return converted + shift;
}
// Mu' -> Mo'
if (!approximate) return -1;
// We can't convert Mo into Mo'
// converted == Mo
return append(converted, Math.min(change.newLength, converted - change.startOneside));
}
else { // ?m '-> ?o'
if (value < change.startTwoside) { // Sm' -> So'
return convertFromTwoside(value, side, approximate, index - 1);
}
if (value >= change.startTwoside + change.newLength) { // Em' -> Eo'
// Em' == Em + shift; Eo' == Eo + shift
// value == Em'; converted == Eo
int converted = convertFromTwoside(value - shift, side, approximate, index - 1);
return append(converted, shift);
}
// Mm' -> Mo'
// Ao == Ao'; Am == Am'; Mo' - Ao' == Mm' - Am'
// convertedStart == Ao; value - change.startOneside == Mm' - Am'
int convertedStart = convertFromTwoside(change.startTwoside, side, approximate, index - 1);
return append(convertedStart, value - change.startTwoside);
}
}
private int convertFromOneside(int value, @NotNull Side side, boolean approximate, int index) {
if (index < 0) {
return convert(getFragments(side, true), value, approximate);
}
CorrectedChange change = myChanges.get(index);
int shift = change.newLength - change.oldLength;
if (value < change.startOneside) { // So' -> Sm', So' -> Su'
// So' == So; Sm' == Sm; Su' == Su
// value = So'
return convertFromOneside(value, side, approximate, index - 1);
}
if (value >= change.startOneside + change.newLength) { // Eo' -> Em', Eo' -> Eu'
// Eo' == Eo + shift; Em' == Em + shift; Eu' == Eu
// value = Eo'
int converted = convertFromOneside(value - shift, side, approximate, index - 1);
return append(converted, side == change.side ? shift : 0);
}
if (side != change.side) { // Mo' -> Mu'
if (!approximate) return -1;
// we can't convert Mo' into Mo. And thus get valid Mu/Mu'.
// return: Au'
return convertFromOneside(change.startOneside, side, approximate, index - 1);
}
else { // Mo' -> Mm'
// Ao == Ao'; Am == Am'; Mo' - Ao' == Mm' - Am'
// value = Mo'
int convertedStart = convertFromOneside(change.startOneside, side, approximate, index - 1);
return append(convertedStart, value - change.startOneside);
}
}
private int append(int value, int shift) {
return value == -1 ? -1 : value + shift;
}
}
private static class CorrectedChange {
public final int startOneside;
public final int startTwoside;
public final int oldLength;
public final int newLength;
@NotNull public final Side side;
public CorrectedChange(int startOneside, int startTwoside, int oldLength, int newLength, @NotNull Side side) {
this.startOneside = startOneside;
this.startTwoside = startTwoside;
this.oldLength = oldLength;
this.newLength = newLength;
this.side = side;
}
}
}