/*******************************************************************************
 * Copyright (c) 2000, 2014 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.internal.dtree;

import org.eclipse.core.internal.utils.*;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;

/**
 * <code>DataTreeNode</code>s are the nodes of a <code>DataTree</code>.  Their
 * information and their subtrees are complete, and do not represent deltas on
 * another node or subtree.
 */
public class DataTreeNode extends AbstractDataTreeNode {
	protected Object data;

	/**
	 * Creates a new node
	 *
	 * @param name name of node
	 * @param data data for node
	 */
	public DataTreeNode(String name, Object data) {
		super(name, AbstractDataTreeNode.NO_CHILDREN);
		this.data = data;
	}

	/**
	 * Creates a new node
	 *
	 * @param name name of node
	 * @param data data for node
	 * @param children children for new node
	 */
	public DataTreeNode(String name, Object data, AbstractDataTreeNode[] children) {
		super(name, children);
		this.data = data;
	}

	/**
	 * @see AbstractDataTreeNode#asBackwardDelta(DeltaDataTree, DeltaDataTree, IPath)
	 */
	@Override
	AbstractDataTreeNode asBackwardDelta(DeltaDataTree myTree, DeltaDataTree parentTree, IPath key) {
		if (parentTree.includes(key))
			return parentTree.copyCompleteSubtree(key);
		return new DeletedNode(name);
	}

	/**
	 * If this node is a node in a comparison tree, this method reverses
	 * the comparison for this node and all children.  Returns null
	 * if this node should no longer be included in the comparison tree.
	 */
	@Override
	AbstractDataTreeNode asReverseComparisonNode(IComparator comparator) {
		NodeComparison comparison = null;
		try {
			comparison = ((NodeComparison) data).asReverseComparison(comparator);
		} catch (ClassCastException e) {
			Assert.isTrue(false, Messages.dtree_reverse);
		}

		int nextChild = 0;
		for (AbstractDataTreeNode c : children) {
			AbstractDataTreeNode child = c.asReverseComparisonNode(comparator);
			if (child != null) {
				children[nextChild++] = child;
			}
		}

		if (nextChild == 0 && comparison.getUserComparison() == 0) {
			/* no children and no change */
			return null;
		}

		/* set the new data */
		data = comparison;

		/* shrink child array as necessary */
		if (nextChild < children.length) {
			AbstractDataTreeNode[] newChildren = new AbstractDataTreeNode[nextChild];
			System.arraycopy(children, 0, newChildren, 0, nextChild);
			children = newChildren;
		}

		return this;
	}

	AbstractDataTreeNode compareWith(DataTreeNode other, IComparator comparator) {
		AbstractDataTreeNode[] comparedChildren = compareWith(children, other.children, comparator);
		Object oldData = data;
		Object newData = other.data;

		/* don't allow comparison of implicit root node */
		int userComparison = 0;
		if (name != null) {
			userComparison = comparator.compare(oldData, newData);
		}

		return new DataTreeNode(name, new NodeComparison(oldData, newData, NodeComparison.K_CHANGED, userComparison), comparedChildren);
	}

	@Override
	AbstractDataTreeNode compareWithParent(IPath key, DeltaDataTree parent, IComparator comparator) {
		if (!parent.includes(key))
			return convertToAddedComparisonNode(this, NodeComparison.K_ADDED);
		DataTreeNode inParent = (DataTreeNode) parent.copyCompleteSubtree(key);
		return inParent.compareWith(this, comparator);
	}

	/**
	 * Creates and returns a new copy of the receiver.
	 */
	@Override
	AbstractDataTreeNode copy() {
		if (children.length > 0) {
			AbstractDataTreeNode[] childrenCopy = new AbstractDataTreeNode[children.length];
			System.arraycopy(children, 0, childrenCopy, 0, children.length);
			return new DataTreeNode(name, data, childrenCopy);
		}
		return new DataTreeNode(name, data, children);
	}

