/*******************************************************************************
 * Copyright (c) 2001, 2008 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 - xml:space='preserve' support
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.provisional.format;

import java.util.List;
import java.util.Vector;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.wst.sse.core.internal.format.IStructuredFormatContraints;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.utils.StringUtils;
import org.eclipse.wst.xml.core.internal.Logger;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.w3c.dom.Node;


public class TextNodeFormatter extends NodeFormatter {
	static private final String CR = "\r"; //$NON-NLS-1$
	static private final String DELIMITERS = " \t\n\r\f"; //$NON-NLS-1$
	static private final String EMPTY_STRING = ""; //$NON-NLS-1$
	static private final String FF = "\f"; //$NON-NLS-1$
	static private final String LF = "\n"; //$NON-NLS-1$
	static private final String SPACE = " "; //$NON-NLS-1$
	static private final String TAB = "\t"; //$NON-NLS-1$

	private String compressSpaces(String string, IStructuredFormatContraints formatContraints) {
		/*
		 * Note that the StructuredTextEditor supports mixed new line
		 * characters (CR, LF, CRLF) in one file. We have to handle that when
		 * we try to preserve blank lines.
		 */
		String[] stringArray = null;
		boolean clearAllBlankLines = formatContraints.getClearAllBlankLines();

		if (clearAllBlankLines)
			stringArray = StringUtils.asArray(string);
		else
			stringArray = StringUtils.asArray(string, DELIMITERS, true);

		StringBuffer compressedString = new StringBuffer();
		if (stringArray.length > 0) {
			boolean cr = false, lf = false, cr2 = false, nonSpace = true;

			if (stringArray[0].compareTo(CR) == 0)
				cr = true;
			else if (stringArray[0].compareTo(LF) == 0)
				lf = true;
			else if ((stringArray[0].compareTo(SPACE) != 0) && (stringArray[0].compareTo(TAB) != 0) && (stringArray[0].compareTo(FF) != 0)) {
				compressedString.append(stringArray[0]);
				nonSpace = true;
			}

			for (int i = 1; i < stringArray.length; i++) {
				if (stringArray[i].compareTo(CR) == 0) {
					if (cr && lf) {
						if (nonSpace) {
							compressedString.append(CR + LF);
							nonSpace = false;
						}
						compressedString.append(stringArray[i]);
						cr2 = true;
					}
					else if (cr) {
						if (nonSpace) {
							compressedString.append(CR);
							nonSpace = false;
						}
						compressedString.append(stringArray[i]);
						cr2 = true;
					}
					else
						cr = true;
				}
				else if (stringArray[i].compareTo(LF) == 0) {
					if (cr && lf && cr2) {
						compressedString.append(stringArray[i]);
					}
					else if (lf) {
						if (nonSpace) {
							compressedString.append(LF);
							nonSpace = false;
						}
						compressedString.append(stringArray[i]);
					}
					else
						lf = true;
				}
				else if ((stringArray[i].compareTo(SPACE) != 0) && (stringArray[i].compareTo(TAB) != 0) && (stringArray[i].compareTo(FF) != 0)) {
					if (compressedString.length() > 0)
						compressedString.append(SPACE);
					compressedString.append(stringArray[i]);

					cr = false;
					lf = false;
					cr2 = false;
					nonSpace = true;
				}
			}
		}

		return compressedString.toString();
	}

	protected void formatNode(IDOMNode node, IStructuredFormatContraints formatContraints) {
		// [111674] If inside xml:space="preserve" element, we bail
		if (formatContraints.getInPreserveSpaceElement())
			return;
		if (node != null) {
			IStructuredDocument doc = node.getStructuredDocument();

			int lineWidth = getFormatPreferences().getLineWidth();
			int currentAvailableLineWidth = computeAvailableLineWidth(doc, node.getStartOffset(), lineWidth);

			String nodeText = getNodeText(node);
			String compressedText = compressSpaces(nodeText, formatContraints);

			IDOMNode parentNode = (IDOMNode) node.getParentNode();

			if (((enoughSpace(parentNode, currentAvailableLineWidth, compressedText)) && (noSiblingsAndNoFollowingComment(node)) && !firstStructuredDocumentRegionContainsLineDelimiters(parentNode)) || node.getStartOffset() == 0) {
				handleNoReflow(node, doc, compressedText, parentNode);
			}
			else {
				// not enough space, need to reflow text
				String nodeIndentation = formatContraints.getCurrentIndent();
				currentAvailableLineWidth = lineWidth - getIndentationLength(nodeIndentation);
				List vector = reflowText(compressedText, currentAvailableLineWidth);
				int vectorSize = vector.size();
				StringBuffer reflowedTextBuffer = new StringBuffer();
				String lineDelimiter = getLineDelimiter(doc, node.getStartOffset());
				// handle first line specially to check for allowWhitespace
				if (vectorSize > 0) {
					// determines whether or not to allow whitespace if there
					// is an entity or cdata before it
					boolean allowWhitespace = true;
					// [206072] StringIndexOutOfBoundsException
					if (nodeText.length() == 0 || !Character.isWhitespace(nodeText.charAt(0))) {
						Node previousSibling = node.getPreviousSibling();
						if (previousSibling != null && (previousSibling.getNodeType() == Node.ENTITY_REFERENCE_NODE || previousSibling.getNodeType() == Node.CDATA_SECTION_NODE))
							allowWhitespace = false;
					}
					String theString = (String) vector.get(0);
					if (allowWhitespace) {
						reflowedTextBuffer.append(lineDelimiter);
						if (theString.trim().length() > 0)
							reflowedTextBuffer.append(nodeIndentation).append(theString);
					}
					else {
						reflowedTextBuffer.append(theString);
					}
				}
				// do the rest of the lines
				for (int i = 1; i < vectorSize; i++) {
					String theString = (String) vector.get(i);
					if (theString.trim().length() > 0)
						reflowedTextBuffer.append(lineDelimiter).append(nodeIndentation).append(theString);
					else
						reflowedTextBuffer.append(lineDelimiter);
				}
				String reflowedText = reflowedTextBuffer.toString();
				if (node.getNextSibling() == null) {
					if (isEndTagMissing(parentNode)) {
						// don't add indentation to end if parent end tag is
						// missing
					}

					else {
						// add parent's indentation to end
						nodeIndentation = getNodeIndent(parentNode);
						if (!reflowedText.endsWith(lineDelimiter + nodeIndentation)) {
							reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
							reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
						}
					}
				}
				else {
					if (!reflowedText.endsWith(lineDelimiter + nodeIndentation)) {
						// not already ended with the expected indentation
						Node nextSibling = node.getNextSibling();
						if (nextSibling.getNodeType() == Node.COMMENT_NODE) {
							// add indentation to end if
							// currentTextEndsWithLineDelimiter
							// or followed by multiLineComment

							int indexOfLastLineDelimiter = StringUtils.indexOfLastLineDelimiter(nodeText);
							boolean currentTextEndsWithLineDelimiter = indexOfLastLineDelimiter != -1;
							if (currentTextEndsWithLineDelimiter) {
								// no more non blank character after the last
								// line delimiter
								currentTextEndsWithLineDelimiter = StringUtils.indexOfNonblank(nodeText, indexOfLastLineDelimiter) == -1;
							}

							String nodeValue = nextSibling.getNodeValue();
							boolean multiLineComment = StringUtils.containsLineDelimiter(nodeValue);

							if (currentTextEndsWithLineDelimiter || multiLineComment) {
								reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
								reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
							}
						}
						else if (nextSibling.getNodeType() == Node.ENTITY_REFERENCE_NODE || nextSibling.getNodeType() == Node.CDATA_SECTION_NODE) {
							int textLength = nodeText.length();
							if (textLength > 0 && Character.isWhitespace(nodeText.charAt(textLength - 1))) {
								reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
								reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
							}
						}
						else {
							// not a comment, just add add indentation to end
							reflowedText = StringUtils.appendIfNotEndWith(reflowedText, lineDelimiter);
							reflowedText = StringUtils.appendIfNotEndWith(reflowedText, nodeIndentation);
						}
					}
				}

				replaceNodeValue(node, reflowedText);
			}
		}
	}

	/**
	 * Keeps text inline with its parent (no reflow necessary)
	 * 
	 * @param node
	 * @param doc
	 * @param compressedText
	 * @param parentNode
	 */
	private void handleNoReflow(IDOMNode node, IStructuredDocument doc, String compressedText, IDOMNode parentNode) {
		String nodeIndentation;
		// enough space and text has no line delimiters and (node has no
		// siblings or followed by inline comment) and
		// parentFirstStructuredDocumentRegionContainsLineDelimiters

		if (isEndTagMissing(parentNode)) {
			parentNode = (IDOMNode) parentNode.getParentNode();
			while (isEndTagMissing(parentNode))
				parentNode = (IDOMNode) parentNode.getParentNode();

			// add parent's indentation to end
			nodeIndentation = getNodeIndent(parentNode);
			String lineDelimiter = getLineDelimiter(doc, node.getStartOffset());
			if (!compressedText.endsWith(lineDelimiter + nodeIndentation)) {
				compressedText = StringUtils.appendIfNotEndWith(compressedText, lineDelimiter);
				compressedText = StringUtils.appendIfNotEndWith(compressedText, nodeIndentation);
			}
		}

		if ((parentNode != null) && (parentNode.getNodeType() == Node.DOCUMENT_NODE) && (node.getNodeValue().length() > 0) && (node.getNodeValue().trim().length() == 0) && ((node.getPreviousSibling() == null) || (node.getNextSibling() == null)))
			// delete spaces at the beginning or end of the document
			compressedText = EMPTY_STRING;

		replaceNodeValue(node, compressedText);
	}

	private boolean noSiblingsAndNoFollowingComment(IDOMNode node) {
		IDOMNode nextSibling = (IDOMNode) node.getNextSibling();
		return !nodeHasSiblings(node) || (noLineDelimiter(node) && isComment(nextSibling) && noLineDelimiter(nextSibling));
	}

	private boolean isComment(IDOMNode node) {
		boolean result = false;
		if (node != null) {
			result = node.getNodeType() == Node.COMMENT_NODE;
		}
		return result;
	}

	private boolean noLineDelimiter(IDOMNode node) {
		boolean result = false;
		if (node != null) {
			result = !StringUtils.containsLineDelimiter(node.getNodeValue());
		}
		return result;
	}

	/**
	 * Calculates if there is enough space on the current line for
	 * compressedText (and for its parent end tag)
	 * 
	 * @param parentNode
	 * @param currentAvailableLineWidth
	 * @param compressedText
	 * @return
	 */
	private boolean enoughSpace(IDOMNode parentNode, int currentAvailableLineWidth, String compressedText) {
		int parentEndTagLength = parentNode.getNodeName().length() + 3;
		return compressedText.length() <= (currentAvailableLineWidth - parentEndTagLength) && !StringUtils.containsLineDelimiter(compressedText);
	}

	protected Vector reflowText(String text, int availableWidth) {
		String[] stringArray = null;
		boolean clearAllBlankLines = getFormatPreferences().getClearAllBlankLines();

		if (clearAllBlankLines)
			stringArray = StringUtils.asArray(text);
		else
			stringArray = StringUtils.asArray(text, DELIMITERS, true);

		Vector output = new Vector();
		if ((stringArray != null) && (stringArray.length > 0)) {
			StringBuffer buffer = new StringBuffer();
			if (stringArray[0].compareTo(CR) != 0)
				buffer.append(stringArray[0]);
			int bufferLength = stringArray[0].toString().length();
			boolean cr = stringArray[0].compareTo(CR) == 0;

			for (int i = 1; i < stringArray.length; i++) {
				String eachString = stringArray[i];
				if ((eachString.compareTo(SPACE) != 0) && (eachString.compareTo(TAB) != 0) && (eachString.compareTo(FF) != 0)) {
					if ((bufferLength + 1 + eachString.length() > availableWidth) || (eachString.compareTo(CR) == 0) || (eachString.compareTo(LF) == 0)) {
						if ((eachString.compareTo(LF) == 0) && cr) {
							// do nothing
						}
						else {
							output.add(buffer.toString());
							buffer = new StringBuffer();
							bufferLength = 0;
						}
						cr = eachString.compareTo(CR) == 0;
					}
					else if (buffer.toString().trim().length() > 0) {
						buffer.append(SPACE);
						bufferLength++;
					}
					if ((eachString.compareTo(CR) != 0) && (eachString.compareTo(LF) != 0)) {
						buffer.append(eachString);
						bufferLength = bufferLength + eachString.length();
					}
				}
			}
			output.add(buffer.toString());
		}
		else
			output.add(text);

		return output;
	}

	private String getLineDelimiter(IStructuredDocument doc, int nodeOffset) {
		int line = doc.getLineOfOffset(nodeOffset);
		String lineDelimiter = doc.getLineDelimiter();
		try {
			if (line > 0) {
				lineDelimiter = doc.getLineDelimiter(line - 1);
			}
		}
		catch (BadLocationException e) {
			// log for now, unless we find reason not to
			Logger.log(Logger.INFO, e.getMessage());
		}
		// BUG115716: if cannot get line delimiter from current line, just
		// use default line delimiter
		if (lineDelimiter == null)
			lineDelimiter = doc.getLineDelimiter();
		return lineDelimiter;
	}

	private int computeAvailableLineWidth(IStructuredDocument doc, int nodeOffset, int lineWidth) {
		// compute current available line width
		int currentAvailableLineWidth = 0;
		try {
			int lineOffset = doc.getLineInformationOfOffset(nodeOffset).getOffset();
			String text = doc.get(lineOffset, nodeOffset - lineOffset);
			int usedWidth = getIndentationLength(text);
			currentAvailableLineWidth = lineWidth - usedWidth;
		}
		catch (BadLocationException e) {
			// log for now, unless we find reason not to
			Logger.log(Logger.INFO, e.getMessage());
		}
		return currentAvailableLineWidth;
	}

}
