/*******************************************************************************
 * Copyright (c) 2006, 2017 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.BufferedReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.List;

import org.eclipse.compare.CompareUI;
import org.eclipse.compare.ICompareFilter;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IEncodedStreamContentAccessor;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.SharedDocumentAdapter;
import org.eclipse.compare.contentmergeviewer.IDocumentRange;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.compare.internal.patch.LineReader;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.services.IDisposable;
import org.eclipse.ui.texteditor.IDocumentProvider;

/**
 * An {@link IStructureCreator2} that attempts to use an {@link IDocumentProvider}
 * to obtain a shared document for an {@link ITypedElement}.
 * <p>
 * Clients may subclass this class.
 * </p>
 *
 * @since 3.3
 */
public abstract class StructureCreator implements IStructureCreator2 {
	@Override
	public IStructureComparator getStructure(Object input) {
		String contents= null;
		IDocument doc= CompareUI.getDocument(input);
		if (doc == null) {
			if (input instanceof IStreamContentAccessor) {
				IStreamContentAccessor sca= (IStreamContentAccessor) input;
				try {
					contents= Utilities.readString(sca);
				} catch (CoreException e) {
					// return null indicates the error.
					CompareUIPlugin.log(e);
					return null;
				}
			}

			if (contents == null) {
				// Node has no contents
				return null;
			}

			doc= new Document(contents);
			setupDocument(doc);
		}

		try {
			return createStructureComparator(input, doc, null, null);
		} catch (CoreException e) {
			CompareUIPlugin.log(e);
			return null;
		}
	}

	@Override
	public IStructureComparator createStructure(final Object element,
			final IProgressMonitor monitor) throws CoreException {
		final IStructureComparator[] result = new IStructureComparator[] { null };
		Runnable runnable = () -> {
			try {
				result[0]= internalCreateStructure(element, monitor);
			} catch (OperationCanceledException ex) {
				return;
			}
		};
		Utilities.runInUIThread(runnable);
		return result[0];
	}

	/*
	 * We need to create the structure in the UI thread since IDocument requires this
	 */
	private IStructureComparator internalCreateStructure(Object element,
			IProgressMonitor monitor) {
		final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(element);
		if (sda != null) {
			final IEditorInput input = sda.getDocumentKey(element);
			if (input != null) {
				final IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(input);
				if (provider != null) {
					try {
						sda.connect(provider, input);
						IDocument document = provider.getDocument(input);
						setupDocument(document);
						return createStructureComparator(element, document, wrapSharedDocumentAdapter(sda, element, document), monitor);
					} catch (CoreException e) {
						// Connection to the document provider failed.
						// Log and fall through to use simple structure
						CompareUIPlugin.log(e);
					}
				}
			}
		}
		return getStructure(element);
	}

	/**
	 * Creates an {@link IStructureComparator} for the given element using the
	 * contents available in the given document. If the provided
	 * {@link ISharedDocumentAdapter} is not <code>null</code> then the
	 * {@link IStructureComparator} returned by this method must implement the
	 * {@link IDisposable} interface and disconnect from the adapter when the
	 * comparator is disposed. The {@link StructureDiffViewer} class will call
	 * dispose if the {@link IStructureComparator} also implements
	 * {@link IDisposable}. Other clients must do the same.
	 * <p>
	 * It should be noted that the provided {@link ISharedDocumentAdapter}
	 * will provide the key associated with the given element when
	 * {@link ISharedDocumentAdapter#getDocumentKey(Object)} is called
	 * for any {@link IDocumentRange} node whose document matches the
	 * provided document. Thus, this adapter should also be returned
	 * by the structure comparator and its children when they are adapted
	 * to an {@link ISharedDocumentAdapter}.
	 * @param element the element
	 * @param document the document that has the contents for the element
	 * @param sharedDocumentAdapter the shared document adapter from which the
	 *            document was obtained or <code>null</code> if the document
	 *            is not shared.
	 * @param monitor a progress monitor or <code>null</code> if progress is not required
	 *
	 * @return a structure comparator
	 * @throws CoreException
	 */
	protected abstract IStructureComparator createStructureComparator(
			final Object element, IDocument document,
			final ISharedDocumentAdapter sharedDocumentAdapter,
			IProgressMonitor monitor) throws CoreException;

