/*******************************************************************************
 * 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 net.sourceforge.phpdt.internal.ui.text.template.contentassist;

import net.sourceforge.phpdt.internal.corext.template.php.CompilationUnitContext;
import net.sourceforge.phpdt.internal.corext.template.php.JavaContext;
import net.sourceforge.phpdt.internal.ui.text.java.IPHPCompletionProposal;
import net.sourceforge.phpdt.internal.ui.util.ExceptionHandler;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;
import net.sourceforge.phpeclipse.phpeditor.EditorHighlightingSynchronizer;
import net.sourceforge.phpeclipse.phpeditor.PHPEditor;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.jface.text.templates.DocumentTemplateContext;
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.TemplateContext;
import org.eclipse.jface.text.templates.TemplateException;
import org.eclipse.jface.text.templates.TemplateVariable;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

/**
 * A template proposal.
 */
public class TemplateProposal implements IPHPCompletionProposal,
		ICompletionProposalExtension2, ICompletionProposalExtension3 {

	private final Template fTemplate;

	private final TemplateContext fContext;

	private final Image fImage;

	private IRegion fRegion;

	private int fRelevance;

	private IRegion fSelectedRegion; // initialized by apply()

	private String fDisplayString;

	/**
	 * Creates a template proposal with a template and its context.
	 * 
	 * @param template
	 *            the template
	 * @param context
	 *            the context in which the template was requested
	 * @param region
	 *            the region this proposal applies to
	 * @param image
	 *            the icon of the proposal
	 */
	public TemplateProposal(Template template, TemplateContext context,
			IRegion region, Image image) {
		Assert.isNotNull(template);
		Assert.isNotNull(context);
		Assert.isNotNull(region);

		fTemplate = template;
		fContext = context;
		fImage = image;
		fRegion = region;

		fDisplayString = null;

		if (context instanceof JavaContext) {
			switch (((JavaContext) context).getCharacterBeforeStart()) {
			// high relevance after whitespace
			case ' ':
			case '\r':
			case '\n':
			case '\t':
				fRelevance = 90;
				break;
			default:
				fRelevance = 0;
			}
		} else {
			fRelevance = 90;
		}
	}

	/*
	 * @see ICompletionProposal#apply(IDocument)
	 */
	public final void apply(IDocument document) {
		// not called anymore
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#apply(org.eclipse.jface.text.ITextViewer,
	 *      char, int, int)
	 */
	public void apply(ITextViewer viewer, char trigger, int stateMask,
			int offset) {

		try {

			fContext.setReadOnly(false);
			TemplateBuffer templateBuffer;
			try {
				templateBuffer = fContext.evaluate(fTemplate);
			} catch (TemplateException e1) {
				fSelectedRegion = fRegion;
				return;
			}

			int start = getReplaceOffset();
			int end = getReplaceEndOffset();
			end = Math.max(end, offset);

			// insert template string
			IDocument document = viewer.getDocument();
			String templateString = templateBuffer.getString();
			document.replace(start, end - start, templateString);

			// translate positions
			LinkedModeModel model = new LinkedModeModel();
			TemplateVariable[] variables = templateBuffer.getVariables();

			MultiVariableGuess guess = fContext instanceof CompilationUnitContext ? ((CompilationUnitContext) fContext)
					.getMultiVariableGuess()
					: null;

			boolean hasPositions = false;
			for (int i = 0; i != variables.length; i++) {
				TemplateVariable variable = variables[i];

				if (variable.isUnambiguous())
					continue;

				LinkedPositionGroup group = new LinkedPositionGroup();

				int[] offsets = variable.getOffsets();
				int length = variable.getLength();

				LinkedPosition first;
				if (guess != null && variable instanceof MultiVariable) {
					first = new VariablePosition(document, offsets[0] + start,
							length, guess, (MultiVariable) variable);
					guess.addSlave((VariablePosition) first);
				} else {
					String[] values = variable.getValues();
					ICompletionProposal[] proposals = new ICompletionProposal[values.length];
					for (int j = 0; j < values.length; j++) {
						ensurePositionCategoryInstalled(document, model);
						Position pos = new Position(offsets[0] + start, length);
						document.addPosition(getCategory(), pos);
						proposals[j] = new PositionBasedCompletionProposal(
								values[j], pos, length);
					}

					if (proposals.length > 1)
						first = new ProposalPosition(document, offsets[0]
								+ start, length, proposals);
					else
						first = new LinkedPosition(document,
								offsets[0] + start, length);
				}

				for (int j = 0; j != offsets.length; j++)
					if (j == 0)
						group.addPosition(first);
					else
						group.addPosition(new LinkedPosition(document,
								offsets[j] + start, length));

				model.addGroup(group);
				hasPositions = true;
			}

			if (hasPositions) {
				model.forceInstall();
				PHPEditor editor = getJavaEditor();
				if (editor != null) {
					model
							.addLinkingListener(new EditorHighlightingSynchronizer(
									editor));
				}

				LinkedModeUI ui = new EditorLinkedModeUI(model, viewer);
				ui.setExitPosition(viewer, getCaretOffset(templateBuffer)
						+ start, 0, Integer.MAX_VALUE);
				ui.enter();

				fSelectedRegion = ui.getSelectedRegion();
			} else
				fSelectedRegion = new Region(getCaretOffset(templateBuffer)
						+ start, 0);

		} catch (BadLocationException e) {
			PHPeclipsePlugin.log(e);
			openErrorDialog(viewer.getTextWidget().getShell(), e);
			fSelectedRegion = fRegion;
		} catch (BadPositionCategoryException e) {
			PHPeclipsePlugin.log(e);
			openErrorDialog(viewer.getTextWidget().getShell(), e);
			fSelectedRegion = fRegion;
		}

	}

	/**
	 * Returns the currently active java editor, or <code>null</code> if it
	 * cannot be determined.
	 * 
	 * @return the currently active java editor, or <code>null</code>
	 */
	private PHPEditor getJavaEditor() {
		IEditorPart part = PHPeclipsePlugin.getActivePage().getActiveEditor();
		if (part instanceof PHPEditor)
			return (PHPEditor) part;
		else
			return null;
	}

	/**
	 * Returns the offset of the range in the document that will be replaced by
	 * applying this template.
	 * 
	 * @return the offset of the range in the document that will be replaced by
	 *         applying this template
	 */
	private int getReplaceOffset() {
		int start;
		if (fContext instanceof DocumentTemplateContext) {
			DocumentTemplateContext docContext = (DocumentTemplateContext) fContext;
			start = docContext.getStart();
		} else {
			start = fRegion.getOffset();
		}
		return start;
	}

	/**
	 * Returns the end offset of the range in the document that will be replaced
	 * by applying this template.
	 * 
	 * @return the end offset of the range in the document that will be replaced
	 *         by applying this template
	 */
	private int getReplaceEndOffset() {
		int end;
		if (fContext instanceof DocumentTemplateContext) {
			DocumentTemplateContext docContext = (DocumentTemplateContext) fContext;
			end = docContext.getEnd();
		} else {
			end = fRegion.getOffset() + fRegion.getLength();
		}
		return end;
	}

	private void ensurePositionCategoryInstalled(final IDocument document,
			LinkedModeModel model) {
		if (!document.containsPositionCategory(getCategory())) {
			document.addPositionCategory(getCategory());
			final InclusivePositionUpdater updater = new InclusivePositionUpdater(
					getCategory());
			document.addPositionUpdater(updater);

			model.addLinkingListener(new ILinkedModeListener() {

				/*
				 * @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel,
				 *      int)
				 */
				public void left(LinkedModeModel environment, int flags) {
					try {
						document.removePositionCategory(getCategory());
					} catch (BadPositionCategoryException e) {
						// ignore
					}
					document.removePositionUpdater(updater);
				}

				public void suspend(LinkedModeModel environment) {
				}

				public void resume(LinkedModeModel environment, int flags) {
				}
			});
		}
	}

	private String getCategory() {
		return "TemplateProposalCategory_" + toString(); //$NON-NLS-1$
	}

	private int getCaretOffset(TemplateBuffer buffer) {

		TemplateVariable[] variables = buffer.getVariables();
		for (int i = 0; i != variables.length; i++) {
			TemplateVariable variable = variables[i];
			if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME))
				return variable.getOffsets()[0];
		}

		return buffer.getString().length();
	}

	/*
	 * @see ICompletionProposal#getSelection(IDocument)
	 */
	public Point getSelection(IDocument document) {
		return new Point(fSelectedRegion.getOffset(), fSelectedRegion
				.getLength());
	}

	/*
	 * @see ICompletionProposal#getAdditionalProposalInfo()
	 */
	public String getAdditionalProposalInfo() {
		try {
			fContext.setReadOnly(true);
			TemplateBuffer templateBuffer;
			try {
				templateBuffer = fContext.evaluate(fTemplate);
			} catch (TemplateException e1) {
				return null;
			}

			return templateBuffer.getString();

		} catch (BadLocationException e) {
			handleException(PHPeclipsePlugin.getActiveWorkbenchShell(),
					new CoreException(new Status(IStatus.ERROR,
							PHPeclipsePlugin.getPluginId(), IStatus.OK, "", e))); //$NON-NLS-1$
			return null;
		}
	}

	/*
	 * @see ICompletionProposal#getDisplayString()
	 */
	public String getDisplayString() {
		if (fDisplayString == null) {
			fDisplayString = fTemplate.getName()
					+ TemplateContentAssistMessages
							.getString("TemplateProposal.delimiter") + fTemplate.getDescription(); //$NON-NLS-1$
		}
		return fDisplayString;
	}

	public void setDisplayString(String displayString) {
		fDisplayString = displayString;
	}

	/*
	 * @see ICompletionProposal#getImage()
	 */
	public Image getImage() {
		return fImage;
	}

	/*
	 * @see ICompletionProposal#getContextInformation()
	 */
	public IContextInformation getContextInformation() {
		return null;
	}

	private void openErrorDialog(Shell shell, Exception e) {
		MessageDialog.openError(shell, TemplateContentAssistMessages
				.getString("TemplateEvaluator.error.title"), e.getMessage()); //$NON-NLS-1$
	}

	private void handleException(Shell shell, CoreException e) {
		ExceptionHandler.handle(e, shell, TemplateContentAssistMessages
				.getString("TemplateEvaluator.error.title"), null); //$NON-NLS-1$
	}

	/*
	 * @see IJavaCompletionProposal#getRelevance()
	 */
	public int getRelevance() {
		return fRelevance;
	}

	public void setRelevance(int relevance) {
		fRelevance = relevance;
	}

	public Template getTemplate() {
		return fTemplate;
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getInformationControlCreator()
	 */
	public IInformationControlCreator getInformationControlCreator() {
		return new TemplateInformationControlCreator();
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#selected(org.eclipse.jface.text.ITextViewer,
	 *      boolean)
	 */
	public void selected(ITextViewer viewer, boolean smartToggle) {
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#unselected(org.eclipse.jface.text.ITextViewer)
	 */
	public void unselected(ITextViewer viewer) {
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument,
	 *      int, org.eclipse.jface.text.DocumentEvent)
	 */
	public boolean validate(IDocument document, int offset, DocumentEvent event) {
		try {
			int replaceOffset = getReplaceOffset();
			if (offset >= replaceOffset) {
				String content = document.get(replaceOffset, offset
						- replaceOffset);
				return fTemplate.getName().startsWith(content);
			}
		} catch (BadLocationException e) {
			// concurrent modification - ignore
		}
		return false;
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementString()
	 */
	public CharSequence getPrefixCompletionText(IDocument document,
			int completionOffset) {
		return fTemplate.getName();
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementOffset()
	 */
	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
		return getReplaceOffset();
	}
}
