/*******************************************************************************
 * Copyright (c) 2001, 2010 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     Jesper Steen M�ller - initial IDocumentExtension4 support - #102822
 *     David Carver (Intalio) - bug 300443 - some constants aren't static final
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.format;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.wst.sse.core.internal.Logger;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.util.Assert;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;


public abstract class AbstractStructuredFormatProcessor implements IStructuredFormatProcessor {
	protected IStructuredFormatContraints fFormatContraints = null;
	protected IProgressMonitor fProgressMonitor = null;
	public boolean refreshFormatPreferences = true; // special flag for JUnit
	/*
	 * Max length of text to be formatted to be considered a "small change"
	 * Used for document rewrite session type.
	 */
	private static final int MAX_SMALL_FORMAT_SIZE = 1000;

	protected void ensureClosed(OutputStream outputStream, InputStream inputStream) {

		try {
			if (inputStream != null) {
				inputStream.close();
			}
		}
		catch (IOException e) {
			Logger.logException(e); // hopeless
		}
		try {
			if (outputStream != null) {
				outputStream.close();
			}
		}
		catch (IOException e) {
			Logger.logException(e); // hopeless
		}
	}

	public String formatContent(String input) throws IOException, CoreException {
		if (input == null)
			return input;

		IStructuredModel structuredModel = null;
		InputStream inputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for read. Will return formatted
			// string and NOT save model.
			inputStream = new ByteArrayInputStream(input.getBytes("UTF8")); //$NON-NLS-1$
			String id = inputStream.toString() + "." + getFileExtension(); //$NON-NLS-1$
			structuredModel = StructuredModelManager.getModelManager().getModelForRead(id, inputStream, null);

			// format
			formatModel(structuredModel);

			// return output
			return structuredModel.getStructuredDocument().get();
		}
		finally {
			ensureClosed(null, inputStream);
			// release from model manager
			if (structuredModel != null)
				structuredModel.releaseFromRead();
		}
	}

	public String formatContent(String input, int start, int length) throws IOException, CoreException {
		if (input == null)
			return input;

		if ((start >= 0) && (length >= 0) && (start + length <= input.length())) {
			IStructuredModel structuredModel = null;
			InputStream inputStream = null;
			try {
				// setup structuredModel
				// Note: We are getting model for read. Will return formatted
				// string and NOT save model.
				inputStream = new ByteArrayInputStream(input.getBytes("UTF8")); //$NON-NLS-1$
				String id = inputStream.toString() + "." + getFileExtension(); //$NON-NLS-1$
				structuredModel = StructuredModelManager.getModelManager().getModelForRead(id, inputStream, null);

				// format
				formatModel(structuredModel, start, length);

				// return output
				return structuredModel.getStructuredDocument().get();
			}
			finally {
				ensureClosed(null, inputStream);
				// release from model manager
				if (structuredModel != null)
					structuredModel.releaseFromRead();
			}
		}
		else
			return input;
	}

	public void formatDocument(IDocument document) throws IOException, CoreException {
		if (document == null)
			return;

		IStructuredModel structuredModel = null;
		// OutputStream outputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for edit. Will save model if model
			// changed.
			structuredModel = StructuredModelManager.getModelManager().getExistingModelForEdit(document);

			// format
			formatModel(structuredModel);

			// save model if needed
			if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
				structuredModel.save();
		}
		finally {
			// ensureClosed(outputStream, null);
			// release from model manager
			if (structuredModel != null)
				structuredModel.releaseFromEdit();
		}
	}

	public void formatDocument(IDocument document, int start, int length) throws IOException, CoreException {
		if (document == null)
			return;

		if ((start >= 0) && (length >= 0) && (start + length <= document.getLength())) {
			IStructuredModel structuredModel = null;
			// OutputStream outputStream = null;
			try {
				// setup structuredModel
				// Note: We are getting model for edit. Will save model if
				// model changed.
				structuredModel = StructuredModelManager.getModelManager().getExistingModelForEdit(document);

				if (structuredModel != null) {
					// format
					formatModel(structuredModel, start, length);

					// save model if needed
					if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
						structuredModel.save();
				}
			}
			finally {
				// ensureClosed(outputStream, null);
				// release from model manager
				if (structuredModel != null)
					structuredModel.releaseFromEdit();
			}
		}
	}

	public void formatFile(IFile file) throws IOException, CoreException {
		if (file == null)
			return;

		IStructuredModel structuredModel = null;
		// OutputStream outputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for edit. Will save model if model
			// changed.
			structuredModel = StructuredModelManager.getModelManager().getModelForEdit(file);

			// format
			formatModel(structuredModel);

			// save model if needed
			if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
				structuredModel.save();
		}
		finally {
			// ensureClosed(outputStream, null);
			// release from model manager
			if (structuredModel != null) {
				structuredModel.releaseFromEdit();
			}

		}
	}

	public void formatFile(IFile file, int start, int length) throws IOException, CoreException {
		if (file == null)
			return;

		IStructuredModel structuredModel = null;
		// OutputStream outputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for edit. Will save model if model
			// changed.
			structuredModel = StructuredModelManager.getModelManager().getModelForEdit(file);

			// format
			formatModel(structuredModel, start, length);

			// save model if needed
			if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
				structuredModel.save();
		}
		finally {
			// ensureClosed(outputStream, null);
			// release from model manager
			if (structuredModel != null)
				structuredModel.releaseFromEdit();
		}
	}

	public void formatFileName(String fileName) throws IOException, CoreException {
		if (fileName == null)
			return;

		IStructuredModel structuredModel = null;
		InputStream inputStream = null;
		// OutputStream outputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for edit. Will save model if model
			// changed.
			inputStream = new FileInputStream(fileName);
			structuredModel = StructuredModelManager.getModelManager().getModelForEdit(fileName, inputStream, null);

			// format
			formatModel(structuredModel);

			// save model if needed
			if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
				structuredModel.save();
		}
		finally {
			// ensureClosed(outputStream, inputStream);
			// release from model manager
			if (structuredModel != null)
				structuredModel.releaseFromEdit();
		}
	}

	public void formatFileName(String fileName, int start, int length) throws IOException, CoreException {
		if (fileName == null)
			return;

		IStructuredModel structuredModel = null;
		InputStream inputStream = null;
		// OutputStream outputStream = null;
		try {
			// setup structuredModel
			// Note: We are getting model for edit. Will save model if model
			// changed.
			inputStream = new FileInputStream(fileName);
			structuredModel = StructuredModelManager.getModelManager().getModelForEdit(fileName, inputStream, null);

			// format
			formatModel(structuredModel, start, length);

			// save model if needed
			if (!structuredModel.isSharedForEdit() && structuredModel.isSaveNeeded())
				structuredModel.save();
		}
		finally {
			// ensureClosed(outputStream, inputStream);
			// release from model manager
			if (structuredModel != null)
				structuredModel.releaseFromEdit();
		}
	}

	public void formatModel(IStructuredModel structuredModel) {
		int start = 0;
		int length = structuredModel.getStructuredDocument().getLength();

		formatModel(structuredModel, start, length);
	}

	public void formatModel(IStructuredModel structuredModel, int start, int length) {
		if (structuredModel != null) {
			// for debugging purposes
			long startTime = System.currentTimeMillis();

			IDocumentExtension4 docExt4 = null;
			if (structuredModel.getStructuredDocument() instanceof IDocumentExtension4) {
				docExt4 = (IDocumentExtension4) structuredModel.getStructuredDocument();
			}
			DocumentRewriteSession rewriteSession = null;

			try {
				// whenever formatting model, fire abouttochange/modelchanged
				structuredModel.aboutToChangeModel();
				DocumentRewriteSessionType rewriteType = (length > MAX_SMALL_FORMAT_SIZE) ? DocumentRewriteSessionType.UNRESTRICTED : DocumentRewriteSessionType.UNRESTRICTED_SMALL;
				rewriteSession = (docExt4 == null || docExt4.getActiveRewriteSession() != null) ? null : docExt4.startRewriteSession(rewriteType);

				if ((start == 0) && (length == structuredModel.getStructuredDocument().getLength()))
					setFormatWithSiblingIndent(structuredModel, false);
				else
					setFormatWithSiblingIndent(structuredModel, true);

				if ((start >= 0) && (length >= 0) && (start + length <= structuredModel.getStructuredDocument().getLength())) {
					List activeNodes = getAllActiveNodes(structuredModel, start, length);
					if (activeNodes.size() > 0) {
						Node firstNode = (Node) activeNodes.get(0);
						Node lastNode = (Node) activeNodes.get(activeNodes.size() - 1);

						boolean done = false;
						Node eachNode = firstNode;
						Node nextNode = null;
						while (!done) {
							// update "done"
							done = (eachNode == lastNode);

							/*
							 * get next sibling before format because eachNode
							 * may be deleted, for example when it's an empty
							 * text node
							 */
							nextNode = eachNode.getNextSibling();

							// format each node
							formatNode(eachNode);

							// update each node
							if ((nextNode != null) && (nextNode.getParentNode() == null))
								// nextNode is deleted during format
								eachNode = eachNode.getNextSibling();
							else
								eachNode = nextNode;

							// This should not be needed, but just in case
							// something went wrong with with eachNode.
							// We don't want an infinite loop here.
							if (eachNode == null)
								done = true;
						}

					}
				}
			}
			finally {
				// we need two finally's, just in case first fails
				try {
					if ((docExt4 != null) && (rewriteSession != null))
						docExt4.stopRewriteSession(rewriteSession);
				}
				finally {
					// always make sure to fire changedmodel when done
					structuredModel.changedModel();
				}
			}

			if (Logger.DEBUG_FORMAT) {
				long endTime = System.currentTimeMillis();
				System.out.println("formatModel time: " + (endTime - startTime)); //$NON-NLS-1$
			}
		}
	}

	public void formatNode(Node node) {
		if (node != null) {
			Node newNode = node;

			// format the owner node if it's an attribute node
			if (node.getNodeType() == Node.ATTRIBUTE_NODE)
				newNode = ((Attr) node).getOwnerElement();

			// refresh format preferences before getting formatter
			if (refreshFormatPreferences)
				refreshFormatPreferences();

			// get formatter and format contraints
			IStructuredFormatter formatter = getFormatter(newNode);
			// TODO_future: added assert to replace "redundant null check".
			// if formatter is ever null, we should provide some
			// default formatter to serve as place holder.
			Assert.isNotNull(formatter, "formatter was null for a node, "); //$NON-NLS-1$
			IStructuredFormatContraints formatContraints = formatter.getFormatContraints();
			formatContraints.setFormatWithSiblingIndent(true);
			// format each node
			formatter.format(newNode, formatContraints);
		}
	}

	/**
	 * @deprecated Use getAllActiveNodes instead
	 */
	protected Vector getActiveNodes(IStructuredModel structuredModel, int startNodeOffset, int length) {
		List allActiveNodes = getAllActiveNodes(structuredModel, startNodeOffset, length);
		return new Vector(allActiveNodes);
	}

	protected List getAllActiveNodes(IStructuredModel structuredModel, int startNodeOffset, int length) {
		List activeNodes = new ArrayList();

		if (structuredModel != null) {
			Node startNode = (Node) structuredModel.getIndexedRegion(startNodeOffset);
			// see https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=4711
			//
			// We have to watch for selection boundary conditions. Use this as
			// an example: <a>text</a><b>text</b>,
			// If the whole <a> node is selected, like:
			// |<a>text</a>|<b>text</b>, we need to substract the length by 1
			// to find
			// the node at the end of the selection:
			// structuredModel.getIndexedRegion(startNodeOffset + length - 1),
			// or else
			// we'd find the next adjacent node.
			//
			// However, when the selection length is 0 (meaning no text is
			// selected), the cursor is at the beginning
			// of the node we want to format: |<a>text</a><b>text</b>, the
			// node at the end of the selection is:
			// structuredModel.getIndexedRegion(startNodeOffset + length).
			int endNodeOffset = length > 0 ? startNodeOffset + length - 1 : startNodeOffset + length;
			Node endNode = (Node) structuredModel.getIndexedRegion(endNodeOffset);

			// make sure it's an non-empty document
			if (startNode != null) {
				while (isSiblingOf(startNode, endNode) == false) {
					if (endNode != null)
						endNode = endNode.getParentNode();
					if (endNode == null) {
						startNode = startNode.getParentNode();
						// see
						// https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=4711
						// and notes above
						endNodeOffset = length > 0 ? startNodeOffset + length - 1 : startNodeOffset + length;
						endNode = (Node) structuredModel.getIndexedRegion(endNodeOffset);
					}
				}

				while (startNode != endNode) {
					activeNodes.add(startNode);
					startNode = startNode.getNextSibling();
				}
				if (startNode != null)
					activeNodes.add(startNode);
			}
		}

		return activeNodes;
	}

	abstract protected String getFileExtension();

	protected IStructuredFormatContraints getFormatContraints(IStructuredModel structuredModel) {
		// 262135 - NPE during format of empty document
		if ((fFormatContraints == null) && (structuredModel != null)) {
			Node node = (Node) structuredModel.getIndexedRegion(0);

			if (node != null) {
				IStructuredFormatter formatter = getFormatter(node);
				if (formatter != null) {
					fFormatContraints = formatter.getFormatContraints();
				}
			}
		}

		return fFormatContraints;
	}

	abstract protected IStructuredFormatter getFormatter(Node node);

	protected boolean isSiblingOf(Node node, Node endNode) {
		if (endNode == null)
			return true;
		else {
			Node siblingNode = node;
			while (siblingNode != null) {
				if (siblingNode == endNode)
					return true;
				else
					siblingNode = siblingNode.getNextSibling();
			}
			return false;
		}
	}

	abstract protected void refreshFormatPreferences();

	protected void setFormatWithSiblingIndent(IStructuredModel structuredModel, boolean formatWithSiblingIndent) {
		// 262135 - NPE during format of empty document
		IStructuredFormatContraints formatContraints = getFormatContraints(structuredModel);

		if (formatContraints != null)
			formatContraints.setFormatWithSiblingIndent(formatWithSiblingIndent);
	}

	public void setProgressMonitor(IProgressMonitor monitor) {
		fProgressMonitor = monitor;
	}
}