	/**
	 * Sets up the newly created document as appropriate. Any document partitioners
	 * should be added to a custom slot using the {@link IDocumentExtension3} interface
	 * in case the document is shared via a file buffer.
	 * @param document a document
	 */
	protected void setupDocument(IDocument document) {
		String partitioning = getDocumentPartitioning();
		if (partitioning == null || !(document instanceof IDocumentExtension3)) {
			if (document.getDocumentPartitioner() == null) {
				IDocumentPartitioner partitioner= getDocumentPartitioner();
				if (partitioner != null) {
					document.setDocumentPartitioner(partitioner);
					partitioner.connect(document);
				}
			}
		} else {
			IDocumentExtension3 ex3 = (IDocumentExtension3) document;
			if (ex3.getDocumentPartitioner(partitioning) == null) {
				IDocumentPartitioner partitioner= getDocumentPartitioner();
				if (partitioner != null) {
					ex3.setDocumentPartitioner(partitioning, partitioner);
					partitioner.connect(document);
				}
			}
		}
	}

	/**
	 * Returns the partitioner to be associated with the document or
	 * <code>null</code> is partitioning is not needed or if the subclass
	 * overrode {@link #setupDocument(IDocument)} directly.
	 * @return a partitioner
	 */
	protected IDocumentPartitioner getDocumentPartitioner() {
		return null;
	}

	/**
	 * Returns the partitioning to which the partitioner returned from
	 * {@link #getDocumentPartitioner()} is to be associated. Return <code>null</code>
	 * only if partitioning is not needed or if the subclass
	 * overrode {@link #setupDocument(IDocument)} directly.
	 * @see IDocumentExtension3
	 * @return a partitioning
	 */
	protected String getDocumentPartitioning() {
		return null;
	}

	/**
	 * Default implementation of save that extracts the contents from
	 * the document of an {@link IDocumentRange} and sets it on the
	 * input. If the input is an {@link IEncodedStreamContentAccessor},
	 * the charset of the input is used to extract the contents from the
	 * document. If the input adapts to {@link ISharedDocumentAdapter} and
	 * the document of the {@link IDocumentRange} matches that of the
	 * input, then the save is issued through the shared document adapter.
	 * @see org.eclipse.compare.structuremergeviewer.IStructureCreator#save(org.eclipse.compare.structuremergeviewer.IStructureComparator, java.lang.Object)
	 */
	@Override
	public void save(IStructureComparator node, Object input) {
		if (node instanceof IDocumentRange && input instanceof IEditableContent) {
			IDocument document= ((IDocumentRange)node).getDocument();
			// First check to see if we have a shared document
			final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(input);
			if (sda != null) {
				IEditorInput key = sda.getDocumentKey(input);
				if (key != null) {
					IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(key);
					if (provider != null) {
						IDocument providerDoc = provider.getDocument(key);
						// We have to make sure that the document we are saving is the same as the shared document
						if (providerDoc != null && providerDoc == document) {
							if (save(provider, document, input, sda, key))
								return;
						}
					}
				}
			}
			IEditableContent bca= (IEditableContent) input;
			String contents= document.get();
			String encoding= null;
			if (input instanceof IEncodedStreamContentAccessor) {
				try {
					encoding= ((IEncodedStreamContentAccessor)input).getCharset();
				} catch (CoreException e1) {
					// ignore
				}
			}
			if (encoding == null)
				encoding= ResourcesPlugin.getEncoding();
			byte[] bytes;
			try {
				bytes= contents.getBytes(encoding);
			} catch (UnsupportedEncodingException e) {
				bytes= contents.getBytes();
			}
			bca.setContent(bytes);
		}
	}

	private boolean save(final IDocumentProvider provider, final IDocument document,
			final Object input, final ISharedDocumentAdapter sda, final IEditorInput key) {
		try {
			sda.flushDocument(provider, key, document, false);
			return true;
		} catch (CoreException e) {
			CompareUIPlugin.log(e);
		}
		return false;
	}

