/*******************************************************************************
 * Copyright (c) 2010, 2011 IBM Corporation and others.
 * 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:
 *     Bruno Medeiros - initial API and implementation
 *******************************************************************************/
package melnorme.lang.ide.core.text.format;


import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertUnreachable;
import static melnorme.utilbox.core.CoreUtil.array;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;

import melnorme.lang.ide.core.text.BlockHeuristicsScannner;
import melnorme.lang.ide.core.text.TextSourceUtils;
import melnorme.lang.ide.core.text.BlockHeuristicsScannner.BlockBalanceResult;
import melnorme.lang.ide.core.text.BlockHeuristicsScannner.BlockTokenRule;

/**
 * LangAutoEditStrategy provides a common auto-edit strategy of smart indenting and de-indenting, 
 * for block based languages (like C-style languages)
 * 
 * TODO smart paste
 * 
 * @author BrunoM
 */
public class LangAutoEditStrategy extends AbstractAutoEditStrategy {
	
	protected static final BlockTokenRule[] BLOCK_RULES_Braces = 
			array(new BlockTokenRule('{', '}'));
	protected static final BlockTokenRule[] BLOCK_RULES_BracesAndParenthesis = 
			array(new BlockTokenRule('{', '}'), new BlockTokenRule('(', ')'));
	
	protected final ILangAutoEditsPreferencesAccess preferences;
	
	protected final String partitioning;
	protected final String contentType;
	
	protected LangAutoEditStrategy(ITextViewer viewer, ILangAutoEditsPreferencesAccess preferences) {
		this(viewer, IDocumentExtension3.DEFAULT_PARTITIONING, IDocument.DEFAULT_CONTENT_TYPE, preferences);
	}
	
	public LangAutoEditStrategy(ITextViewer viewer, String partitioning, String contentType,
			ILangAutoEditsPreferencesAccess preferences) {
		super(viewer);
		this.preferences = preferences;
		this.partitioning = partitioning;
		this.contentType = contentType;
	}
	
	protected boolean parenthesesAsBlocks;
	protected String indentUnit;
	
	@Override
	protected void doCustomizeDocumentCommand(IDocument doc, DocumentCommand cmd) throws BadLocationException {
		parenthesesAsBlocks = preferences.parenthesesAsBlocks();
		indentUnit = preferences.getIndentUnit();
		boolean isSmartIndent = preferences.isSmartIndent();
		
		if(isSmartIndent && isSimpleNewLineKeyPress(cmd)) {
			smartIndentAfterNewLine(doc, cmd);
		} else if(smartDeIndentAfterDeletion(doc, cmd)) {
			return;
		} else if(isSmartIndent && isSimpleKeyPressCommand(cmd)) {
			smartIndentOnKeypress(doc, cmd);
		} else {
		}
	}
	
	protected BlockHeuristicsScannner createBlockHeuristicsScanner(IDocument doc) {
		return new BlockHeuristicsScannner(doc, partitioning, contentType, getBlockRules());
	}
	
	protected BlockTokenRule[] getBlockRules() {
		if(parenthesesAsBlocks) {
			return BLOCK_RULES_BracesAndParenthesis;
		} else {
			return BLOCK_RULES_Braces;
		}
	}
	
	/* ------------------------------------- */
	
	protected void smartIndentAfterNewLine(IDocument doc, DocumentCommand cmd) throws BadLocationException {
		if(cmd.length > 0 || cmd.text == null)
			return;
		
		IRegion lineRegion = doc.getLineInformationOfOffset(cmd.offset);
		int lineEnd = getRegionEnd(lineRegion);
		
		int postWsEndPos = TextSourceUtils.findEndOfIndent(docContents, cmd.offset); 
		boolean hasPendingTextAfterEdit = postWsEndPos != lineEnd;
		
		
		BlockHeuristicsScannner bhscanner = createBlockHeuristicsScanner(doc);
		
		int offsetForBalanceCalculation = findOffsetForBalanceCalculation(doc, bhscanner, cmd.offset);
		int lineForBalanceCalculation = doc.getLineOfOffset(cmd.offset);
		
		// Find block balances of preceding text (line start to edit cursor)
		LineIndentResult nli = determineIndent(doc, bhscanner, lineForBalanceCalculation, offsetForBalanceCalculation);
		cmd.text += nli.nextLineIndent;
		
		BlockBalanceResult blockInfo = nli.blockInfo;
		if(blockInfo.unbalancedOpens > 0) {
			if(preferences.closeBraces() && !hasPendingTextAfterEdit){
				
				if(bhscanner.shouldCloseBlock(blockInfo.rightmostUnbalancedBlockOpenOffset)) {
					//close block
					cmd.caretOffset = cmd.offset + cmd.text.length();
					cmd.shiftsCaret = false;
					String delimiter = TextUtilities.getDefaultLineDelimiter(doc);
					char openChar = doc.getChar(blockInfo.rightmostUnbalancedBlockOpenOffset);
					char closeChar = bhscanner.getClosingPeer(openChar); 
					cmd.text += delimiter + addIndent(nli.editLineIndent, blockInfo.unbalancedOpens - 1) + closeChar;
				}
			}
			return;
		}
	}
	
