/*******************************************************************************
 * Copyright (c) 2000, 2011 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.compare.structuremergeviewer;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;

import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IEditableContentExtension;
import org.eclipse.compare.IEncodedStreamContentAccessor;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IDocumentRange;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.widgets.Shell;


/**
 * A document range node represents a structural element
 * when performing a structure compare of documents.
 * <code>DocumentRangeNodes</code> are created while parsing the document and represent
 * a semantic entity (e.g. a Java class or method).
 * As a consequence of the parsing a <code>DocumentRangeNode</code> maps to a range
 * of characters in the document.
 * <p>
 * Since a <code>DocumentRangeNode</code> implements the <code>IStructureComparator</code>
 * and <code>IStreamContentAccessor</code> interfaces it can be used as input to the
 * differencing engine. This makes it possible to perform
 * a structural diff on a document and have the nodes and leaves of the compare easily map
 * to character ranges within the document.
 * <p>
 * Clients need to be aware that this node registers position updaters with the document
 * using {@link IDocument#addPosition(String, Position)} with the category set to
 * {@link IDocumentRange#RANGE_CATEGORY}. The {@link StructureDiffViewer} will
 * remove the category when the nodes are no longer being used. Other clients
 * must do the same.
 * <p>
 * Subclasses may add additional state collected while parsing the document.
 * </p>
 * @see Differencer
 */
