/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
 
package org.eclipse.ant.internal.ui.editor;
import org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter;
import org.eclipse.ant.internal.ui.editor.model.AntElementNode;
import org.eclipse.ant.internal.ui.editor.outline.AntModel;
import org.eclipse.ant.internal.ui.model.AntUIPlugin;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultAutoIndentStrategy;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;

/**
 * Auto indent strategy for Ant build files
 */
public class AntAutoIndentStrategy extends DefaultAutoIndentStrategy {
	
	private AntModel fModel;
	private int fAccumulatedChange= 0;
	
	public AntAutoIndentStrategy(AntModel model) {
		fModel= model;
	}
	
	/**
	 * Sets the indentation based on the Ant element node that contains the offset
	 * of the document command.
	 *
	 * @param d the document to work on
	 * @param c the command to deal with
	 */
	private synchronized void autoIndentAfterNewLine(IDocument d, DocumentCommand c) {
		
		if (c.offset == -1 || d.getLength() == 0 || fModel.getProjectNode(false) == null) {
			return;
		}
		
		int position= (c.offset == d.getLength() ? c.offset  - 1 : c.offset);
		AntElementNode node= fModel.getProjectNode(false).getNode(position - fAccumulatedChange);
		if (node == null) {
			return;
		}
		
		try {
			StringBuffer correct= XmlDocumentFormatter.getLeadingWhitespace(node.getOffset(), d);
			if (!nextNodeIsEndTag(c.offset, d)) {
				correct.append(XmlDocumentFormatter.createIndent());
			}
			StringBuffer buf= new StringBuffer(c.text);
			buf.append(correct);
			fAccumulatedChange+= buf.length();
			
			int line= d.getLineOfOffset(position);
			IRegion reg= d.getLineInformation(line);
			int lineEnd= reg.getOffset() + reg.getLength();
			int contentStart= findEndOfWhiteSpace(d, c.offset, lineEnd);
			
			c.length=  Math.max(contentStart - c.offset, 0);
			c.caretOffset= c.offset + buf.length();
			c.shiftsCaret= false;
			c.text= buf.toString();
	
		} catch (BadLocationException e) {
			AntUIPlugin.log(e);
		}
	}
	