	protected int findOffsetForBalanceCalculation(IDocument doc, BlockHeuristicsScannner bhscanner, int offset) {
		while(offset < doc.getLength()) {
			char ch;
			try {
				ch = doc.getChar(offset);
			} catch(BadLocationException e) {
				break;
			}
			if(!bhscanner.isClosingBrace(ch)) {
				break;
			}
			offset++;
		}
		return offset;
	}
	
	public static class LineIndentResult {
		String editLineIndent;
		String nextLineIndent;
		BlockBalanceResult blockInfo;
		public LineIndentResult(String editLineIndent, String nextLineIndent, BlockBalanceResult blockInfo) {
			this.editLineIndent = editLineIndent;
			this.nextLineIndent = nextLineIndent;
			this.blockInfo = blockInfo;
		}
	}
	
	protected final LineIndentResult determineIndent(IDocument doc, BlockHeuristicsScannner bhscanner, int line) 
			throws BadLocationException {
		IRegion lineRegion = doc.getLineInformation(line);
		return determineIndent(doc, bhscanner, line, getRegionEnd(lineRegion));
	}
	
	protected LineIndentResult determineIndent(IDocument doc, BlockHeuristicsScannner bhscanner, final int line,
			final int editOffset) throws BadLocationException {
		
		IRegion lineRegion = doc.getLineInformation(line);
		final int lineStart = lineRegion.getOffset();
		assertTrue(lineStart <= editOffset && editOffset <= getRegionEnd(lineRegion));
		
		ITypedRegion partition = bhscanner.getPartition(lineStart);
		if(partitionIsIgnoredForLineIndentString(editOffset, partition)) {
			if(line == 0) {
				// empty/zero block balance
				return new LineIndentResult("", "", new BlockBalanceResult());
			} else {
				return determineIndent(doc, bhscanner, line-1);
			}
		}
		
		BlockBalanceResult blockInfo = bhscanner.calculateBlockBalances(lineStart, editOffset);
		
		if(blockInfo.unbalancedOpens == 0 && blockInfo.unbalancedCloses > 0) {
			int blockStartOffset = bhscanner.findBlockStart(blockInfo.rightmostUnbalancedBlockCloseOffset);
			assertTrue(doc.getLineOfOffset(blockStartOffset) <= doc.getLineOfOffset(lineStart));
			
			
			String startLineIndent = getLineIndentForOffset(blockStartOffset);
			
			// Now calculate the balance for the block start line, before the block start
			int lineOffset = TextSourceUtils.findLineStartForOffset(docContents, blockStartOffset);
			BlockBalanceResult blockStartInfo = bhscanner.calculateBlockBalances(lineOffset, blockStartOffset);
			
			// Add the indent of the start line, plus the unbalanced opens there
			String newLineIndent = addIndent(startLineIndent, blockStartInfo.unbalancedOpens); 
			return new LineIndentResult(null, newLineIndent, blockInfo);
		}
		
		// The indent string to be added to the new line
		String lineIndent = getLineIndentForLineStart(lineStart, editOffset);
		if(blockInfo.unbalancedOpens == 0 && blockInfo.unbalancedCloses == 0) {
			return new LineIndentResult(null, lineIndent, blockInfo); // finished
		}
		
		if(blockInfo.unbalancedOpens > 0) {
			String newLineIndent = addIndent(lineIndent, blockInfo.unbalancedOpens);
			// cache lineIndent so as to not recalculate
			return new LineIndentResult(lineIndent, newLineIndent, blockInfo); // finished
		}
		throw assertUnreachable();
	}
	
	/** Subclasses may override. */
	protected boolean partitionIsIgnoredForLineIndentString(final int editOffset, ITypedRegion partition) {
		return partitionTypeIsIgnoredForLineIndentString(partition) && editOffset <= getRegionEnd(partition);
	}
	
	/** Subclasses may override. */
	protected boolean partitionTypeIsIgnoredForLineIndentString(ITypedRegion partition) {
		return !partition.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
	}
	
	protected String addIndent(String indentStr, int indentDelta) {
		return indentStr + TextSourceUtils.stringNTimes(indentUnit, indentDelta);
	}
	
	/* ------------------------------------- */
	