public class DocumentRangeNode
		implements IDocumentRange, IStructureComparator, IEditableContent,
		IEncodedStreamContentAccessor, IAdaptable, IEditableContentExtension {
	private static final String UTF_16= "UTF-16"; //$NON-NLS-1$

	private IDocument fBaseDocument;
	private Position fRange; // the range in the base document
	private int fTypeCode;
	private String fID;
	private Position fAppendPosition; // a position where to insert a child textually
	private ArrayList<DocumentRangeNode> fChildren;
	private final DocumentRangeNode fParent;

	/**
	 * Creates a new <code>DocumentRangeNode</code> for the given range within the specified
	 * document. The <code>typeCode</code> is uninterpreted client data. The ID is used when comparing
	 * two nodes with each other: i.e. the differencing engine performs a content compare
	 * on two nodes if their IDs are equal.
	 *
	 * @param typeCode a type code for this node
	 * @param id an identifier for this node
	 * @param document document on which this node is based on
	 * @param start start position of range within document
	 * @param length length of range
	 */
	public DocumentRangeNode(int typeCode, String id, IDocument document, int start, int length) {
		this(null, typeCode, id, document, start, length);
	}

	/**
	 * Creates a new <code>DocumentRangeNode</code> for the given range within the specified
	 * document. The <code>typeCode</code> is uninterpreted client data. The ID is used when comparing
	 * two nodes with each other: i.e. the differencing engine performs a content compare
	 * on two nodes if their IDs are equal.
	 *
	 * @param parent the parent node
	 * @param typeCode a type code for this node
	 * @param id an identifier for this node
	 * @param document document on which this node is based on
	 * @param start start position of range within document
	 * @param length length of range
	 * @since 3.3
	 */
	public DocumentRangeNode(DocumentRangeNode parent, int typeCode, String id, IDocument document, int start, int length) {
		fParent = parent;
		fTypeCode= typeCode;
		fID= id;
		fBaseDocument= document;
		registerPositionUpdater(start, length);
	}

	private void registerPositionUpdater(int start, int length) {
		fBaseDocument.addPositionCategory(RANGE_CATEGORY);
		fRange= new Position(start, length);
		try {
			fBaseDocument.addPosition(RANGE_CATEGORY, fRange);
		} catch (BadPositionCategoryException ex) {
			CompareUIPlugin.log(ex);
		} catch (BadLocationException ex) {
			CompareUIPlugin.log(ex);
		}
	}

	/* (non Javadoc)
	 * see IDocumentRange.getDocument
	 */
	@Override
	public IDocument getDocument() {
		return fBaseDocument;
	}

	/* (non Javadoc)
	 * see IDocumentRange.getRange
	 */
	@Override
	public Position getRange() {
		return fRange;
	}

	/**
	 * Returns the type code of this node.
	 * The type code is uninterpreted client data which can be set in the constructor.
	 *
	 * @return the type code of this node
	 */
	public int getTypeCode() {
		return fTypeCode;
	}

	/**
	 * Returns this node's id.
	 * It is used in <code>equals</code> and <code>hashcode</code>.
	 *
	 * @return the node's id
	 */
	public String getId() {
		return fID;
	}

	/**
	 * Sets this node's id.
	 * It is used in <code>equals</code> and <code>hashcode</code>.
	 *
	 * @param id the new id for this node
	 */
	public void setId(String id) {
		fID= id;
	}

	/**
	 * Adds the given node as a child.
	 *
	 * @param node the node to add as a child
	 */
	public void addChild(DocumentRangeNode node) {
		if (fChildren == null)
			fChildren= new ArrayList<>();
		fChildren.add(node);
	}

	@Override
	public Object[] getChildren() {
		if (fChildren != null)
			return fChildren.toArray();
		return new Object[0];
	}

	/**
	 * Sets the length of the range of this node.
	 *
	 * @param length the length of the range
	 */
	public void setLength(int length) {
		getRange().setLength(length);
	}

	/**
	 * Sets a position within the document range that can be used to (legally) insert
	 * text without breaking the syntax of the document.
	 * <p>
	 * E.g. when parsing a Java document the "append position" of a <code>DocumentRangeNode</code>
	 * representing a Java class could be the character position just before the closing bracket.
	 * Inserting the text of a new method there would not disturb the syntax of the class.
	 *
	 * @param pos the character position within the underlying document where text can be legally inserted
	 */
	public void setAppendPosition(int pos) {
		if (fAppendPosition != null)
			try {
				fBaseDocument.removePosition(RANGE_CATEGORY, fAppendPosition);
			} catch (BadPositionCategoryException e) {
				// Ignore
			}
		try {
			// TODO: Avoid an exception for a position that is past the end of the document
			if (pos <= getDocument().getLength()) {
				Position p= new Position(pos);
				fBaseDocument.addPosition(RANGE_CATEGORY, p);
				fAppendPosition= p;
			}
		} catch (BadPositionCategoryException ex) {
			// silently ignored
		} catch (BadLocationException ex) {
			// silently ignored
		}
	}

	/**
	 * Returns the position that has been set with <code>setAppendPosition</code>.
	 * If <code>setAppendPosition</code> hasn't been called, the position after the last character
	 * of this range is returned. This method will return <code>null</code> if the position
	 * could not be registered with the document.
	 *
	 * @return a position where text can be legally inserted
	 */
	public Position getAppendPosition() {
		if (fAppendPosition == null) {
			try {
				Position p= new Position(fBaseDocument.getLength());
				fBaseDocument.addPosition(RANGE_CATEGORY, p);
				fAppendPosition= p;
				return fAppendPosition;
			} catch (BadPositionCategoryException ex) {
				// silently ignored
			} catch (BadLocationException ex) {
				// silently ignored
			}
		}
		return new Position(fBaseDocument.getLength());
	}

	/**
	 * Implementation based on <code>getID</code>.
     * @param other the object to compare this <code>DocumentRangeNode</code> against.
     * @return <code>true</code> if the <code>DocumentRangeNodes</code>are equal; <code>false</code> otherwise.
	 */
	@Override
	public boolean equals(Object other) {
		if (other != null && other.getClass() == getClass()) {
			DocumentRangeNode tn= (DocumentRangeNode) other;
			return fTypeCode == tn.fTypeCode && fID.equals(tn.fID);
		}
		return super.equals(other);
	}

	/**
	 * Implementation based on <code>getID</code>.
	 * @return a hash code for this object.
	 */
	@Override
	public int hashCode() {
		return fID.hashCode();
	}

	/*
	 * Find corresponding position
	 */
	private Position findCorrespondingPosition(DocumentRangeNode otherParent, DocumentRangeNode child) {

		// we try to find a predecessor of left Node which exists on the right side

		if (child != null && fChildren != null) {
			int ix= otherParent.fChildren.indexOf(child);
			if (ix >= 0) {

				for (int i= ix - 1; i >= 0; i--) {
					DocumentRangeNode c1= otherParent.fChildren.get(i);
					int i2= fChildren.indexOf(c1);
					if (i2 >= 0) {
						DocumentRangeNode c= fChildren.get(i2);
						//System.out.println("  found corresponding: " + i2 + " " + c);
						Position p= c.fRange;

						//try {
						Position po= new Position(p.getOffset() + p.getLength() + 1, 0);
						//c.fBaseDocument.addPosition(RANGE_CATEGORY, po);
						return po;
						//} catch (BadLocationException ex) {
						//}
						//break;
					}
				}

				for (int i= ix; i < otherParent.fChildren.size(); i++) {
					DocumentRangeNode c1= otherParent.fChildren.get(i);
					int i2= fChildren.indexOf(c1);
					if (i2 >= 0) {
						DocumentRangeNode c= fChildren.get(i2);
						//System.out.println("  found corresponding: " + i2 + " " + c);
						Position p= c.fRange;
						//try {
						Position po= new Position(p.getOffset(), 0);
						//c.fBaseDocument.addPosition(RANGE_CATEGORY, po);
						return po;
						//} catch (BadLocationException ex) {
						//}
						//break;
					}
				}

			}
		}
		return getAppendPosition();
	}

	private void add(String s, DocumentRangeNode parent, DocumentRangeNode child) {

		Position p= findCorrespondingPosition(parent, child);
		if (p != null) {
			try {
				fBaseDocument.replace(p.getOffset(), p.getLength(), s);
			} catch (BadLocationException ex) {
				CompareUIPlugin.log(ex);
			}
		}
	}

	/* (non Javadoc)
	 * see IStreamContentAccessor.getContents
	 */
	@Override
	public InputStream getContents() {
		String s;
		try {
			s= fBaseDocument.get(fRange.getOffset(), fRange.getLength());
		} catch (BadLocationException ex) {
			s= ""; //$NON-NLS-1$
		}
		return new ByteArrayInputStream(Utilities.getBytes(s, UTF_16));
	}


	/**
	 * If this node has a parent, return the editability of the parent.
	 * Otherwise return <code>true</code>. Subclasses may override.
	 * @see org.eclipse.compare.IEditableContent#isEditable()
	 */
	@Override
	public boolean isEditable() {
		if (fParent != null)
			return fParent.isEditable();
		return true;
	}

	@Override
	public ITypedElement replace(ITypedElement child, ITypedElement other) {
		if (fParent == null) {
			// TODO: I don't believe this code does anything useful but just in case
			// I'm leaving it in but disabling it for the shared document case
			// since all the subclasses that have been converted overrode the method anyway
			DocumentRangeNode src= null;
			String srcContents= ""; //$NON-NLS-1$

			if (other != null) {
				src= (DocumentRangeNode) child;

				if (other instanceof IStreamContentAccessor) {
					try {
						srcContents= Utilities.readString((IStreamContentAccessor)other);
					} catch(CoreException ex) {
						// NeedWork
						CompareUIPlugin.log(ex);
					}
				}
			}

			if (child == null) // no destination: we have to add the contents into the parent
				add(srcContents, null, src);
		}
		nodeChanged(this);
		return child;
	}

	/**
	 * Default implementation that calls {@link #internalSetContents(byte[])}
	 * and then {@link #nodeChanged(DocumentRangeNode)}. Subclasses
	 * may override but should then call {@link #nodeChanged(DocumentRangeNode)}
	 * after the contents have been set.
	 * @see org.eclipse.compare.IEditableContent#setContent(byte[])
	 */
	@Override
	public void setContent(byte[] content) {
		internalSetContents(content);
		nodeChanged(this);
	}

	/**
	 * Method that is invoked from {@link #setContent(byte[])}. By default,
	 * this method does nothing. Subclasses may override.
	 * @param content the new content
	 * @since 3.3
	 */
	protected void internalSetContents(byte[] content) {
		// By default, do nothing
	}

	@Override
	public String getCharset() {
		return UTF_16;
	}

	/**
	 * Method that should be invoked whenever the contents of this node are
	 * changed. the change is propagated to the parent if there is one.
	 * @param node the node that has changed.
	 * @since 3.3
	 */
	protected void nodeChanged(DocumentRangeNode node) {
		if (fParent != null)
			fParent.nodeChanged(node);
	}

	/**
	 * Implement {@link IAdaptable#getAdapter(Class)} in order to provide
	 * an {@link ISharedDocumentAdapter} that provides the proper look up key based
	 * on the input from which this structure node was created. The proper
	 * shared document adapter is obtained by calling {@link #getAdapter(Class)}
	 * on this node's parent if there is one.
	 * @param adapter the adapter class to look up
	 * @return the object adapted to the given class or <code>null</code>
	 * @see IAdaptable#getAdapter(Class)
	 * @since 3.3
	 */
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == ISharedDocumentAdapter.class && fParent != null)
			return fParent.getAdapter(adapter);

		return Platform.getAdapterManager().getAdapter(this, adapter);
	}

	@Override
	public boolean isReadOnly() {
		if (fParent != null)
			return fParent.isReadOnly();
		return false;
	}

	@Override
	public IStatus validateEdit(Shell shell) {
		if (fParent != null)
			return fParent.validateEdit(shell);
		return Status.OK_STATUS;
	}

	/**
	 * Return the parent of this node or <code>null</code>
	 * if the node doesn't have a parent or the parent is not known.
	 * @return the parent of this node or <code>null</code>
	 */
	public Object getParentNode() {
		return fParent;
	}
}

