/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *     Mickael Istria (Red Hat Inc.) - Bug 488937
 *******************************************************************************/
package org.eclipse.core.internal.watson;

import java.io.*;
import java.util.*;
import org.eclipse.core.internal.dtree.*;
import org.eclipse.core.runtime.*;

/** <code>ElementTreeWriter</code> flattens an ElementTree
 * onto a data output stream.
 *
 * <p>This writer generates the most up-to-date format
 * of a saved element tree (cf. readers, which must usually also
 * deal with backward compatibility issues).  The flattened
 * representation always includes a format version number.
 *
 * <p>The writer has an <code>IElementInfoFactory</code>,
 * which it consults for writing element infos.
 *
 * <p>Element tree writers are thread-safe; several
 * threads may share a single writer.
 *
 */
public class ElementTreeWriter {
	/**
	 * The current format version number.
	 */
	public static final int CURRENT_FORMAT = 1;

	/**
	 * Constant representing infinite depth
	 */
	public static final int D_INFINITE = DataTreeWriter.D_INFINITE;

	/**
	 * For writing DeltaDataTrees
	 */
	protected DataTreeWriter dataTreeWriter;

	/**
	 * Constructs a new element tree writer that works for
	 * the given element info flattener.
	 */
	public ElementTreeWriter(final IElementInfoFlattener flattener) {

		/* wrap the IElementInfoFlattener in an IDataFlattener */
		IDataFlattener f = new IDataFlattener() {
			@Override
			public void writeData(IPath path, Object data, DataOutput output) throws IOException {
				// never write the root node of an ElementTree
				//because it contains the parent backpointer.
				if (!Path.ROOT.equals(path)) {
					flattener.writeElement(path, data, output);
				}
			}

			@Override
			public Object readData(IPath path, DataInput input) {
				return null;
			}
		};
		dataTreeWriter = new DataTreeWriter(f);
	}

	/**
	 * Sorts the given array of trees so that the following rules are true:
	 * 	 - The first tree has no parent
	 * 	 - No tree has an ancestor with a greater index in the array.
	 * If there are no missing parents in the given trees array, this means
	 * that in the resulting array, the i'th tree's parent will be tree i-1.
	 * The input tree array may contain duplicate trees.
	 * The sort order is written to the given output stream.
	 */
	protected ElementTree[] sortTrees(ElementTree[] trees, DataOutput output) throws IOException {

		/* the sorted list */
		int numTrees = trees.length;
		ElementTree[] sorted = new ElementTree[numTrees];
		int[] order = new int[numTrees];

		/* first build a table of ElementTree -> HashMap of Integers(indices in trees array) */
		HashMap<ElementTree, List<Integer>> table = new HashMap<>(numTrees * 2 + 1);
		for (int i = 0; i < trees.length; i++) {
			List<Integer> indices = table.get(trees[i]);
			if (indices == null) {
				indices = new ArrayList<>();
				table.put(trees[i], indices);
			}
			indices.add(i);
		}

		/* find the oldest tree (a descendent of all other trees) */
		ElementTree oldest = trees[ElementTree.findOldest(trees)];

		/**
		 * Walk through the chain of trees from oldest to newest,
		 * adding them to the sorted list as we go.
		 */
		int i = numTrees - 1;
		while (i >= 0) {
			/* add all instances of the current oldest tree to the sorted list */
			List<Integer> indices = table.remove(oldest);
			for (Enumeration<Integer> e = Collections.enumeration(indices); e.hasMoreElements();) {
				Integer next = e.nextElement();
				sorted[i] = oldest;
				order[i] = next.intValue();
				i--;
			}
			if (i >= 0) {
				/* find the next tree in the list */
				ElementTree parent = oldest.getParent();
				while (table.get(parent) == null) {
					if (parent == null) {
						throw new IOException("null parent found while sorting trees"); //$NON-NLS-1$
					}
					parent = parent.getParent();
				}
				oldest = parent;
			}
		}

		/* write the order array */
		for (i = 0; i < numTrees; i++) {
			writeNumber(order[i], output);
		}
		return sorted;
	}

