/*******************************************************************************
 * Copyright (c) 2015 Pivotal, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Pivotal, Inc. - initial API and implementation
 *******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.completion;

import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.text.IRegion;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment.YamlPathSegmentType;
import org.springframework.ide.vscode.commons.yaml.structure.YamlDocument;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SChildBearingNode;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SKeyNode;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNode;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureParser.SNodeType;
import org.springframework.ide.vscode.commons.yaml.util.YamlIndentUtil;
import org.springframework.ide.vscode.commons.yaml.util.YamlUtil;

/**
 * Helper class that provides methods for creating the edits in a YamlDocument that
 * insert new 'property paths' into the document.
 *
 * @author Kris De Volder
 */
public class YamlPathEdits extends DocumentEdits {

	private YamlDocument doc;
	private YamlIndentUtil indentUtil;

	public YamlPathEdits(YamlDocument doc) {
		super(doc.getDocument());
		this.doc = doc;
		this.indentUtil = new YamlIndentUtil(doc);
	}

	/**
	 * Create the necessary edits to ensure that a given property
	 * path exists, placing cursor in the right place also to start
	 * start typing the property value.
	 * <p>
	 * This also handles cases where all or some of the path already
	 * exists. In the former case no edits are performed only cursor
	 * movement. In the latter case, the right place to start inserting
	 * the 'missing' portion of the path is found and the edits
	 * are created there.
	 */
	public void createPath(SChildBearingNode node, YamlPath path, String appendText) throws Exception {
		//This code doesn't handle selection of subddocuments
		// or creation of new subdocuments so must not call it on
		//ROOT node but start at an appropriate SDocNode (or below)
		Assert.isLegal(node.getNodeType()!=SNodeType.ROOT);
		if (!path.isEmpty()) {
			YamlPathSegment s = path.getSegment(0);
			if (s.getType()==YamlPathSegmentType.VAL_AT_KEY) {
				String key = s.toPropString();
				SKeyNode existing = node.getChildWithKey(key);
				if (existing==null) {
					createNewPath(node, path, appendText);
				} else {
					createPath(existing, path.tail(), appendText);
				}
			}
		} else {
			//whole path already exists. Just try to move cursor somewhere
			// sensible in the existing tail-end-node of the path.
			SNode child = node.getFirstRealChild();
			if (child!=null) {
				moveCursorTo(child.getStart());
			} else if (node.getNodeType()==SNodeType.KEY) {
				SKeyNode keyNode = (SKeyNode) node;
				int colonOffset = keyNode.getColonOffset();
				char c = doc.getChar(colonOffset+1);
				if (c==' ') {
					moveCursorTo(colonOffset+2); //cursor after the ": "
				} else {
					moveCursorTo(colonOffset+1); //cursor after the ":"
				}
			}
		}
	}

	private void createNewPath(SChildBearingNode parent, YamlPath path, String appendText) throws Exception {
		int indent = YamlIndentUtil.getNewChildKeyIndent(parent);
		int insertionPoint = getNewPathInsertionOffset(parent);
		boolean startOnNewLine = true;
		insert(insertionPoint, createPathInsertionText(path, indent, startOnNewLine, appendText));
	}

	protected String createPathInsertionText(YamlPath path, int indent, boolean startOnNewLine, String appendText) {
		StringBuilder buf = new StringBuilder();
		for (int i = 0; i < path.size(); i++) {
			if (startOnNewLine||i>0) {
				indentUtil.addNewlineWithIndent(indent, buf);
			}
			String key = path.getSegment(i).toPropString();
			buf.append(YamlUtil.stringEscape(key));
			buf.append(":");
			if (i<path.size()-1) {
				indent += YamlIndentUtil.INDENT_BY;
			} else {
				buf.append(indentUtil.applyIndentation(appendText, indent));
			}
		}
		return buf.toString();
	}

	public int getNewPathInsertionOffset(SChildBearingNode parent) throws Exception {
		int insertAfterLine = doc.getLineOfOffset(parent.getTreeEnd());
		while (insertAfterLine>=0 && doc.getLineIndentation(insertAfterLine)==-1) {
			insertAfterLine--;
		}
		if (insertAfterLine<0) {
			//This code is probably 'dead' because:
			//   - it can only occur if all lines in the 'parent' are empty
			//   - if parent is any other node than SRootNode then it must have at least one
			//     non-emtpy line
			//  => parent must be SRootNode and only contain comment or empty lines
			//  But in that case we will never need to compute a 'new path insertion offset'
			//  since we will always be in the case where completions are to be inserted
			//  in place (i.e. at the current cursor).
			return 0; //insert at beginning of document
		} else {
			IRegion r = doc.getLineInformation(insertAfterLine);
			return r.getOffset() + r.getLength();
		}
	}

	public void createPathInPlace(SNode contextNode, YamlPath relativePath, int insertionPoint, String appendText) throws Exception {
		int indent = YamlIndentUtil.getNewChildKeyIndent(contextNode);
		insert(insertionPoint, createPathInsertionText(relativePath, indent, needNewline(contextNode, insertionPoint), appendText));
	}

	private boolean needNewline(SNode contextNode, int insertionPoint) throws Exception {
		if (contextNode.getNodeType()==SNodeType.SEQ) {
			// after a '- ' its okay to put key on same line
			return false;
		} else {
			return lineHasTextBefore(insertionPoint);
		}
	}

	private boolean lineHasTextBefore(int insertionPoint) throws Exception {
		String textBefore = doc.getLineTextBefore(insertionPoint);
		return !textBefore.trim().isEmpty();
	}

	/**
	 * Deletes this node, and all of its children.
	 */
	public void deleteNode(SNode node) throws Exception {
		delete(node.getStart(), node.getTreeEnd());
		deleteLineBackwardAtOffset(node.getStart());
	}

}