	/**
	 * Returns a new node containing a child with the given local name in
	 * addition to the receiver's current children and data.
	 *
	 * @param localName
	 *	name of new child
	 * @param childNode
	 *	new child node
	 */
	DataTreeNode copyWithNewChild(String localName, DataTreeNode childNode) {

		AbstractDataTreeNode[] children = this.children;
		int left = 0;
		int right = children.length - 1;
		while (left <= right) {
			int mid = (left + right) / 2;
			int compare = localName.compareTo(children[mid].name);
			if (compare < 0) {
				right = mid - 1;
			} else if (compare > 0) {
				left = mid + 1;
			} else {
				throw new Error(); // it shouldn't have been here yet
			}
		}

		AbstractDataTreeNode[] newChildren = new AbstractDataTreeNode[children.length + 1];
		System.arraycopy(children, 0, newChildren, 0, left);
		childNode.setName(localName);
		newChildren[left] = childNode;
		System.arraycopy(children, left, newChildren, left + 1, children.length - left);
		return new DataTreeNode(this.getName(), this.getData(), newChildren);
	}

	/**
	 * Returns a new node without the specified child, but with the rest
	 * of the receiver's current children and its data.
	 *
	 * @param localName
	 *	name of child to exclude
	 */
	DataTreeNode copyWithoutChild(String localName) {

		int index, newSize;
		DataTreeNode newNode;
		AbstractDataTreeNode children[];

		index = this.indexOfChild(localName);
		if (index == -1) {
			newNode = (DataTreeNode) this.copy();
		} else {
			newSize = this.size() - 1;
			children = new AbstractDataTreeNode[newSize];
			newNode = new DataTreeNode(this.getName(), this.getData(), children);
			newNode.copyChildren(0, index - 1, this, 0); //#from:to:with:startingAt:
			newNode.copyChildren(index, newSize - 1, this, index + 1);
		}
		return newNode;
	}

