package com.almworks.jira.structure.api.forest;
import com.almworks.integers.*;
import com.almworks.integers.util.LongSetBuilder;
import com.almworks.jira.structure.api.StructureError;
import com.almworks.jira.structure.api.StructureException;
import com.almworks.jira.structure.util.La;
import com.almworks.jira.structure.util.StructureUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.almworks.jira.structure.api.StructureError.INVALID_MOVE;
import static com.almworks.jira.structure.api.StructureError.ISSUE_MISSING_FROM_STRUCTURE;
/**
* @author Igor Sereda
*/
public class Forest implements Cloneable {
private static final Logger logger = LoggerFactory.getLogger(Forest.class);
private WritableLongList myIssues;
private WritableIntList myDepths;
private boolean myImmutable;
public Forest() {
this(new LongArray(), new IntArray(), true);
}
/**
* Creates a singular tree. Singular tree cannot be stored (the issue is a standalone issue), but can be used between
* operations on other trees.
*
* @param issue issue ID of the root
*/
public Forest(long issue) {
this(LongArray.create(issue), IntArray.create(0), true);
}
/**
* Constructs a forest based on the given issues and depths.
* <p/>
* Issue list and depth list must conform to the IssueTree invariants. If invariants are violated, then either an
* error is thrown (if assertions are on), or further behavior is undefined.
*
* @param issues list of issue IDs
* @param depths list of corresponding depths
* @param reuseLists if true, passed instances of List can be used by this IssueTree (otherwise a copy is made)
*/
public Forest(WritableLongList issues, WritableIntList depths, boolean reuseLists) {
myIssues = reuseLists ? issues : new LongArray(issues);
myDepths = reuseLists ? depths : new IntArray(depths);
assert checkInvariants();
}
public Forest(LongList issues, IntList depths) {
this(new LongArray(issues), new IntArray(depths), true);
}
/**
* Checks whether IssueTree invariants hold. Throws AssertionError if not, with a diagnostic message.
*
* @return true
*/
boolean checkInvariants() {
String problem = getDiagnosics();
assert problem == null : problem;
return true;
}
/**
* Checks whether IssueTree invariants hold.
*
* @return null if all invariants are true, otherwise return a message with description of the problem
*/
public String getDiagnosics() {
int size = myIssues.size();
if (size != myDepths.size()) return "array size mismatch";
if (size == 0) return null;
if (myDepths.get(0) != 0) return "root not at 0 depth";
Map<Long, Integer> issues = new HashMap<Long, Integer>();
int depth = -1;
for (int i = 0; i < size; i++) {
long issue = myIssues.get(i);
if (issue <= 0) {
return "bad issue @" + i + " " + issue + " " + this;
}
Integer prev = issues.put(issue, i);
if (prev != null) {
return "duplicate issue @" + i + " " + issue + " " + this;
}
int d = myDepths.get(i);
if (d < 0) {
return "bad depth @" + i + " " + d + " " + issue + " " + this;
}
if (d > depth + 1) {
return "bad depth change @" + i + " " + depth + " " + d + " " + issue + " " + this;
}
depth = d;
}
return null;
}
/**
* Checks if an issue is in the tree.
*
* @param issueId issue ID
* @return true if this tree contains the issue
*/
public boolean containsIssue(long issueId) {
return myIssues.contains(issueId);
}
public void mergeForest(Forest forest, long under, long after) throws StructureException {
mergeForest(forest, under, after, null);
}
/**
* Invariant: after calling this method, either all issues from forest are in this forest, or exception is thrown.
* <p/>
* Merge is slow - more efficient algorithm can be employed if needed
*
* @param forest the source forest to merge into this forest
* @param under under position
* @param after after position
* @param previousParents if not null, then issue ids that used to be direct parents of the moved issues are added there
* @throws StructureException if operation is not possible
*/
public void mergeForest(Forest forest, long under, long after, LongCollector previousParents)
throws StructureException
{
if (forest == null || forest.isEmpty()) return;
assert checkInvariants();
assert forest.checkInvariants();
checkModification();
if (isMutuallyExclusiveWith(forest)) {
addForestMutuallyExclusive(forest, under, after);
return;
}
if (under > 0) {
int parentIndex = myIssues.indexOf(under);
for (int p = parentIndex; p >= 0; p = getParentIndex(p)) {
long pathIssue = myIssues.get(p);
if (forest.containsIssue(pathIssue)) {
throw new StructureException(INVALID_MOVE,
"cannot merge " + forest + " in " + this + " under " + under + ": issue "
+ pathIssue + " on parent path is in the merged forest");
}
}
}
int size = forest.size();
int i = 0;
while (i < size) {
long nextAfter = forest.getIssues().get(i);
i = mergeSubforest(forest, i, under, after, previousParents);
after = nextAfter;
}
assert checkInvariants();
}
private boolean isMutuallyExclusiveWith(Forest forest) {
if (this.size() == 0 || forest.size() == 0) return true;
boolean sw = forest.size() < this.size();
LongList sortIssues = sw ? forest.getIssues() : this.getIssues();
LongList checkIssues = sw ? this.getIssues() : forest.getIssues();
LongSetBuilder builder = new LongSetBuilder();
builder.addAll(sortIssues);
LongList sorted = builder.toSortedCollection();
int size = checkIssues.size();
for (int i = 0; i < size; i++) {
if (sorted.binarySearch(checkIssues.get(i)) >= 0) return false;
}
return true;
}
private int mergeSubforest(Forest forest, int index, long under, long after,
LongCollector previousParents) throws StructureException
{
LongList issues = forest.getIssues();
long issue = issues.get(index);
int thisIndex = this.indexOf(issue); // don't confuse thisIndex with index
if (thisIndex >= 0) {
if (previousParents != null) {
int parentIndex = getParentIndex(thisIndex);
if (parentIndex >= 0) previousParents.add(myIssues.get(parentIndex));
}
moveSubtreeAtIndex(thisIndex, under, after, issue);
} else {
addForestMutuallyExclusive(new Forest(issue), under, after);
}
IntList depths = forest.getDepths();
int depth = depths.get(index);
int size = forest.size();
index++;
long subAfter = 0;
while (index < size && depths.get(index) > depth) {
long nextSubAfter = issues.get(index);
index = mergeSubforest(forest, index, issue, subAfter, previousParents);
subAfter = nextSubAfter;
}
return index;
}
private void addForestMutuallyExclusive(Forest forest, long under, long after) throws StructureException {
if (forest == null || forest.isEmpty()) return;
assert checkInvariants();
assert forest.checkInvariants();
checkModification();
int parentIndex = getUnderIndex(under);
int parentDepth = parentIndex < 0 ? -1 : myDepths.get(parentIndex);
int insertAtIndex = parentIndex + 1;
int insertAtDepth = parentDepth + 1;
if (after > 0) {
int siblingIndex = myIssues.indexOf(after);
if (siblingIndex < 0) {
logger.warn(this + ": ignoring invalid after: issue " + after);
} else {
siblingIndex = getPathIndexAtDepth(siblingIndex, insertAtDepth);
if (siblingIndex < 0) {
logger.warn(this + ": ignoring invalid after: issue " + after + " not at the right level");
} else if (parentIndex != getParentIndex(siblingIndex)) {
logger.warn(this + ": ignoring invalid after: issue " + after + " not under parent " + under);
} else {
insertAtIndex = getSubtreeEnd(siblingIndex);
}
}
}
/*
// the following was attempt to add "after = -1" to mean as "append to the end"
// the solution is deemed unreliable (could be additional source of conflicts)
else if (after < 0) {
insertAtIndex = insertAtDepth == 0 ? myIssues.size() : findSubtreeEnd(parentIndex);
}
*/
LongList issues = forest.myIssues;
IntList depths = forest.myDepths;
myIssues.insertAll(insertAtIndex, issues);
myDepths.insertAll(insertAtIndex, depths);
if (insertAtDepth != 0) {
for (int i = insertAtIndex + issues.size() - 1; i >= insertAtIndex; i--) {
myDepths.set(i, myDepths.get(i) + insertAtDepth);
}
}
assert checkInvariants();
}
private int getUnderIndex(long under) throws StructureException {
int parentIndex;
if (under <= 0) {
parentIndex = -1;
} else {
parentIndex = myIssues.indexOf(under);
if (parentIndex < 0) {
throw new StructureException(ISSUE_MISSING_FROM_STRUCTURE, null, under,
"cannot find under " + under + " in " + this);
}
}
return parentIndex;
}
/**
* Removes a subtree from this forest.
* <p/>
* This method will create a new <tt>IssueTree</tt> with <tt>issue</tt> at its root and all subissues, properly
* outdented.
*
* @param issue issue ID of the root of the subtree to be removed
* @return null if this tree does not contain <tt>issue</tt>, otherwise an IssueTree with the <tt>issue</tt> as the
* root
*/
public Forest removeSubtree(long issue) {
assert checkInvariants();
int issueIndex = myIssues.indexOf(issue);
if (issueIndex < 0) return null;
return removeSubtreeAtIndex(issueIndex);
}
private Forest removeSubtreeAtIndex(int index) {
assert checkInvariants();
checkModification();
int lastIndex = getSubtreeEnd(index);
Forest r = copySubtree0(index, lastIndex);
myIssues.removeRange(index, lastIndex);
myDepths.removeRange(index, lastIndex);
assert checkInvariants();
return r;
}
private Forest copySubtree0(int from, int to) {
LongArray newIssues = new LongArray(myIssues.subList(from, to));
IntArray newDepths = new IntArray(myDepths.subList(from, to));
// Decrement depths. its separate tree so depths should start from 0
int issueDepth = newDepths.get(0);
if (issueDepth > 0) {
for (int i = 0; i < newDepths.size(); i++) {
int d = newDepths.get(i) - issueDepth;
if (d < 0 || d == 0 && i > 0) {
throw new IllegalStateException("bad depth " + d + " @ " + i);
}
newDepths.set(i, d);
}
}
return new Forest(newIssues, newDepths, true);
}
public Forest copySubtree(long rootIssue) {
assert checkInvariants();
int issueIndex = myIssues.indexOf(rootIssue);
if (issueIndex < 0) return null;
int lastIndex = getSubtreeEnd(issueIndex);
return copySubtree0(issueIndex, lastIndex);
}
public int getSubtreeEnd(int index) {
if (index < 0) return 0;
int size = myDepths.size();
if (index > size) return size;
int d = myDepths.get(index);
int r = index + 1;
for (; r < size; r++) {
if (myDepths.get(r) <= d) break;
}
return r;
}
/**
* Method for accessing issue list.
*
* @return unmodifiable list of issue IDs
*/
public LongList getIssues() {
return myIssues;
}
/**
* Method for accessing depth list.
*
* @return unmodifiable list of issue depths
*/
public IntList getDepths() {
return myDepths;
}
public long getIssue(int index) {
return myIssues.get(index);
}
public int getDepth(int index) {
return myDepths.get(index);
}
/**
* Gets parent issue.
*
* @param issueId issue ID
* @return null if issue is not in tree or if it's the tree root, otherwise issue ID of the parent issue
*/
public Long getParent(Long issueId) {
int index = myIssues.indexOf(issueId);
if (index < 0) return null;
int pi = getParentIndex(index);
return pi < 0 ? null : myIssues.get(pi);
}
/**
* Gets parent issue index.
*
* @param index position of the issue
* @return -1 if issue is the tree root, otherwise position of the parent issue
*/
public int getParentIndex(int index) {
int depth = myDepths.get(index);
if (depth == 0) return -1;
for (int i = index - 1; i >= 0; i--) {
if (myDepths.get(i) == depth - 1) return i;
}
assert false : index + " " + this;
return -1;
}
public int getPathIndexAtDepth(int index, int depth) {
if (depth < 0) return -1;
if (myDepths.get(index) < depth) return -1;
for (int i = index; i >= 0; i--) {
if (myDepths.get(i) == depth) return i;
}
assert false : index + " " + this;
return -1;
}
/**
* Gets previous sibling.
*
* @param issue issue ID
* @return null if issue is not in the tree or if there's no previous sibling, otherwise issue ID of the issue that is
* under the same parent right before <tt>issue</tt>
*/
public Long getPreviousSibling(long issue) {
int index = myIssues.indexOf(issue);
if (index < 0) return null;
int depth = myDepths.get(index);
for (int i = index - 1; i >= 0; i--) {
int d = myDepths.get(i);
if (d < depth) return null;
if (d == depth) return myIssues.get(i);
}
return null;
}
/**
* Gets children of an issue.
*
* @param issue issue ID
* @return a list of direct children of that issue, or null if the issue is not in the tree
*/
public LongArray getChildren(long issue) {
return getChildrenAtIndex(myIssues.indexOf(issue));
}
public LongArray getChildrenAtIndex(int index) {
if (index < 0) return null;
LongArray children = new LongArray();
int depth = myDepths.get(index);
for (int i = index + 1; i < myDepths.size(); i++) {
int d = myDepths.get(i);
if (d <= depth) break;
if (d == depth + 1) children.add(myIssues.get(i));
}
return children;
}
public LongArray getRoots() {
LongArray r = new LongArray();
for (int i = 0, size = myDepths.size(); i < size; i++) {
if (myDepths.get(i) == 0) r.add(myIssues.get(i));
}
return r;
}
/**
* Filters this forest by hiding issues that do not pass the <tt>filter</tt> condition.
* <p/>
* As a result, a number of issue trees and standalone issues are produced, which represent the same hierarchy but
* with all issues satisfying the condition. Multiple issue trees may be produced by this single tree if the root
* issue does not pass the filter condition.
* <p/>
* This issue tree is not modified by this method.
*
* @param filter called once for every issue in this tree; if returns true, the issue is allowed
* @return a forest of issues in the order of their root appearance in this tree
*/
public Forest filter(La<Long, ?> filter) {
if (filter == null) return this;
int firstFiltered = 0;
int size = myIssues.size();
for (; firstFiltered < size; firstFiltered++) {
if (!filter.accepts(myIssues.get(firstFiltered))) break;
}
if (firstFiltered == size) return this;
LongArray issues = new LongArray(size);
IntArray depths = new IntArray(size);
int i = 0;
while (i < size) {
i = buildFilteredSubtree(issues, depths, i, 0, firstFiltered, filter);
}
return new Forest(issues, depths, true);
}
private int buildFilteredSubtree(WritableLongList issues, WritableIntList depths, int index, int targetDepth,
int firstFiltered, La<Long, ?> filter)
{
int size = myIssues.size();
if (index >= size) return index;
int rootDepth = myDepths.get(index);
int delta = targetDepth - rootDepth;
int i = index;
while (i < size) {
long issue = myIssues.get(i);
int depth = myDepths.get(i);
if (depth < rootDepth || (depth == rootDepth && i > index)) break;
boolean accepted = i < firstFiltered || (i > firstFiltered && filter.accepts(issue));
i++;
if (accepted) {
issues.add(issue);
depths.add(depth + delta);
} else {
while (i < size) {
long nextDepth = myDepths.get(i);
if (nextDepth <= depth) break;
assert nextDepth == depth + 1 : i + " " + depth + " " + nextDepth;
i = buildFilteredSubtree(issues, depths, i, depth + delta, firstFiltered, filter);
}
}
}
return i;
}
public Forest filterSoft(La<Long, ?> filter) {
if (filter == null) return this;
int size = myIssues.size();
int lastFiltered = size - 1;
for (; lastFiltered >= 0; lastFiltered--) {
if (!filter.accepts(myIssues.get(lastFiltered))) break;
}
if (lastFiltered < 0) return this;
LongArray revIssues = new LongArray();
IntArray revDepth = new IntArray();
int lastDepth = 0;
if (lastFiltered < size - 1) {
revIssues.addAll(myIssues.subList(lastFiltered + 1, size));
revDepth.addAll(myDepths.subList(lastFiltered + 1, size));
revIssues = StructureUtil.reverse(revIssues);
revDepth = StructureUtil.reverse(revDepth);
lastDepth = revDepth.get(revDepth.size() - 1);
}
for (int i = lastFiltered; i >= 0; i--) {
int depth = myDepths.get(i);
long issue = myIssues.get(i);
boolean passes = depth < lastDepth || (i < lastFiltered && filter.accepts(issue));
if (passes) {
revIssues.add(issue);
revDepth.add(depth);
lastDepth = depth;
}
}
assert lastDepth == 0;
revIssues = StructureUtil.reverse(revIssues);
revDepth = StructureUtil.reverse(revDepth);
return new Forest(revIssues, revDepth, true);
}
/**
* Makes this instance non-modifiable
*
* @return issue tree with the same data (backed by different collections), which cannot be modified
*/
public Forest makeImmutable() {
myImmutable = true;
return this;
}
private void checkModification() {
if (myImmutable) throw new UnsupportedOperationException();
}
/**
* Gets the number of issues in this tree.
*
* @return the number of issues, also the size of lists returned by {@link #getIssues()} and {@link #getDepths()}
*/
public int size() {
return myIssues.size();
}
public Forest copy() {
return new Forest(myIssues, myDepths);
}
public Forest copyFilteredForRoot(long rootIssue) {
assert checkInvariants();
int from = myIssues.indexOf(rootIssue);
if (from < 0) return new Forest();
int to = getSubtreeEnd(from);
LongArray newIssues = new LongArray(myIssues.subList(from, to));
IntArray newDepths = new IntArray(myDepths.subList(from, to));
// also insert all parents
for (int i = getParentIndex(from); i >= 0; i = getParentIndex(i)) {
newIssues.insert(0, myIssues.get(i));
newDepths.insert(0, myDepths.get(i));
}
return new Forest(newIssues, newDepths, true);
}
public boolean moveSubtree(long issue, long under, long after) throws StructureException {
assert checkInvariants();
int idx = indexOf(issue);
if (idx < 0) return false;
moveSubtreeAtIndex(idx, under, after, issue);
return true;
}
private void moveSubtreeAtIndex(int index, long under, long after, long issue) throws StructureException {
assert checkInvariants();
checkModification();
int parentIndex = getUnderIndex(under);
if (parentIndex >= 0) {
int underParentIndex = getPathIndexAtDepth(parentIndex, myDepths.get(index));
if (underParentIndex == index) {
throw new StructureException(StructureError.INVALID_MOVE, null, under,
this + ": cannot move " + issue + " (index " + index + ") under " + under);
}
}
Forest forest = removeSubtreeAtIndex(index);
assert forest != null;
addForestMutuallyExclusive(forest, under, after);
assert checkInvariants();
}
public int indexOf(long issue) {
return myIssues.indexOf(issue);
}
public boolean isEmpty() {
return myIssues.isEmpty();
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Forest forest = (Forest) o;
if (!myDepths.equals(forest.myDepths)) return false;
if (!myIssues.equals(forest.myIssues)) return false;
return true;
}
public int hashCode() {
int result = myIssues.hashCode();
result = 31 * result + myDepths.hashCode();
return result;
}
public String toString() {
return toStringLimited(20);
}
public String toFullString() {
return toStringLimited(Integer.MAX_VALUE);
}
private String toStringLimited(int maxElements) {
StringBuilder r = new StringBuilder("forest(");
String prefix = "";
if (myImmutable) {
r.append("ro");
prefix = ",";
}
int size = myIssues.size();
int len = Math.min(maxElements, size);
for (int i = 0; i < len; i++) {
r.append(prefix).append(myIssues.get(i)).append(':').append(myDepths.get(i));
prefix = ",";
}
if (len < size) r.append(" ... [").append(size).append(']');
r.append(')');
return r.toString();
}
public Forest clone() {
try {
Forest copy = (Forest) super.clone();
copy.myIssues = new LongArray(copy.myIssues);
copy.myDepths = new IntArray(copy.myDepths);
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
public long getLastChild(long parent) {
int index = parent == 0 ? -1 : indexOf(parent);
return getLastChildByIndex(index);
}
public long getLastChildByIndex(int parentIndex) {
int size = myDepths.size();
if (parentIndex >= size) return 0;
int depth = parentIndex < 0 ? 0 : myDepths.get(parentIndex) + 1;
int index = -1;
for (int i = parentIndex < 0 ? 0 : parentIndex + 1; i < size; i++) {
int d = myDepths.get(i);
if (d < depth) break;
if (d == depth) {
index = i;
}
}
return index < 0 ? 0 : myIssues.get(index);
}
/**
* <p>Goes over the forest in the backwards direction and reports to the visitor pairs of (parent, direct children).</p>
* <p>Invariants:</p>
* <ul>
* <li>The number of calls to the visitor is equal to the forest size: every issue is reported as a parent once.</li>
* <li>A child issue is reported (as the parent of its sub-issues) before parent is reported: the iteration goes upwards.</li>
* <li>A leaf issue is reported as a parent with no children.</li>
* </ul>
* <p>Note: it is possible to also report all top-level issues with slight modifications of the method and visitor contract.</p>
* <p>Note: if the forest is modified during iteration, the results are undefined.</p>
*
* @param visitor the visitor to receive pairs of (parent, children)
*/
public void visitParentChildrenUpwards(ForestParentChildrenVisitor visitor) {
int index = size();
if (index == 0) return;
// arrays for reuse
List<LongArray> containerStack = new ArrayList<LongArray>();
while (index > 0) {
index = visitUpwards0(index, 0, visitor, containerStack);
}
}
/**
* Processes a subtree that ends at the specified index, and starts somewhere above it at the given depth.
* Recursively processes nested subtrees first.
*
* @param endIndex equals to {@link #getSubtreeEnd} of the subtree processed.
* @param targetDepth the required depth of the root
* @param visitor visitor
* @param containerStack reusable arrays
* @return the index of the root of the processed subtree, or -1 to abort iteration
*/
private int visitUpwards0(int endIndex, int targetDepth, ForestParentChildrenVisitor visitor,
List<LongArray> containerStack)
{
if (endIndex <= 0) return endIndex;
int depth = myDepths.get(endIndex - 1);
assert depth >= targetDepth : depth + " " + targetDepth;
// does previous row contain a leaf of the required depth?
if (depth == targetDepth) {
boolean proceed = visitor.visit(this, myIssues.get(endIndex - 1), LongList.EMPTY);
return proceed ? endIndex - 1 : -1;
}
// not a leaf - there will be children, allocate array
while (containerStack.size() <= targetDepth) containerStack.add(new LongArray());
LongArray children = containerStack.get(targetDepth);
children.clear();
// sub-iteration - gather nested sub-trees
int index = endIndex;
while (index > 0 && myDepths.get(index - 1) > targetDepth) {
index = visitUpwards0(index, targetDepth + 1, visitor, containerStack);
if (index < 0) return index;
children.add(myIssues.get(index));
}
// we should exit by hitting the parent at the specified targetDepth
assert index > 0 && myDepths.get(index - 1) == targetDepth : index + " " + this;
index--;
long parent = myIssues.get(index);
children = StructureUtil.reverse(children);
boolean proceed = visitor.visit(this, parent, children);
return proceed ? index : -1;
}
public LongArray getPath(long issue) {
LongArray r = new LongArray();
for (int i = indexOf(issue); i >= 0; i = getParentIndex(i)) {
r.insert(0, myIssues.get(i));
}
return r;
}
}