/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.subtasks;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListIterator;
abstract public class OrderedMetadataListUpdater<LIST> {
public OrderedMetadataListUpdater() {
DependencyInjectionService.getInstance().inject(this);
}
public interface OrderedListIterator {
public void processTask(long taskId, Metadata metadata);
}
// --- abstract and empty
abstract protected Metadata getTaskMetadata(LIST list, long taskId);
abstract protected IntegerProperty indentProperty();
abstract protected LongProperty orderProperty();
abstract protected LongProperty parentProperty();
abstract protected void iterateThroughList(Filter filter, LIST list, OrderedListIterator iterator);
abstract protected Metadata createEmptyMetadata(LIST list, long taskId);
/**
* @param list
* @param filter
*/
protected void initialize(LIST list, Filter filter) {
//
}
/**
* @param list
*/
protected void beforeIndent(LIST list) {
//
}
/**
* @param metadata
*/
protected void onMovedOrIndented(Metadata metadata) {
//
}
/**
* @param list
* @param taskId
* @param metadata
* @param indent
* @param order
*/
protected void beforeSaveIndent(LIST list, long taskId, Metadata metadata, int indent, int order) {
//
}
// --- task indenting
/**
* Indent a task and all its children
*/
public void indent(final Filter filter, final LIST list, final long targetTaskId, final int delta) {
if(list == null)
return;
beforeIndent(list);
final AtomicInteger targetTaskIndent = new AtomicInteger(-1);
final AtomicInteger previousIndent = new AtomicInteger(-1);
final AtomicLong previousTask = new AtomicLong(Task.NO_ID);
final AtomicLong globalOrder = new AtomicLong(-1);
iterateThroughList(filter, list, new OrderedListIterator() {
@Override
public void processTask(long taskId, Metadata metadata) {
if(!metadata.isSaved())
metadata = createEmptyMetadata(list, taskId);
int indent = metadata.containsNonNullValue(indentProperty()) ?
metadata.getValue(indentProperty()) : 0;
long order = globalOrder.incrementAndGet();
metadata.setValue(orderProperty(), order);
if(targetTaskId == taskId) {
// if indenting is warranted, indent me and my children
if(indent + delta <= previousIndent.get() + 1 && indent + delta >= 0) {
targetTaskIndent.set(indent);
metadata.setValue(indentProperty(), indent + delta);
if(parentProperty() != null) {
long newParent = computeNewParent(filter, list,
taskId, indent + delta - 1);
if (newParent == taskId)
metadata.setValue(parentProperty(), Task.NO_ID);
else
metadata.setValue(parentProperty(), newParent);
}
saveAndUpdateModifiedDate(metadata);
}
} else if(targetTaskIndent.get() > -1) {
// found first task that is not beneath target
if(indent <= targetTaskIndent.get())
targetTaskIndent.set(-1);
else {
metadata.setValue(indentProperty(), indent + delta);
saveAndUpdateModifiedDate(metadata);
}
} else {
previousIndent.set(indent);
previousTask.set(taskId);
}
if(!metadata.isSaved())
saveAndUpdateModifiedDate(metadata);
}
});
onMovedOrIndented(getTaskMetadata(list, targetTaskId));
}
/**
* Helper function to iterate through a list and compute a new parent for the target task
* based on the target parent's indent
* @param list
* @param targetTaskId
* @param newIndent
* @return
*/
private long computeNewParent(Filter filter, LIST list, long targetTaskId, int targetParentIndent) {
final AtomicInteger desiredParentIndent = new AtomicInteger(targetParentIndent);
final AtomicLong targetTask = new AtomicLong(targetTaskId);
final AtomicLong lastPotentialParent = new AtomicLong(Task.NO_ID);
final AtomicBoolean computedParent = new AtomicBoolean(false);
iterateThroughList(filter, list, new OrderedListIterator() {
@Override
public void processTask(long taskId, Metadata metadata) {
if (targetTask.get() == taskId) {
computedParent.set(true);
}
int indent = metadata.getValue(indentProperty());
if (!computedParent.get() && indent == desiredParentIndent.get()) {
lastPotentialParent.set(taskId);
}
}
});
if (lastPotentialParent.get() == Task.NO_ID) return Task.NO_ID;
return lastPotentialParent.get();
}
// --- task moving
/**
* Move a task and all its children to the position right above
* taskIdToMoveto. Will change the indent level to match taskIdToMoveTo.
*
* @param newTaskId task we will move above. if -1, moves to end of list
*/
public void moveTo(Filter filter, LIST list, final long targetTaskId,
final long moveBeforeTaskId) {
if(list == null)
return;
Node root = buildTreeModel(filter, list);
Node target = findNode(root, targetTaskId);
if(target != null && target.parent != null) {
if(moveBeforeTaskId == -1) {
target.parent.children.remove(target);
root.children.add(target);
target.parent = root;
} else {
Node sibling = findNode(root, moveBeforeTaskId);
if(sibling != null && !ancestorOf(target, sibling)) {
int index = sibling.parent.children.indexOf(sibling);
if(target.parent == sibling.parent &&
target.parent.children.indexOf(target) < index)
index--;
target.parent.children.remove(target);
sibling.parent.children.add(index, target);
target.parent = sibling.parent;
}
}
}
traverseTreeAndWriteValues(list, root, new AtomicLong(0), -1);
onMovedOrIndented(getTaskMetadata(list, targetTaskId));
}
private boolean ancestorOf(Node ancestor, Node descendant) {
if(descendant.parent == ancestor)
return true;
if(descendant.parent == null)
return false;
return ancestorOf(ancestor, descendant.parent);
}
protected static class Node {
public final long taskId;
public Node parent;
public final ArrayList<Node> children = new ArrayList<Node>();
public Node(long taskId, Node parent) {
this.taskId = taskId;
this.parent = parent;
}
}
protected void traverseTreeAndWriteValues(LIST list, Node node, AtomicLong order, int indent) {
if(node.taskId != Task.NO_ID) {
Metadata metadata = getTaskMetadata(list, node.taskId);
if(metadata == null)
metadata = createEmptyMetadata(list, node.taskId);
metadata.setValue(orderProperty(), order.getAndIncrement());
metadata.setValue(indentProperty(), indent);
boolean parentChanged = false;
if(parentProperty() != null && metadata.getValue(parentProperty()) !=
node.parent.taskId) {
parentChanged = true;
metadata.setValue(parentProperty(), node.parent.taskId);
}
saveAndUpdateModifiedDate(metadata);
if(parentChanged)
onMovedOrIndented(metadata);
}
for(Node child : node.children) {
traverseTreeAndWriteValues(list, child, order, indent + 1);
}
}
protected Node findNode(Node node, long taskId) {
if(node.taskId == taskId)
return node;
for(Node child : node.children) {
Node found = findNode(child, taskId);
if(found != null)
return found;
}
return null;
}
protected Node buildTreeModel(Filter filter, LIST list) {
final Node root = new Node(Task.NO_ID, null);
final AtomicInteger previoustIndent = new AtomicInteger(-1);
final AtomicReference<Node> currentNode = new AtomicReference<Node>(root);
iterateThroughList(filter, list, new OrderedListIterator() {
@Override
public void processTask(long taskId, Metadata metadata) {
int indent = metadata.getValue(indentProperty());
int previousIndentValue = previoustIndent.get();
if(indent == previousIndentValue) { // sibling
Node parent = currentNode.get().parent;
currentNode.set(new Node(taskId, parent));
parent.children.add(currentNode.get());
} else if(indent > previousIndentValue) { // child
Node parent = currentNode.get();
currentNode.set(new Node(taskId, parent));
parent.children.add(currentNode.get());
} else { // in a different tree
Node node = currentNode.get().parent;
for(int i = indent; i < previousIndentValue; i++) {
node = node.parent;
if(node == null) {
node = root;
break;
}
}
currentNode.set(new Node(taskId, node));
node.children.add(currentNode.get());
}
previoustIndent.set(indent);
}
});
return root;
}
protected void saveAndUpdateModifiedDate(Metadata metadata) {
if(metadata.getSetValues().size() == 0)
return;
PluginServices.getMetadataService().save(metadata);
}
// --- task cascading operations
public interface OrderedListNodeVisitor {
public void visitNode(Node node);
}
/**
* Apply an operation only to the children of the task
*/
public void applyToChildren(Filter filter, LIST list, long targetTaskId,
OrderedListNodeVisitor visitor) {
Node root = buildTreeModel(filter, list);
Node target = findNode(root, targetTaskId);
if(target != null)
for(Node child : target.children)
applyVisitor(child, visitor);
}
private void applyVisitor(Node node, OrderedListNodeVisitor visitor) {
visitor.visitNode(node);
for(Node child : node.children)
applyVisitor(child, visitor);
}
/**
* Removes a task from the order hierarchy and un-indent children
* @param filter
* @param list
* @param targetTaskId
*/
public void onDeleteTask(Filter filter, LIST list, final long targetTaskId) {
if(list == null)
return;
Node root = buildTreeModel(filter, list);
Node target = findNode(root, targetTaskId);
if(target != null && target.parent != null) {
int targetIndex = target.parent.children.indexOf(target);
target.parent.children.remove(targetIndex);
for(Node node : target.children) {
node.parent = target.parent;
target.parent.children.add(targetIndex++, node);
}
}
traverseTreeAndWriteValues(list, root, new AtomicLong(0), -1);
}
// --- utility
public void debugPrint(Filter filter, LIST list) {
iterateThroughList(filter, list, new OrderedListIterator() {
public void processTask(long taskId, Metadata metadata) {
System.err.format("id %d: order %d, indent:%d, parent:%d\n", taskId, //$NON-NLS-1$
metadata.getValue(orderProperty()),
metadata.getValue(indentProperty()),
parentProperty() == null ? Task.NO_ID : metadata.getValue(parentProperty()));
}
});
}
@SuppressWarnings("nls")
public void debugPrint(Node root, int depth) {
for(int i = 0; i < depth; i++) System.err.print(" + ");
System.err.format("%03d", root.taskId);
System.err.print("\n");
for(int i = 0; i < root.children.size(); i++)
debugPrint(root.children.get(i), depth + 1);
}
}