	/**
	 * Returns an array of delta nodes representing the forward delta between
	 * the given two lists of nodes.
	 * The given nodes must all be complete nodes.
	 */
	protected static AbstractDataTreeNode[] forwardDeltaWith(AbstractDataTreeNode[] oldNodes, AbstractDataTreeNode[] newNodes, IComparator comparer) {
		if (oldNodes.length == 0 && newNodes.length == 0) {
			return NO_CHILDREN;
		}

		AbstractDataTreeNode[] childDeltas = null;
		int numChildDeltas = 0;
		int childDeltaMax = 0;

		// do a merge
		int oldIndex = 0;
		int newIndex = 0;
		while (oldIndex < oldNodes.length && newIndex < newNodes.length) {
			String oldName = oldNodes[oldIndex].name;
			String newName = newNodes[newIndex].name;
			int compare = oldName.compareTo(newName);
			if (compare == 0) {
				AbstractDataTreeNode deltaNode = forwardDeltaWithOrNullIfEqual(oldNodes[oldIndex++], newNodes[newIndex++], comparer);
				if (deltaNode != null) {
					if (numChildDeltas >= childDeltaMax) {
						if (childDeltas == null)
							childDeltas = new AbstractDataTreeNode[childDeltaMax = 5];
						else
							System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[childDeltaMax = childDeltaMax * 2 + 1], 0, numChildDeltas);
					}
					childDeltas[numChildDeltas++] = deltaNode;
				}
			} else if (compare < 0) {
				if (numChildDeltas >= childDeltaMax) {
					if (childDeltas == null)
						childDeltas = new AbstractDataTreeNode[childDeltaMax = 5];
					else
						System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[childDeltaMax = childDeltaMax * 2 + 1], 0, numChildDeltas);
				}
				childDeltas[numChildDeltas++] = new DeletedNode(oldName);
				oldIndex++;
			} else {
				if (numChildDeltas >= childDeltaMax) {
					if (childDeltas == null)
						childDeltas = new AbstractDataTreeNode[childDeltaMax = 5];
					else
						System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[childDeltaMax = childDeltaMax * 2 + 1], 0, numChildDeltas);
				}
				childDeltas[numChildDeltas++] = newNodes[newIndex++];
			}
		}
		while (oldIndex < oldNodes.length) {
			if (numChildDeltas >= childDeltaMax) {
				if (childDeltas == null)
					childDeltas = new AbstractDataTreeNode[childDeltaMax = 5];
				else
					System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[childDeltaMax = childDeltaMax * 2 + 1], 0, numChildDeltas);
			}
			childDeltas[numChildDeltas++] = new DeletedNode(oldNodes[oldIndex++].name);
		}
		while (newIndex < newNodes.length) {
			if (numChildDeltas >= childDeltaMax) {
				if (childDeltas == null)
					childDeltas = new AbstractDataTreeNode[childDeltaMax = 5];
				else
					System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[childDeltaMax = childDeltaMax * 2 + 1], 0, numChildDeltas);
			}
			childDeltas[numChildDeltas++] = newNodes[newIndex++];
		}

		// trim size of result
		if (numChildDeltas == 0) {
			return NO_CHILDREN;
		}
		if (numChildDeltas < childDeltaMax) {
			System.arraycopy(childDeltas, 0, childDeltas = new AbstractDataTreeNode[numChildDeltas], 0, numChildDeltas);
		}
		return childDeltas;
	}

	/**
	 * Returns a node representing the forward delta between
	 * the given two (complete) nodes.
	 */
	protected AbstractDataTreeNode forwardDeltaWith(DataTreeNode other, IComparator comparer) {
		AbstractDataTreeNode deltaNode = forwardDeltaWithOrNullIfEqual(this, other, comparer);
		if (deltaNode == null) {
			return new NoDataDeltaNode(name, NO_CHILDREN);
		}
		return deltaNode;
	}

	/**
	 * Returns a node representing the forward delta between
	 * the given two (complete) nodes, or null if the two nodes are equal.
	 * Although typed as abstract nodes, the given nodes must be complete.
	 */
	protected static AbstractDataTreeNode forwardDeltaWithOrNullIfEqual(AbstractDataTreeNode oldNode, AbstractDataTreeNode newNode, IComparator comparer) {
		AbstractDataTreeNode[] childDeltas = forwardDeltaWith(oldNode.children, newNode.children, comparer);
		Object newData = newNode.getData();
		if (comparer.compare(oldNode.getData(), newData) == 0) {
			if (childDeltas.length == 0) {
				return null;
			}
			return new NoDataDeltaNode(newNode.name, childDeltas);
		}
		return new DataDeltaNode(newNode.name, newData, childDeltas);
	}

	/**
	 * Returns the data for the node
	 */
	@Override
	public Object getData() {
		return data;
	}

	/**
	 * Returns true if the receiver can carry data, false otherwise.
	 */
	@Override
	boolean hasData() {
		return true;
	}

	/**
	 * Sets the data for the node
	 */
	void setData(Object o) {
		data = o;
	}

	/**
	 * Simplifies the given node, and answers its replacement.
	 */
	@Override
	AbstractDataTreeNode simplifyWithParent(IPath key, DeltaDataTree parent, IComparator comparer) {
		/* If not in parent, can't be simplified */
		if (!parent.includes(key)) {
			return this;
		}
		/* Can't just call simplify on children since this will miss the case
		 where a child exists in the parent but does not in this.
		 See PR 1FH5RYA. */
		DataTreeNode parentsNode = (DataTreeNode) parent.copyCompleteSubtree(key);
		return parentsNode.forwardDeltaWith(this, comparer);
	}

	@Override
	public void storeStrings(StringPool set) {
		super.storeStrings(set);
		//copy data for thread safety
		Object o = data;
		if (o instanceof IStringPoolParticipant)
			((IStringPoolParticipant) o).shareStrings(set);
	}

	/**
	 * Returns a unicode representation of the node.  This method is used
	 * for debugging purposes only (no NLS support needed)
	 */
	@Override
	public String toString() {
		return "a DataTreeNode(" + this.getName() + ") with " + getChildren().length + " children."; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	/**
	 * Returns a constant describing the type of node.
	 */
	@Override
	int type() {
		return T_COMPLETE_NODE;
	}
}