	/**
	 * Create an {@link ISharedDocumentAdapter} that will provide the document key for the given input
	 * object for any {@link DocumentRangeNode} instances whose document is the same as the
	 * provided document.
	 * @param input the input element
	 * @param document the document associated with the input element
	 * @return a shared document adapter that provides the proper document key for document range nodes
	 */
	private final ISharedDocumentAdapter wrapSharedDocumentAdapter(ISharedDocumentAdapter elementAdapter, final Object input, final IDocument document) {
		// We need to wrap the adapter so that the proper document key gets returned
		return new SharedDocumentAdapterWrapper(elementAdapter) {
			@Override
			public IEditorInput getDocumentKey(Object element) {
				if (hasSameDocument(element)) {
					return super.getDocumentKey(input);
				}
				return super.getDocumentKey(element);
			}
			private boolean hasSameDocument(Object element) {
				if (element instanceof DocumentRangeNode) {
					DocumentRangeNode drn = (DocumentRangeNode) element;
					return drn.getDocument() == document;
				}
				return false;
			}
		};
	}

	/**
	 * Default implementation of {@link #createElement(Object, Object, IProgressMonitor)}
	 * that uses {@link #getPath(Object, Object)} to determine the
	 * path for the element, {@link #createStructure(Object, IProgressMonitor)} to create the structure
	 * and {@link #findElement(IStructureComparator, String[])} to find the
	 * element in the structure. Subclasses may override.
	 * @param element the element
	 * @param input the containing input
	 * @param monitor a progress monitor
	 * @return the sub-structure element in the input for the given element
	 * @throws CoreException if a parse error occurred
	 */
	@Override
	public ITypedElement createElement(Object element, Object input, IProgressMonitor monitor)
			throws CoreException {
		String[] path= getPath(element, input);
		if (path == null) {
			// TODO: Temporary code until subclasses are updated
			IStructureComparator locate = locate(element, input);
			if (locate instanceof ITypedElement) {
				return (ITypedElement)locate;
			}
			return null;
		}

		// Build the structure
		IStructureComparator structure= createStructure(input, monitor);
		if (structure == null)	// we couldn't parse the structure
			return null;		// so we can't find anything

		// find the path in the tree
		return findElement(structure, path);
	}

	/**
	 * Default implementation of {@link #locate(Object, Object)} that
	 * uses {@link #getPath(Object, Object)} to determine the
	 * path for the element, {@link #getStructure(Object)} to create the structure
	 * and {@link #findElement(IStructureComparator, String[])} to find the
	 * element in the structure.  Subclasses may override.
	 * @param element the element
	 * @param input the containing input
	 * @return the sub-structure element in the input for the given element
	 */
	@Override
	public IStructureComparator locate(Object element, Object input) {
		String[] path= getPath(element, input);
		if (path == null)
			return null;
		// Build the structure
		IStructureComparator structure= getStructure(input);
		if (structure == null)	// we couldn't parse the structure
			return null;		// so we can't find anything

		// find the path in the tree
		return (IStructureComparator)findElement(structure, path);
	}

	/**
	 * Finds the element at the given path in the given structure.
	 * This method is invoked from the {@link #createElement(Object, Object, IProgressMonitor)}
	 * and {@link #locate(Object, Object)} methods to find the element for
	 * the given path.
	 * @param structure the structure
	 * @param path the path of an element in the structure
	 * @return the element at the given path in the structure or <code>null</code>
	 */
	protected ITypedElement findElement(IStructureComparator structure, String[] path) {
		return (ITypedElement)find(structure, path, 0);
	}

