/*******************************************************************************
 * Copyright (c) 2000, 2006 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.template.contentassist;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.RangeMarker;
import org.eclipse.text.edits.TextEdit;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.jface.dialogs.MessageDialog;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.templates.GlobalTemplateVariables;
import org.eclipse.jface.text.templates.Template;
import org.eclipse.jface.text.templates.TemplateBuffer;
import org.eclipse.jface.text.templates.TemplateException;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ITrackedNodePosition;

import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.GenericVisitor;
import org.eclipse.jdt.internal.corext.template.java.CompilationUnitContext;
import org.eclipse.jdt.internal.corext.template.java.CompilationUnitContextType;
import org.eclipse.jdt.internal.corext.template.java.JavaContextType;

import org.eclipse.jdt.ui.text.java.IInvocationContext;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.text.correction.AssistContext;
import org.eclipse.jdt.internal.ui.text.correction.SurroundWith;

public class SurroundWithTemplateProposal extends TemplateProposal {
	
	private static class SurroundWithTemplate extends SurroundWith {
		
		private static final String $_LINE_SELECTION= "${" + GlobalTemplateVariables.LineSelection.NAME + "}"; //$NON-NLS-1$ //$NON-NLS-2$
		
		private final Template fTemplate;
		private final IJavaProject fCurrentProject;
		private ITrackedNodePosition fNewBodyPosition;
		private ASTNode fTemplateNode;
		
		public SurroundWithTemplate(IInvocationContext context, Statement[] selectedNodes, Template template) {
			super(context.getASTRoot(), selectedNodes);
			fTemplate= template;
			fCurrentProject= context.getCompilationUnit().getJavaProject();
		}
		
		public ITrackedNodePosition getNewBodyPosition() {
			return fNewBodyPosition;
		}
		
		/**
		 * Generate a new code skeleton.
		 * @param newBody The new body which will be filled with code.
		 * @param rewrite The rewrite to use to change the ast.
		 * @return The root of the new code.
		 */
		protected Statement generateCodeSkeleton(Block newBody, ASTRewrite rewrite) {
			fNewBodyPosition= rewrite.track(newBody);
			return newBody;
		}
		
		protected List getVariableDeclarationReadsInside(Statement[] selectedNodes, int maxVariableId) {
			if (isNewContext())
				return super.getVariableDeclarationReadsInside(selectedNodes, maxVariableId);
			return new ArrayList();
		}
		
		protected boolean isNewContext() {

			final String templateVariableRegEx= "\\$\\{[^\\}]*\\}"; //$NON-NLS-1$
			
			String template= fTemplate.getPattern();
			int currentPosition= template.indexOf($_LINE_SELECTION);
			int insertionPosition= -1;
			while (currentPosition != -1) {
				insertionPosition= currentPosition;
				template= template.replaceFirst(templateVariableRegEx, ""); //$NON-NLS-1$
				currentPosition= template.indexOf($_LINE_SELECTION);
			}
			template= template.replaceAll(templateVariableRegEx, ""); //$NON-NLS-1$

			AST ast= getAst();
			ASTParser parser= ASTParser.newParser(ast.apiLevel());
			parser.setSource(template.toCharArray());
			parser.setProject(fCurrentProject);
			parser.setKind(ASTParser.K_STATEMENTS);
			ASTNode root= parser.createAST(null);
			if (((Block)root).statements().isEmpty()) {
				parser= ASTParser.newParser(ast.apiLevel());
				parser.setSource(template.toCharArray());
				parser.setProject(fCurrentProject);
				parser.setKind(ASTParser.K_EXPRESSION);
				root= parser.createAST(null);
			}
			
			final int lineSelectionPosition= insertionPosition;
			root.accept(new GenericVisitor() {
				public void endVisit(Block node) {
					super.endVisit(node);
					if (fTemplateNode == null && node.getStartPosition() <= lineSelectionPosition && node.getLength() + node.getStartPosition() >= lineSelectionPosition) {
						fTemplateNode= node;
					}
				}
			});
			
			if (fTemplateNode != null && ASTNodes.getParent(fTemplateNode, MethodDeclaration.class) != null) {
				return true;
			}
			
			return false;
		}
		
	}


	private final IRegion fRegion;
	private final ICompilationUnit fCompilationUnit;
	private final CompilationUnitContext fContext;
	private final Template fTemplate;
	private final Statement[] fSelectedStatements;
	private TemplateProposal fProposal;
	private IRegion fSelectedRegion;

	public SurroundWithTemplateProposal(ICompilationUnit compilationUnit, Template template, CompilationUnitContext context, IRegion region, Image image, Statement[] selectedStatements) {
		super(template, context, region, image);
		fCompilationUnit= compilationUnit;
		fTemplate= template;
		fContext= context;
		fRegion= region;
		fSelectedStatements= selectedStatements;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.jdt.internal.ui.text.template.contentassist.TemplateProposal#getAdditionalProposalInfo()
	 */
	public String getPreviewContent() {
		try {
			IDocument document= new Document(fCompilationUnit.getBuffer().getContents());
			CompilationUnitContext context= createNewContext(document);
			
			int offset= context.getCompletionOffset();
			int start= context.getStart();
			int end= context.getEnd();
			IRegion region= new Region(start, end - start);
			
			context.setReadOnly(false);
			TemplateBuffer templateBuffer;
			try {
				templateBuffer= context.evaluate(fTemplate);
			} catch (TemplateException e1) {
				JavaPlugin.log(e1);
				return null;
			}
			
			start= region.getOffset();
			end= region.getOffset() + region.getLength();
			end= Math.max(end, offset);

			String templateString= templateBuffer.getString();
			document.replace(start, end - start, templateString);
			
			return document.get();
			
		} catch (MalformedTreeException e) {
			JavaPlugin.log(e);
		} catch (IllegalArgumentException e) {
			JavaPlugin.log(e);
		} catch (BadLocationException e) {
			JavaPlugin.log(e);
		} catch (CoreException e) {
			JavaPlugin.log(e);
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.internal.ui.text.template.contentassist.TemplateProposal#apply(org.eclipse.jface.text.ITextViewer, char, int, int)
	 */
	public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
		try {
			IDocument document= viewer.getDocument();
			CompilationUnitContext context= createNewContext(document);
			
			int start= context.getStart();
			int end= context.getEnd();
			IRegion region= new Region(start, end - start);

			//Evaluate the template within the new context
			fProposal= new TemplateProposal(fTemplate, context, region, null);
			fProposal.apply(viewer, trigger, stateMask, context.getCompletionOffset());
		} catch (MalformedTreeException e) {
			handleException(viewer, e, fRegion);
		} catch (IllegalArgumentException e) {
			handleException(viewer, e, fRegion);
		} catch (BadLocationException e) {
			handleException(viewer, e, fRegion);
		} catch (CoreException e) {
			handleException(viewer, e, fRegion);
		}
	}
	
	public Point getSelection(IDocument document) {
		if (fSelectedRegion != null) {
			return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
		} else if (fProposal != null) {
			return fProposal.getSelection(document);
		} else {
			return null;
		}
	}
	
	private CompilationUnitContext createNewContext(IDocument document) throws CoreException, BadLocationException {
		AssistContext invocationContext= new AssistContext(fCompilationUnit, fContext.getStart(), fContext.getEnd() - fContext.getStart());
		
		SurroundWithTemplate surroundWith= new SurroundWithTemplate(invocationContext, fSelectedStatements, fTemplate);
		Map options= fCompilationUnit.getJavaProject().getOptions(true);
		MultiTextEdit allEdits= (MultiTextEdit)surroundWith.getRewrite().rewriteAST(document, options);
		removeBlockStartAndEnd(allEdits);
		MultiTextEdit blockFreeEdits= (MultiTextEdit)allEdits.copy();
		removeBlock(blockFreeEdits);
		
		String newSelection= calculateNewSelection(document, surroundWith.getNewBodyPosition(), allEdits);
		
		//Change the document such that it contains the new variable declarations (i.e. with final keyword)
		blockFreeEdits.apply(document);
		
		//The document may have changed and we need a new context
		
		//Find position for template insertion in new document (no leading indentation)
		int replaceOffset= surroundWith.getNewBodyPosition().getStartPosition();
		int offset= document.getLineOffset(document.getLineOfOffset(replaceOffset));
		
		//Create the new context
		CompilationUnitContextType contextType= (CompilationUnitContextType) JavaPlugin.getDefault().getTemplateContextRegistry().getContextType(JavaContextType.NAME);
		CompilationUnitContext context= contextType.createContext(document, offset, replaceOffset - offset, fCompilationUnit);
		context.setVariable("selection", newSelection); //$NON-NLS-1$
		context.setForceEvaluation(true);
		return context;
	}

	private void removeBlockStartAndEnd(MultiTextEdit edits) {
		TextEdit[] children= edits.getChildren();
		int i= 0;
		while (!(children[i] instanceof RangeMarker)) {
			i++;
		}
		i++;
		edits.removeChild(i);
		while (!(children[i] instanceof RangeMarker)) {
			i++;
		}
		children= edits.getChildren();
		TextEdit edit= children[i - 2];
		if (edit instanceof InsertEdit) {
			final InsertEdit insert= (InsertEdit) edit;
			final String text= insert.getText();
			if (text != null && text.startsWith(";")) { //$NON-NLS-1$
				children[i - 2]= new InsertEdit(edit.getOffset(), ";"); //$NON-NLS-1$
				edits.removeChildren();
				edits.addChildren(children);
				return;
			}
		}
		edits.removeChild(i - 2);
	}
	
	private void removeBlock(MultiTextEdit edits) {
		TextEdit[] children= edits.getChildren();
		int i= 0;
		while (!(children[i] instanceof RangeMarker)) {
			i++;
		}
		i++;
		int pos= i;
		while (!(children[i] instanceof RangeMarker)) {
			edits.removeChild(pos);
			i++;
		}
	}
	
	private String calculateNewSelection(IDocument document, ITrackedNodePosition position, MultiTextEdit edits) throws BadLocationException {
		IDocument tmpDocument= new Document(String.copyValueOf(document.get().toCharArray()));
		edits.apply(tmpDocument);
		return tmpDocument.get(position.getStartPosition(), position.getLength());
	}
	
	private void handleException(ITextViewer viewer, Exception e, IRegion region) {
		JavaPlugin.log(e);
		openErrorDialog(viewer.getTextWidget().getShell(), e);
		fSelectedRegion= region;
	}

	private void openErrorDialog(Shell shell, Exception e) {
		MessageDialog.openError(shell, TemplateContentAssistMessages.TemplateEvaluator_error_title, e.getMessage());
	}
	
	/**
	 * {@inheritDoc}
	 */
	public boolean validate(IDocument document, int offset, DocumentEvent event) {
		return false;
	}
}
