/*******************************************************************************
 * Copyright (c) 2000, 2005 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 java.io.DataOutput;
import java.io.IOException;
import org.eclipse.core.runtime.*;

/**
 * Class for writing a single data tree (no parents) to an output stream.
 */
public class DataTreeWriter {
	/**
	 * Callback for serializing tree data
	 */
	protected IDataFlattener flatener;

	/**
	 * The stream to write output to
	 */
	protected DataOutput output;

	/**
	 * Constant representing infinite recursion depth
	 */
	public static final int D_INFINITE = -1;

	/**
	 * Creates a new DeltaTreeWriter.
	 */
	public DataTreeWriter(IDataFlattener f) {
		flatener = f;
	}

	/**
	 * Writes the subtree rooted at the given node.
	 * @param node The subtree to write.
	 * @param path  The path of the current node.
	 * @param depth The depth of the subtree to write.
	 */
	protected void writeNode(AbstractDataTreeNode node, IPath path, int depth) throws IOException {
		int type = node.type();

		/* write the node name */
		String name = node.getName();
		if (name == null) {
			name = ""; //$NON-NLS-1$
		}
		output.writeUTF(name);

		/* write the node type */
		writeNumber(type);

		/* maybe write the data */
		if (node.hasData()) {
			Object data = node.getData();

			/**
			 * Write a flag indicating whether or not the data field is null.
			 * Zero means data is null, non-zero means data is present
			 */
			if (data == null) {
				writeNumber(0);
			} else {
				writeNumber(1);
				flatener.writeData(path, node.getData(), output);
			}

		}

		/* maybe write the children */
		if (depth > 0 || depth == D_INFINITE) {
			AbstractDataTreeNode[] children = node.getChildren();

			/* write the number of children */
			writeNumber(children.length);

			/* write the children */
			int newDepth = (depth == D_INFINITE) ? D_INFINITE : depth - 1;
			for (AbstractDataTreeNode element : children) {
				writeNode(element, path.append(element.getName()), newDepth);
			}
		} else {
			/* write the number of children */
			writeNumber(0);
		}
	}

	/**
	 * 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) throws IOException {
		if (number >= 0 && number < 0xff) {
			output.writeByte(number);
		} else {
			output.writeByte(0xff);
			output.writeInt(number);
		}
	}

	/**
	 * Writes a single node to the output.  Does not recurse
	 * on child nodes, and does not write the number of children.
	 */
	protected void writeSingleNode(AbstractDataTreeNode node, IPath path) throws IOException {
		/* write the node name */
		String name = node.getName();
		if (name == null) {
			name = ""; //$NON-NLS-1$
		}
		output.writeUTF(name);

		/* write the node type */
		writeNumber(node.type());

		/* maybe write the data */
		if (node.hasData()) {
			Object data = node.getData();

			/**
			 * Write a flag indicating whether or not the data field is null.
			 * Zero means data is null, non-zero means data is present
			 */
			if (data == null) {
				writeNumber(0);
			} else {
				writeNumber(1);
				flatener.writeData(path, node.getData(), output);
			}
		}
	}

	/**
	 * Writes the given AbstractDataTree to the given stream.  This
	 * writes a single DataTree or DeltaDataTree, ignoring parent
	 * trees.
	 *
	 * @param path Only writes data for the subtree rooted at the given path, and
	 * for all nodes directly between the root and the subtree.
	 * @param depth In the subtree rooted at the given path,
	 *  only write up to this depth.  A depth of infinity is given
	 *  by the constant D_INFINITE.
	 */
	public void writeTree(AbstractDataTree tree, IPath path, int depth, DataOutput output) throws IOException {
		this.output = output;
		/* tunnel down relevant path */
		AbstractDataTreeNode node = tree.getRootNode();
		IPath currentPath = Path.ROOT;
		String[] segments = path.segments();
		for (String nextSegment : segments) {
			/* write this node to the output */
			writeSingleNode(node, currentPath);

			currentPath = currentPath.append(nextSegment);
			node = node.childAtOrNull(nextSegment);

			/* write the number of children for this node */
			if (node != null) {
				writeNumber(1);
			} else {
				/* can't navigate down the path, just give up with what we have so far */
				writeNumber(0);
				return;
			}
		}

		Assert.isTrue(currentPath.equals(path), "dtree.navigationError"); //$NON-NLS-1$

		/* recursively write the subtree we're interested in */
		writeNode(node, path, depth);
	}
}