	protected boolean smartDeIndentAfterDeletion(IDocument doc, DocumentCommand cmd) throws BadLocationException {
		if(!preferences.isSmartDeIndent())
			return false;
		
		if(!cmd.text.isEmpty())
			return false;
		
		IRegion lineRegion = doc.getLineInformationOfOffset(cmd.offset);
		int lineEnd = getRegionEnd(lineRegion);
		int line = doc.getLineOfOffset(cmd.offset);
		
		
		// Delete at beginning of NL
		if(cmd.offset == lineEnd && lengthMatchesLineDelimiter(cmd.length, doc.getLineDelimiter(line))) {
			if(keyWasBackspace())
				return false; // Only Delete key should trigger this edit
			
			int indentLine = line+1;
			if(indentLine < doc.getNumberOfLines()) {
				assertTrue(doc.getLineInformation(indentLine).getOffset() == cmd.offset + cmd.length);
				
				IRegion indentLineRegion = doc.getLineInformation(indentLine);
				int indentEnd = findEndOfIndent(indentLineRegion.getOffset());
				String deletableIndentStr = calculateDeletableIndent(doc, indentLine, indentEnd);
				if(equalsDocumentString(deletableIndentStr, doc, indentLineRegion)) {
					cmd.length += deletableIndentStr.length();
					return true;
				}
			}
			return false;
		}
		
		// Backspace at end of indent case
		if(cmd.length == 1 && isIndentWhiteSpace(doc.getChar(cmd.offset)) && line > 0) {
			if(keyWasDelete())
				return false; // Only Backspace key should trigger this
			
			
			IRegion indentLineRegion = lineRegion;
			int indentLine = line;
			int indentEnd = findEndOfIndent(indentLineRegion.getOffset());
			if(cmd.offset < indentEnd) {
				// potentially true
				
				String deletableIndentStr = calculateDeletableIndent(doc, indentLine, indentEnd);
				if(equalsDocumentString(deletableIndentStr, doc, indentLineRegion)) {
					int acceptedIndentEnd = indentLineRegion.getOffset() + deletableIndentStr.length();
					if(cmd.offset == acceptedIndentEnd-1) {
						int lineDelimLen = doc.getLineDelimiter(line-1).length();
						cmd.offset = indentLineRegion.getOffset() - lineDelimLen;
						cmd.length = lineDelimLen + deletableIndentStr.length();
						return true;
					}
				}
			}
			return false;
		}
		
		return false;
	}
	
	protected static boolean lengthMatchesLineDelimiter(int length, String lineDelimiter) {
		return lineDelimiter != null && length == lineDelimiter.length();
	}
	
	protected boolean isIndentWhiteSpace(char ch) throws BadLocationException {
		return ch == ' ' || ch == '\t';
	}
	
	protected String calculateDeletableIndent(IDocument doc, int indentedLine, int indentEnd)
			throws BadLocationException {
		IRegion indentedLineRegion = doc.getLineInformation(indentedLine);

		String expectedIndentStr = determineExpectedIndent(doc, indentedLine-1);
		int indentLength = indentEnd - indentedLineRegion.getOffset(); 
		if(indentLength < expectedIndentStr.length()) {
			// cap expected length
			expectedIndentStr = expectedIndentStr.substring(0, indentLength);
		}
		return expectedIndentStr;
	}
	
	protected String determineExpectedIndent(IDocument doc, int line) throws BadLocationException {
		BlockHeuristicsScannner bhscanner = createBlockHeuristicsScanner(doc);
		LineIndentResult nli = determineIndent(doc, bhscanner, line);
		String expectedIndentStr = nli.nextLineIndent;
		return expectedIndentStr;
	}
	
	/* ------------------------------------- */
	
	protected void smartIndentOnKeypress(IDocument doc, DocumentCommand cmd) {
		assertTrue(cmd.text.length() == 1);
		
		int offset = cmd.offset;
		
		int lineStart = TextSourceUtils.findLineStartForOffset(docContents, offset);
		String beforeCursor = docContents.substring(lineStart, offset);
		
		if(!beforeCursor.trim().isEmpty()) {
			return;
		}
		
		if(!isCloseSymbol(cmd.text)) {
			return;
		}
		char closeBrace = cmd.text.charAt(0);
		
		BlockHeuristicsScannner bhScanner = createBlockHeuristicsScanner(doc);
		int blockStartOffset = bhScanner.findBlockStart(offset, closeBrace);
		
		// Replace current indent
		cmd.offset = lineStart;
		cmd.length = beforeCursor.length();
		// With indent of block-start line
		String startLineIndent = getLineIndentForOffset(blockStartOffset);
		cmd.text = startLineIndent + closeBrace;
	}
	
	protected boolean isCloseSymbol(String string) {
		for (BlockTokenRule blockTokenRule : getBlockRules()) {
			if(string.charAt(0) == blockTokenRule.close) {
				return true;
			}
		}
		return false;
	}
	
}