package com.todoroo.astrid.subtasks import com.todoroo.astrid.api.Filter import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task.Companion.isValidUuid import org.json.JSONArray import org.json.JSONException import org.tasks.Strings.isNullOrEmpty import org.tasks.data.TaskListMetadata import org.tasks.data.TaskListMetadataDao import org.tasks.db.QueryUtils.showHiddenAndCompleted import timber.log.Timber import java.util.* import javax.inject.Inject class SubtasksFilterUpdater @Inject constructor( private val taskListMetadataDao: TaskListMetadataDao, private val taskDao: TaskDao) { private val idToNode = HashMap() private var treeRoot: Node? = null private fun getSerializedTree(list: TaskListMetadata?): String? { if (list == null) { return "[]" // $NON-NLS-1$ } var order = list.taskIds if (isNullOrEmpty(order) || "null" == order) // $NON-NLS-1$ { order = "[]" // $NON-NLS-1$ } return order } suspend fun writeSerialization(list: TaskListMetadata?, serialized: String?) { if (list != null) { list.taskIds = serialized taskListMetadataDao.update(list) } } suspend fun initialize(list: TaskListMetadata?, filter: Filter) { initializeFromSerializedTree(list, filter, getSerializedTree(list)) applyToFilter(filter) } private fun applyToFilter(filter: Filter) { var query = filter.getSqlQuery() query = query.replace("ORDER BY .*".toRegex(), "") query += "ORDER BY $orderString" query = showHiddenAndCompleted(query) filter.setFilterQueryOverride(query) } fun getIndentForTask(targetTaskId: String?): Int { val n = idToNode[targetTaskId] ?: return 0 return n.indent } suspend fun initializeFromSerializedTree(list: TaskListMetadata?, filter: Filter, serializedTree: String?) { idToNode.clear() treeRoot = buildTreeModel(serializedTree) { node -> node?.let { idToNode[it.uuid] = it } } verifyTreeModel(list, filter) } private suspend fun verifyTreeModel(list: TaskListMetadata?, filter: Filter) { var changedThings = false val keySet: Set = idToNode.keys val currentIds: MutableSet = HashSet(keySet) val idsInQuery: MutableSet = HashSet() var sql = filter.getSqlQuery().replace("ORDER BY .*".toRegex(), "") // $NON-NLS-1$//$NON-NLS-2$ sql = "$sql ORDER BY created" // $NON-NLS-1$ sql = showHiddenAndCompleted(sql) val tasks = taskDao.fetchFiltered(sql) for (task in tasks) { val id = task.uuid idsInQuery.add(id) if (idToNode.containsKey(id)) { continue } changedThings = true val newNode = Node(id, treeRoot, 0) treeRoot!!.children.add(0, newNode) idToNode[id] = newNode } currentIds.removeAll(idsInQuery) if (currentIds.size > 0) { removeNodes(currentIds) changedThings = true } if (changedThings) { writeSerialization(list, serializeTree()) } } private fun removeNodes(idsToRemove: Set) { for (id in idsToRemove) { val node = idToNode[id] ?: continue // Remove node from tree, put all children under parent val parent = node.parent parent!!.children.remove(node) for (child in node.children) { child.parent = parent parent.children.add(child) setNodeIndent(child, parent.indent + 1) } } } fun findNodeForTask(taskId: String?): Node? { return idToNode[taskId] } private val orderedIds: List get() { val ids = ArrayList() orderedIdHelper(treeRoot, ids) return ids } private val orderString: String get() { val ids = orderedIds return buildOrderString(ids) } private fun orderedIdHelper(node: Node?, ids: MutableList) { if (node !== treeRoot) { ids.add(node!!.uuid) } for (child in node!!.children) { orderedIdHelper(child, ids) } } suspend fun applyToDescendants(taskId: String?, visitor: suspend (Node) -> Unit) { val n = idToNode[taskId] ?: return applyToDescendantsHelper(n, visitor) } private suspend fun applyToDescendantsHelper(n: Node, visitor: suspend (Node) -> Unit) { val children = n.children for (child in children) { visitor(child) applyToDescendantsHelper(child, visitor) } } suspend fun indent(list: TaskListMetadata, filter: Filter, targetTaskId: String?, delta: Int) { val node = idToNode[targetTaskId] indentHelper(list, filter, node, delta) } private suspend fun indentHelper(list: TaskListMetadata, filter: Filter, node: Node?, delta: Int) { if (node == null) { return } if (delta == 0) { return } val parent = node.parent ?: return if (delta > 0) { val siblings = parent.children val index = siblings.indexOf(node) if (index <= 0) // Can't indent first child { return } val newParent = siblings[index - 1] siblings.removeAt(index) node.parent = newParent newParent.children.add(node) setNodeIndent(node, newParent.indent + 1) } else if (delta < 0) { if (parent === treeRoot) // Can't deindent a top level item { return } val siblings = parent.children val index = siblings.indexOf(node) if (index < 0) { return } val newParent = parent.parent val newSiblings = newParent!!.children val insertAfter = newSiblings.indexOf(parent) siblings.removeAt(index) node.parent = newParent setNodeIndent(node, newParent.indent + 1) newSiblings.add(insertAfter + 1, node) } writeSerialization(list, serializeTree()) applyToFilter(filter) } private fun setNodeIndent(node: Node, indent: Int) { node.indent = indent adjustDescendantsIndent(node, indent) } private fun adjustDescendantsIndent(node: Node, baseIndent: Int) { for (child in node.children) { child.indent = baseIndent + 1 adjustDescendantsIndent(child, child.indent) } } suspend fun moveTo(list: TaskListMetadata, filter: Filter, targetTaskId: String?, beforeTaskId: String) { val target = idToNode[targetTaskId] ?: return if ("-1" == beforeTaskId) { // $NON-NLS-1$ moveToEndOfList(list, filter, target) return } val before = idToNode[beforeTaskId] ?: return if (isDescendantOf(before, target)) { return } moveHelper(list, filter, target, before) } fun moveToParentOf(moveThis: String?, toParentOfThis: String?) { val target = idToNode[toParentOfThis] ?: return val toMove = idToNode[moveThis] ?: return val newParent = target.parent val oldParent = toMove.parent oldParent!!.children.remove(toMove) toMove.parent = newParent newParent!!.children.add(toMove) setNodeIndent(toMove, toMove.parent!!.indent + 1) } private suspend fun moveHelper(list: TaskListMetadata, filter: Filter, moveThis: Node, beforeThis: Node) { val oldParent = moveThis.parent val oldSiblings = oldParent!!.children val newParent = beforeThis.parent val newSiblings = newParent!!.children var beforeIndex = newSiblings.indexOf(beforeThis) if (beforeIndex < 0) { return } val nodeIndex = oldSiblings.indexOf(moveThis) if (nodeIndex < 0) { return } moveThis.parent = newParent setNodeIndent(moveThis, newParent.indent + 1) oldSiblings.remove(moveThis) if (newSiblings === oldSiblings && beforeIndex > nodeIndex) { beforeIndex-- } newSiblings.add(beforeIndex, moveThis) writeSerialization(list, serializeTree()) applyToFilter(filter) } fun isDescendantOf(desc: String?, parent: String?): Boolean { return isDescendantOf(idToNode[desc], idToNode[parent]) } // Returns true if desc is a descendant of parent private fun isDescendantOf(desc: Node?, parent: Node?): Boolean { var curr = desc while (curr !== treeRoot) { if (curr === parent) { return true } curr = curr!!.parent } return false } private suspend fun moveToEndOfList(list: TaskListMetadata, filter: Filter, moveThis: Node) { val parent = moveThis.parent parent!!.children.remove(moveThis) treeRoot!!.children.add(moveThis) moveThis.parent = treeRoot setNodeIndent(moveThis, 0) writeSerialization(list, serializeTree()) applyToFilter(filter) } suspend fun onCreateTask(list: TaskListMetadata?, filter: Filter, uuid: String) { if (idToNode.containsKey(uuid) || !isValidUuid(uuid)) { return } val newNode = Node(uuid, treeRoot, 0) treeRoot!!.children.add(0, newNode) idToNode[uuid] = newNode writeSerialization(list, serializeTree()) applyToFilter(filter) } suspend fun onDeleteTask(list: TaskListMetadata?, filter: Filter, taskId: String?) { val task = idToNode[taskId] ?: return val parent = task.parent val siblings = parent!!.children var index = siblings.indexOf(task) if (index >= 0) { siblings.removeAt(index) } for (child in task.children) { child.parent = parent siblings.add(index, child) setNodeIndent(child, parent.indent + 1) index++ } idToNode.remove(taskId) writeSerialization(list, serializeTree()) applyToFilter(filter) } fun serializeTree(): String { return serializeTree(treeRoot) } class Node internal constructor(var uuid: String, var parent: Node?, var indent: Int) { val children = ArrayList() } companion object { const val ACTIVE_TASKS_ORDER = "active_tasks_order" // $NON-NLS-1$ const val TODAY_TASKS_ORDER = "today_tasks_order" // $NON-NLS-1$ private const val MAX_ORDERED_TASKS = 900 fun buildOrderString(ids: List): String { val builder = StringBuilder() if (ids.isEmpty()) { return "(1)" // $NON-NLS-1$ } val indices = ids.indices.reversed().take(MAX_ORDERED_TASKS) indices.forEach { i -> builder.append(Task.UUID.eq(ids[i]).toString()) if (i != indices.last()) { builder.append(", ") // $NON-NLS-1$ } } return builder.toString() } fun buildTreeModel(serializedTree: String?, callback: ((Node?) -> Unit)?): Node { val root = Node("-1", null, -1) // $NON-NLS-1$ try { val tree = JSONArray(serializedTree) recursivelyBuildChildren(root, tree, callback) } catch (e: JSONException) { Timber.e(e) } return root } @Throws(JSONException::class) private fun recursivelyBuildChildren( node: Node, children: JSONArray, callback: ((Node?) -> Unit)?) { for (i in 1 until children.length()) { val subarray = children.optJSONArray(i) val uuid: String = if (subarray == null) { children.getString(i) } else { subarray.getString(0) } val child = Node(uuid, node, node.indent + 1) subarray?.let { recursivelyBuildChildren(child, it, callback) } node.children.add(child) callback?.invoke(child) } } fun serializeTree(root: Node?): String { val tree = JSONArray() if (root == null) { return tree.toString() } recursivelySerialize(root, tree) return tree.toString() } private fun recursivelySerialize(node: Node, serializeTo: JSONArray) { val children = node.children serializeTo.put(node.uuid) for (child in children) { if (child.children.size > 0) { val branch = JSONArray() recursivelySerialize(child, branch) serializeTo.put(branch) } else { serializeTo.put(child.uuid) } } } } }