	/**
	 * Writes the delta describing the changes that have to be made
	 * to newerTree to obtain olderTree.
	 *
	 * @param path The path of the subtree to write.  All nodes on the path above
	 *  the subtree are represented as empty nodes.
	 * @param depth The depth of the subtree to write.  A depth of zero writes a
	 *  single node, and a depth of D_INFINITE writes the whole subtree.
	 * @param output The stream to write the subtree to.
	 */
	public void writeDelta(ElementTree olderTree, ElementTree newerTree, IPath path, int depth, final DataOutput output, IElementComparator comparator) throws IOException {

		/* write the version number */
		writeNumber(CURRENT_FORMAT, output);

		/**
		 * Note that in current ElementTree usage, the newest
		 * tree is the complete tree, and older trees are just
		 * deltas on the new tree.
		 */
		DeltaDataTree completeTree = newerTree.getDataTree();
		DeltaDataTree derivedTree = olderTree.getDataTree();
		DeltaDataTree deltaToWrite = null;

		deltaToWrite = completeTree.forwardDeltaWith(derivedTree, comparator);

		Assert.isTrue(deltaToWrite.isImmutable());
		dataTreeWriter.writeTree(deltaToWrite, path, depth, output);
	}

	/**
	 * Writes an array of ElementTrees to the given output stream.
	 * @param trees A chain of ElementTrees, where on tree in the list is
	 * complete, and all other trees are deltas on the previous tree in the list.
	 * @param path The path of the subtree to write.  All nodes on the path above
	 *  the subtree are represented as empty nodes.
	 * @param depth The depth of the subtree to write.  A depth of zero writes a
	 *  single node, and a depth of D_INFINITE writes the whole subtree.
	 * @param output The stream to write the subtree to.

	 */
	public void writeDeltaChain(ElementTree[] trees, IPath path, int depth, DataOutput output, IElementComparator comparator) throws IOException {
		/* Write the format version number */
		writeNumber(CURRENT_FORMAT, output);

		/* Write the number of trees */
		int treeCount = trees.length;
		writeNumber(treeCount, output);

		if (treeCount <= 0) {
			return;
		}

		/**
		 * Sort the trees in ancestral order,
		 * which writes the tree order to the output
		 */
		ElementTree[] sortedTrees = sortTrees(trees, output);

		/* Write the complete tree */
		writeTree(sortedTrees[0], path, depth, output);

		/* Write the deltas for each of the remaining trees */
		for (int i = 1; i < treeCount; i++) {
			writeDelta(sortedTrees[i], sortedTrees[i - 1], path, depth, output, comparator);
		}
	}

	/**
	 * Writes an integer in a compact format biased towards
	 * small non-negative numbers. Numbers between
	 * 0 and 254 inclusive occupy 1 byte; other numbers occupy 5 bytes.
	 */
	protected void writeNumber(int number, DataOutput output) throws IOException {
		if (number >= 0 && number < 0xff) {
			output.writeByte(number);
		} else {
			output.writeByte(0xff);
			output.writeInt(number);
		}
	}

	/**
	 * Writes all or some of an element tree to an output stream.
	 * This always writes the most current version of the element tree
	 * file format, whereas the reader supports multiple versions.
	 *
	 * @param tree The tree to write
	 * @param path The path of the subtree to write.  All nodes on the path above
	 *  the subtree are represented as empty nodes.
	 * @param depth The depth of the subtree to write.  A depth of zero writes a
	 *  single node, and a depth of D_INFINITE writes the whole subtree.
	 * @param output The stream to write the subtree to.
	 */
	public void writeTree(ElementTree tree, IPath path, int depth, final DataOutput output) throws IOException {

		/* Write the format version number. */
		writeNumber(CURRENT_FORMAT, output);

		/* This actually just copies the root node, which is what we want */
		DeltaDataTree subtree = new DeltaDataTree(tree.getDataTree().copyCompleteSubtree(Path.ROOT));

		dataTreeWriter.writeTree(subtree, path, depth, output);
	}
}