	private boolean nextNodeIsEndTag(int offset, IDocument document) {
		if (offset + 1 > document.getLength()) {
			return false;
		}
		try {
			IRegion lineRegion= document.getLineInformationOfOffset(offset);
			offset= findEndOfWhiteSpace(document, offset, lineRegion.getOffset() + lineRegion.getLength());
			String nextChars= document.get(offset, 2).trim();
			if ("</".equals(nextChars) || "/>".equals(nextChars)) { //$NON-NLS-1$ //$NON-NLS-2$
				return true;
			}
		} catch (BadLocationException e) {
		}
		return false;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
	 */
	public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
		
		if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text)) {
			autoIndentAfterNewLine(d, c);
		} else if (c.text.length() > 1) {
			smartPaste(d, c);
		}
	}
	
	 private boolean isLineDelimiter(IDocument document, String text) {
		String[] delimiters= document.getLegalLineDelimiters();
		if (delimiters != null)
			return TextUtilities.equals(delimiters, text) > -1;
		return false;
	}
	
	public synchronized void reconciled() {
		fAccumulatedChange= 0;
	}
	
	private void smartPaste(IDocument document, DocumentCommand command) {
		try {
			if (command.offset == -1 || document.getLength() == 0 || fModel.getProjectNode(false) == null) {
				return;
			}
			String origChange= command.text;
			int position= (command.offset == document.getLength() ? command.offset  - 1 : command.offset);
			AntElementNode node= fModel.getProjectNode(false).getNode(position - fAccumulatedChange);
			if (node == null) {
				return;
			}
			
			// eat any WS before the insertion to the beginning of the line
			int firstLine= 1; // don't format the first line if it has other content before it
			IRegion line= document.getLineInformationOfOffset(command.offset);
			String notSelected= document.get(line.getOffset(), command.offset - line.getOffset());
			if (notSelected.trim().length() == 0) {
				command.length += notSelected.length();
				command.offset= line.getOffset();
				firstLine= 0;
			}
			
			// handle the indentation computation inside a temporary document
			Document temp= new Document(command.text);
			
			// indent the first and second line
			// compute the relative indentation difference from the second line
			// (as the first might be partially selected) and use the value to
			// indent all other lines.
			boolean isIndentDetected= false;
			StringBuffer addition= new StringBuffer();
			int insertLength= 0;
			int lines= temp.getNumberOfLines();
			for (int l= firstLine; l < lines; l++) { // we don't change the number of lines while adding indents
				
				IRegion r= temp.getLineInformation(l);
				int lineOffset= r.getOffset();
				int lineLength= r.getLength();
				
				if (lineLength == 0) { // don't modify empty lines
					continue;
				}
				
				if (!isIndentDetected){
					
					// indent the first pasted line
					StringBuffer current= XmlDocumentFormatter.getLeadingWhitespace(lineOffset, temp);
					StringBuffer correct= XmlDocumentFormatter.getLeadingWhitespace(node.getOffset(), document);
					correct.append(XmlDocumentFormatter.createIndent());
					
					insertLength= subtractIndent(correct, current, addition);
					isIndentDetected= true;
				}
				
				// relatively indent all pasted lines 
				if (insertLength > 0) {
					addIndent(temp, l, addition);
				} else if (insertLength < 0) {
					cutIndent(temp, l, -insertLength);
				}	
			}
			
			// modify the command
			if (!origChange.equals(temp.get())) {
				fAccumulatedChange+=  temp.getLength();
				command.text= temp.get();
			}
			
		} catch (BadLocationException e) {
			AntUIPlugin.log(e);
		}
	}
	
	/**
	 * Indents line <code>line</code> in <code>document</code> with <code>indent</code>.
	 * Leaves leading comment signs alone.
	 * 
	 * @param document the document
	 * @param line the line
	 * @param indent the indentation to insert
	 * @throws BadLocationException on concurrent document modification
	 */
	private void addIndent(Document document, int line, CharSequence indent) throws BadLocationException {
		IRegion region= document.getLineInformation(line);
		int insert= region.getOffset();
		
		// insert indent
		document.replace(insert, 0, indent.toString());
	}
	
	/**
	 * Cuts the visual equivalent of <code>toDelete</code> characters out of the
	 * indentation of line <code>line</code> in <code>document</code>.
	 * 
	 * @param document the document
	 * @param line the line
	 * @param toDelete the number of space equivalents to delete.
	 * @throws BadLocationException on concurrent document modification
	 */
	private void cutIndent(Document document, int line, int toDelete) throws BadLocationException {
		IRegion region= document.getLineInformation(line);
		int from= region.getOffset();
		int endOffset= region.getOffset() + region.getLength();
		
		int to= from;
		while (toDelete > 0 && to < endOffset) {
			char ch= document.getChar(to);
			if (!Character.isWhitespace(ch))
				break;
			toDelete -= computeVisualLength(ch);
			if (toDelete >= 0) {
				to++;
			} else {
				break;
			}
		}
		
		document.replace(from, to - from, null);
	}
	
	/**
	 * Returns the visual length of a given character taking into
	 * account the visual tabulator length.
	 * 
	 * @param ch the character to measure
	 * @return the visual length of <code>ch</code>
	 */
	private int computeVisualLength(char ch) {
		if (ch == '\t') {
			return getVisualTabLengthPreference();
		} 
			
		return 1;
	}
	
	/**
	 * Returns the visual length of a given <code>CharSequence</code> taking into
	 * account the visual tabulator length.
	 * 
	 * @param seq the string to measure
	 * @return the visual length of <code>seq</code>
	 */
	private int computeVisualLength(CharSequence seq) {
		int size= 0;
		int tablen= getVisualTabLengthPreference();
		
		for (int i= 0; i < seq.length(); i++) {
			char ch= seq.charAt(i);
			if (ch == '\t') {
				size += tablen - size % tablen;
			} else {
				size++;
			}
		}
		return size;
	}
	
	/**
	 * Computes the difference of two indentations and returns the difference in
	 * length of current and correct. If the return value is positive, <code>addition</code>
	 * is initialized with a substring of that length of <code>correct</code>.
	 * 
	 * @param correct the correct indentation
	 * @param current the current indentation (might contain non-whitespace)
	 * @param difference a string buffer - if the return value is positive, it will be cleared and set to the substring of <code>current</code> of that length
	 * @return the difference in length of <code>correct</code> and <code>current</code> 
	 */
	private int subtractIndent(CharSequence correct, CharSequence current, StringBuffer difference) {
		int c1= computeVisualLength(correct);
		int c2= computeVisualLength(current);
		int diff= c1 - c2;
		if (diff <= 0) {
			return diff;
		}
		
		difference.setLength(0);
		int len= 0, i= 0;
		while (len < diff) {
			char c= correct.charAt(i++);
			difference.append(c);
			len += computeVisualLength(c);
		}
		
		return diff;
	}
	
	/**
	 * The preference setting for the visual tabulator display.
	 *  
	 * @return the number of spaces displayed for a tabulator in the editor
	 */
	private int getVisualTabLengthPreference() {
		return AntUIPlugin.getDefault().getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
	}
}