package org.xpect.util;
import static org.xpect.util.IDifferencer.ISimilarityFunction.UPPER_SIMILARITY_BOUND;
import static org.xpect.util.IDifferencer.MatchKind.EQUAL;
import static org.xpect.util.IDifferencer.MatchKind.LEFT_ONLY;
import static org.xpect.util.IDifferencer.MatchKind.RIGHT_ONLY;
import static org.xpect.util.IDifferencer.MatchKind.SIMILAR;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
import org.xpect.text.Table;
import org.xpect.text.Table.Column;
import org.xpect.text.Table.Row;
import com.google.common.base.Preconditions;
public class DifferencerImpl implements IDifferencer {
private enum Direction {
BACKWARD, FORWARD
}
private static class DirectionState {
private final Direction direction;
private final State<?> state;
public DirectionState(State<?> state, Direction direction) {
super();
this.state = state;
this.direction = direction;
}
}
private static class Match implements IDifferencer.Match, Comparable<Match> {
private final float cost;
private final DirectionState direction;
private final MatchKind kind;
private final int left;
private final Match previous;
private final int right;
private final float similarity;
public Match(DirectionState direction, int left, int right) {
super();
this.direction = direction;
this.kind = MatchKind.EQUAL;
this.previous = null;
this.cost = 0.0f;
this.similarity = 0.0f;
this.left = left;
this.right = right;
}
public Match(Match previous, MatchKind kind, int left, int right, float similarity) {
super();
// Preconditions.checkPositionIndex(Math.abs(previous.left - left), 1);
// Preconditions.checkPositionIndex(Math.abs(previous.right - right), 1);
this.direction = previous.direction;
this.kind = kind;
this.previous = previous;
this.similarity = similarity;
this.cost = previous.cost + similarity;
this.left = left;
this.right = right;
}
public int compareTo(Match o) {
if (cost < o.cost)
return -1;
if (cost > o.cost)
return 1;
switch (kind) {
case LEFT_ONLY:
switch (o.kind) {
case RIGHT_ONLY:
return direction.direction == Direction.FORWARD ? -1 : 1;
default:
}
case RIGHT_ONLY:
switch (o.kind) {
case LEFT_ONLY:
return direction.direction == Direction.FORWARD ? 1 : -1;
default:
}
default:
}
if (left < o.left)
return -1;
if (left > o.left)
return 1;
if (right < o.right)
return -1;
if (right > o.right)
return 1;
return 0;
}
public MatchKind getKind() {
return kind;
}
public int getLeft() {
if (kind == RIGHT_ONLY)
return IDifferencer.Match.NO_INDEX;
return left;
}
public int getRight() {
if (kind == LEFT_ONLY)
return IDifferencer.Match.NO_INDEX;
return right;
}
public float getSimilarity() {
switch (kind) {
case EQUAL:
return IDifferencer.Match.EQUAL;
case LEFT_ONLY:
case RIGHT_ONLY:
return IDifferencer.Match.UNEQUAL;
case SIMILAR:
return similarity;
}
return 0;
}
@Override
public String toString() {
switch (kind) {
case EQUAL:
return "[" + left + "==" + right + "]";
case LEFT_ONLY:
return "[" + left + "---]";
case RIGHT_ONLY:
return "[---" + right + "]";
case SIMILAR:
return "[" + left + "~~" + right + "]";
}
return "(unknownKind)";
}
}
private static class Solution<T> {
private final Match backwardsEnd;
private final float cost;
private final Match forwardEnd;
private final State<T> state;
public Solution(State<T> state, Match match1, Match match2) {
Preconditions.checkArgument(match1.direction.state == state);
Preconditions.checkArgument(match2.direction.state == state);
Preconditions.checkArgument(match1.direction != match2.direction);
Preconditions.checkArgument(match1.left == match2.left);
Preconditions.checkArgument(match1.right == match2.right);
this.forwardEnd = match1.direction.direction == Direction.FORWARD ? match1 : match2;
this.backwardsEnd = match1.direction.direction == Direction.FORWARD ? match2 : match1;
this.cost = match1.cost + match2.cost;
this.state = state;
}
private int getlastLeft(Match m) {
return m.kind == RIGHT_ONLY ? getlastLeft(m.previous) : m.left;
}
private int getlastRight(Match m) {
return m.kind == LEFT_ONLY ? getlastRight(m.previous) : m.right;
}
public boolean isBetterThan(Solution<T> o) {
if (cost < o.cost)
return true;
if (cost > o.cost)
return false;
return false;
// if (forwardEnd.left < o.forwardEnd.left)
// return true;
// if (forwardEnd.left > o.forwardEnd.left)
// return false;
// if (forwardEnd.right < o.forwardEnd.right)
// return true;
// if (forwardEnd.right > o.forwardEnd.right)
// return false;
// return false;
}
public List<IDifferencer.Match> toList() {
List<IDifferencer.Match> result = new ToStrList(state.left, state.right);
List<IDifferencer.Match> temp = new ArrayList<IDifferencer.Match>();
for (Match m = forwardEnd; m.previous != null; m = m.previous)
temp.add(m);
for (int i = temp.size() - 1; i >= 0; i--)
result.add(temp.get(i));
int lastLeft = getlastLeft(forwardEnd);
int lastRight = getlastRight(forwardEnd);
for (Match m = backwardsEnd; m.previous != null; m = m.previous)
switch (m.kind) {
case LEFT_ONLY:
if (m.left > lastLeft)
result.add(m);
break;
case RIGHT_ONLY:
if (m.right > lastRight)
result.add(m);
break;
default:
if (m.left > lastLeft && m.right > lastRight)
result.add(m);
else if (m.left > lastLeft)
result.add(new Match(m.previous, MatchKind.LEFT_ONLY, m.left, IDifferencer.Match.NO_INDEX, m.similarity));
else if (m.right > lastRight)
result.add(new Match(m.previous, MatchKind.RIGHT_ONLY, IDifferencer.Match.NO_INDEX, m.right, m.similarity));
break;
}
return result;
}
@Override
public String toString() {
return toList().toString();
}
}
private static class State<T> {
private final List<T> left;
private final List<T> right;
private TreeSet<Match> unvisited = new TreeSet<Match>();
private final HashMap<Long, Match> visited = new HashMap<Long, Match>();
public State(List<T> left, List<T> right) {
super();
this.left = left;
this.right = right;
}
@Override
public String toString() {
return format(left, right, unvisited);
}
}
@SuppressWarnings("serial")
private static class ToStrList extends ArrayList<IDifferencer.Match> {
private final List<?> left;
private final List<?> right;
private ToStrList(List<?> left, List<?> right) {
super();
this.left = left;
this.right = right;
}
@Override
public String toString() {
return format(left, right, this);
}
}
private static String format(List<?> left, List<?> right, Collection<?> matches) {
Table table = new Table();
table.setMaxCellWidth(5);
table.setRowSeparatorHeight(0);
table.setSeparatorCrossingBackground("+");
Row topHeader = table.getRow(0);
Row topIndex = table.getRow(1);
topIndex.getBottomSeparator().setBackground("-").setHeight(1);
topIndex.getCell(2).setText("-1");
for (int i = 0; i < right.size(); i++) {
topHeader.getCell(i + 3).setText(right.get(i));
topIndex.getCell(i + 3).setText(i);
}
Column leftHeader = table.getColumn(0);
Column leftIndex = table.getColumn(1);
// leftHeader.getRightSeparator().setWidth(0);
leftIndex.getRightSeparator().setBackground("|");
leftIndex.getCell(2).setText("-1");
for (int i = 0; i < left.size(); i++) {
leftHeader.getCell(i + 3).setText(left.get(i));
leftIndex.getCell(i + 3).setText(i);
}
for (Object o : matches) {
Match match = (Match) o;
String dir = String.valueOf(match.direction.direction.name().charAt(0));
table.getCell(match.left + 3, match.right + 3).setText(match.cost + dir);
}
return table.toString();
}
public <T> List<IDifferencer.Match> diff(List<T> left, List<T> right, ISimilarityFunction<? super T> similarityFunction) {
State<T> state = new State<T>(left, right);
DirectionState forward = new DirectionState(state, Direction.FORWARD);
DirectionState backward = new DirectionState(state, Direction.BACKWARD);
final int leftSize = left.size();
final int rightSize = right.size();
Solution<T> solution = null;
state.unvisited.add(new Match(forward, -1, -1));
state.unvisited.add(new Match(backward, leftSize, rightSize));
Match current;
while (!state.unvisited.isEmpty()) {
current = state.unvisited.pollFirst();
List<Match> newMatches = new LinkedList<Match>();
switch (current.direction.direction) {
case FORWARD:
int forwardLeft = current.left + 1;
int forwardRight = current.right + 1;
if (forwardLeft < leftSize)
newMatches.add(new Match(current, LEFT_ONLY, forwardLeft, current.right, UPPER_SIMILARITY_BOUND));
if (forwardRight < rightSize)
newMatches.add(new Match(current, RIGHT_ONLY, current.left, forwardRight, UPPER_SIMILARITY_BOUND));
if (forwardLeft < leftSize && forwardRight < rightSize) {
float similarity = Math.abs(similarityFunction.similarity(left.get(forwardLeft), right.get(forwardRight)));
if (similarity == ISimilarityFunction.EQUAL)
newMatches.add(new Match(current, EQUAL, forwardLeft, forwardRight, similarity));
else if (similarity < UPPER_SIMILARITY_BOUND)
newMatches.add(new Match(current, SIMILAR, forwardLeft, forwardRight, similarity));
}
break;
case BACKWARD:
int backwardLeft = current.left - 1;
int backwardRight = current.right - 1;
if (backwardLeft >= 0)
newMatches.add(new Match(current, LEFT_ONLY, backwardLeft, current.right, UPPER_SIMILARITY_BOUND));
if (backwardRight >= 0)
newMatches.add(new Match(current, RIGHT_ONLY, current.left, backwardRight, UPPER_SIMILARITY_BOUND));
if (backwardLeft >= 0 && backwardRight >= 0) {
float similarity = Math.abs(similarityFunction.similarity(left.get(backwardLeft), right.get(backwardRight)));
if (similarity == ISimilarityFunction.EQUAL)
newMatches.add(new Match(current, EQUAL, backwardLeft, backwardRight, similarity));
else if (similarity < UPPER_SIMILARITY_BOUND)
newMatches.add(new Match(current, SIMILAR, backwardLeft, backwardRight, similarity));
}
break;
}
for (Match match : newMatches) {
Long key = Long.valueOf((((long) match.left) << 32) + ((long) match.right));
Match old = state.visited.get(key);
if (old == null) {
if (solution == null || solution.cost > match.cost + current.cost) {
state.visited.put(key, match);
state.unvisited.add(match);
}
} else if (old.direction != match.direction) {
Solution<T> newSolution = new Solution<T>(state, match, old);
// System.out.println();
// System.out.println();
// System.out.println("Solution:");
// System.out.println(format(left, right, newSolution.toList()));
if (solution == null || newSolution.isBetterThan(solution)) {
solution = newSolution;
if (!state.unvisited.isEmpty() && state.unvisited.last().cost > old.cost)
state.unvisited = new TreeSet<Match>(state.unvisited.headSet(old, true));
}
}
}
// System.out.println();
// System.out.println();
// System.out.println(state);
}
List<IDifferencer.Match> result = solution.toList();
return result;
}
}