	/**
	 * Recursively extracts the given path from the tree.
	 */
	private IStructureComparator find(IStructureComparator tree, String[] path, int index) {
		if (tree != null) {
			Object[] children= tree.getChildren();
			if (children != null) {
				for (int i= 0; i < children.length; i++) {
					IStructureComparator child= (IStructureComparator) children[i];
					if (child instanceof ITypedElement && child instanceof DocumentRangeNode) {
						String n1= ((DocumentRangeNode)child).getId();
						if (n1 == null) {
							n1= ((ITypedElement)child).getName();
						}
						String n2= path[index];
						if (n1.equals(n2)) {
							if (index == path.length-1)
								return child;
							IStructureComparator result= find(child, path, index+1);
							if (result != null)
								return result;
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * Returns the path of the element in the structure of it's containing input
	 * or <code>null</code> if the element is not contained in the input. This method is
	 * invoked from {@link #createElement(Object, Object, IProgressMonitor)} and
	 * {@link #locate(Object, Object)} methods to determine
	 * the path to be passed to {@link #findElement(IStructureComparator, String[])}.
	 * By default, <code>null</code> is returned. Subclasses may override.
	 * @param element the element
	 * @param input the input
	 * @return the path of the element in the structure of it's containing input
	 * or <code>null</code>
	 */
	protected String[] getPath(Object element, Object input) {
		return null;
	}

	@Override
	public void destroy(Object object) {
		IDisposable disposable = getDisposable(object);
		if (disposable != null)
			disposable.dispose();
	}

	private IDisposable getDisposable(Object object) {
		if (object instanceof IDisposable) {
			return (IDisposable) object;
		}
		if (object instanceof DocumentRangeNode) {
			DocumentRangeNode node = (DocumentRangeNode) object;
			return getDisposable(node.getParentNode());
		}
		return null;
	}

	/**
	 * Returns true if the two nodes are equal for comparison purposes. If
	 * <code>compareFilters</code> is not empty, the filters are applied to each
	 * line of each node's text representation.
	 *
	 * @param node1
	 * @param contributor1
	 *            either 'A', 'L', or 'R' for ancestor, left or right
	 *            contributor
	 * @param node2
	 * @param contributor2
	 *            either 'A', 'L', or 'R' for ancestor, left or right
	 *            contributor
	 * @param ignoreWhitespace
	 *            if <code>true</code> whitespace characters will be ignored
	 *            when determining equality. Note: Will bypass any custom ignore
	 *            whitespace behaviors contributed through implementations of
	 *            <code>org.eclipse.compare.structuremergeviewer.IStructureCreator.getContents()</code>
	 * @param compareFilters
	 *            the filters used to customize the comparison of lines of text.
	 * @return whether the two nodes are equal for comparison purposes
	 * @noreference This method is not intended to be referenced by clients.
	 * @since 3.6
	 */
	public boolean contentsEquals(Object node1, char contributor1,
			Object node2, char contributor2, boolean ignoreWhitespace,
			ICompareFilter[] compareFilters) {

		List<String> lines1 = LineReader.readLines(new BufferedReader(new StringReader(
				getContents(node1, false))));
		List<String> lines2 = LineReader.readLines(new BufferedReader(new StringReader(
				getContents(node2, false))));

		StringBuilder buffer1 = new StringBuilder();
		StringBuilder buffer2 = new StringBuilder();

		int maxLines = Math.max(lines1.size(), lines2.size());
		for (int i = 0; i < maxLines; i++) {
			String s1 = lines1.size() > i ? (String) lines1.get(i) : ""; //$NON-NLS-1$
			String s2 = lines2.size() > i ? (String) lines2.get(i) : ""; //$NON-NLS-1$

			if (compareFilters != null && compareFilters.length > 0) {
				s1 = Utilities.applyCompareFilters(s1, contributor1, s2,
						contributor2, compareFilters);
				s2 = Utilities.applyCompareFilters(s2, contributor2, s1,
						contributor1, compareFilters);
			}
			buffer1.append(s1);
			buffer2.append(s2);
		}
		if (ignoreWhitespace) {
			int l1 = buffer1.length();
			int l2 = buffer2.length();
			int c1 = 0, c2 = 0;
			int i1 = 0, i2 = 0;

			while (c1 != -1) {

				c1 = -1;
				while (i1 < l1) {
					char c = buffer1.charAt(i1++);
					if (!Character.isWhitespace(c)) {
						c1 = c;
						break;
					}
				}

				c2 = -1;
				while (i2 < l2) {
					char c = buffer2.charAt(i2++);
					if (!Character.isWhitespace(c)) {
						c2 = c;
						break;
					}
				}

				if (c1 != c2)
					return false;
			}
		} else if (!buffer1.toString().equals(buffer2.toString())) {
			return false;
		}

		return true;
	}
}
