/*******************************************************************************
 * Copyright (c) 2004, 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:
 *     IBM Rational Software - Initial API and implementation
 *     Jens Elmenthaler - http://bugs.eclipse.org/173458 (camel case completion)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.text.contentassist;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension2;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
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.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.text.ICCompletionProposal;

import org.eclipse.cdt.internal.core.parser.util.ContentAssistMatcherFactory;

import org.eclipse.cdt.internal.ui.text.CTextTools;


public class CCompletionProposal implements ICCompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension2, ICompletionProposalExtension3 {
	private String fDisplayString;
    private String fIdString;
	private String fReplacementString;
	private int fReplacementOffset;
	private int fReplacementLength;
	private int fCursorPosition;
	private Image fImage;
	private IContextInformation fContextInformation;
	private int fContextInformationPosition;
	private String fProposalInfo;
	private char[] fTriggerCharacters;
	protected boolean fToggleEating;
	protected ITextViewer fTextViewer;	
	
	private int fRelevance;
	private StyleRange fRememberedStyleRange;

	/**
	 * Creates a new completion proposal. All fields are initialized based on the provided information.
	 *
	 * @param replacementString the actual string to be inserted into the document
	 * @param replacementOffset the offset of the text to be replaced
	 * @param replacementLength the length of the text to be replaced
	 * @param image the image to display for this proposal
	 * @param displayString the string to be displayed for the proposal
	 * If set to {@code null}, the replacement string will be taken as display string.
	 */
	public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, int relevance) {
        this(replacementString, replacementOffset, replacementLength, image, displayString, null, relevance, null);
	}

    /**
     * Creates a new completion proposal. All fields are initialized based on the provided information.
     *
     * @param replacementString the actual string to be inserted into the document
     * @param replacementOffset the offset of the text to be replaced
     * @param replacementLength the length of the text to be replaced
     * @param image the image to display for this proposal
     * @param displayString the string to be displayed for the proposal
     * @param viewer the text viewer for which this proposal is computed, may be {@code null}
     * If set to {@code null}, the replacement string will be taken as display string.
     */
    public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, int relevance, ITextViewer viewer) {
        this(replacementString, replacementOffset, replacementLength, image, displayString, null, relevance, viewer);
    }

    /**
     * Creates a new completion proposal. All fields are initialized based on the provided information.
     *
     * @param replacementString the actual string to be inserted into the document
     * @param replacementOffset the offset of the text to be replaced
     * @param replacementLength the length of the text to be replaced
     * @param image the image to display for this proposal
     * @param displayString the string to be displayed for the proposal
     * @param idString the string to be uniquely identify this proposal
     * @param viewer the text viewer for which this proposal is computed, may be {@code null}
     * If set to {@code null}, the replacement string will be taken as display string.
     */
    public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, String idString, int relevance, ITextViewer viewer) {
        Assert.isNotNull(replacementString);
		Assert.isTrue(replacementOffset >= 0);
		Assert.isTrue(replacementLength >= 0);
				
		fReplacementString = replacementString;
		fReplacementOffset = replacementOffset;
		fReplacementLength = replacementLength;
		fImage = image;
        fRelevance = relevance;
        fTextViewer = viewer;
        
		fDisplayString = displayString != null ? displayString : replacementString;
        fIdString = idString != null ? idString : displayString;

		fCursorPosition = replacementString.length();
	
		fContextInformation = null;
		fContextInformationPosition = -1;
		fTriggerCharacters = null;
		fProposalInfo = null;
	}
	
	/**
	 * Sets the context information.
	 * @param contextInformation The context information associated with this proposal
	 */
	public void setContextInformation(IContextInformation contextInformation) {
		fContextInformation= contextInformation;
		fContextInformationPosition= (fContextInformation != null ? fCursorPosition : -1);
	}
	
	/**
	 * Sets the trigger characters.
	 * @param triggerCharacters The set of characters which can trigger the application of this completion proposal
	 */
	public void setTriggerCharacters(char[] triggerCharacters) {
		fTriggerCharacters= triggerCharacters;
	}
	
	/**
	 * Sets the proposal info.
	 * @param proposalInfo The additional information associated with this proposal or {@code null}
	 */
	public void setAdditionalProposalInfo(String proposalInfo) {
		fProposalInfo= proposalInfo;
	}
	
	/**
	 * Sets the cursor position relative to the insertion offset. By default this is the length of the completion string
	 * (Cursor positioned after the completion)
	 * @param cursorPosition The cursorPosition to set
	 */
	public void setCursorPosition(int cursorPosition) {
		Assert.isTrue(cursorPosition >= 0);
		fCursorPosition= cursorPosition;
		fContextInformationPosition= fContextInformation != null ? fCursorPosition : -1;
	}
	
	@Override
	public void apply(IDocument document, char trigger, int offset) {
		try {
			// patch replacement length
			int delta= offset - (fReplacementOffset + fReplacementLength);
			if (delta > 0)
				fReplacementLength += delta;
			
			String string;
			if (trigger == (char) 0) {
				string= fReplacementString;
			} else {
				StringBuilder buffer= new StringBuilder(fReplacementString);

				// fix for PR #5533. Assumes that no eating takes place.
				if ((fCursorPosition > 0 && fCursorPosition <= buffer.length() && buffer.charAt(fCursorPosition - 1) != trigger)) {
					buffer.insert(fCursorPosition, trigger);
					++fCursorPosition;
				}
				
				string= buffer.toString();
			}

			// reference position just at the end of the document change.
			int referenceOffset= fReplacementOffset + fReplacementLength;
			final ReferenceTracker referenceTracker= new ReferenceTracker();
			referenceTracker.preReplace(document, referenceOffset);

			replace(document, fReplacementOffset, fReplacementLength, string);

			referenceOffset= referenceTracker.postReplace(document);			
			fReplacementOffset= referenceOffset - (string == null ? 0 : string.length());

			if (fTextViewer != null && string != null) {
				int index= string.indexOf("()"); //$NON-NLS-1$
				if (index != -1 && index + 1 == fCursorPosition) {
					int newOffset= fReplacementOffset + fCursorPosition;
					
					LinkedPositionGroup group= new LinkedPositionGroup();
					group.addPosition(new LinkedPosition(document, newOffset, 0, LinkedPositionGroup.NO_STOP));
					
					LinkedModeModel model= new LinkedModeModel();
					model.addGroup(group);
					model.forceInstall();
					
					LinkedModeUI ui= new EditorLinkedModeUI(model, fTextViewer);
					ui.setSimpleMode(true);
					ui.setExitPolicy(new ExitPolicy(')'));
					ui.setExitPosition(fTextViewer, newOffset + 1, 0, Integer.MAX_VALUE);
					ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
					ui.enter();
				}
			}
		} catch (BadLocationException x) {
			// Ignore
		}		
	}

	/**
	 * A class to simplify tracking a reference position in a document. 
	 */
	private static final class ReferenceTracker {
		/** The reference position category name. */
		private static final String CATEGORY= "reference_position"; //$NON-NLS-1$
		/** The position updater of the reference position. */
		private final IPositionUpdater fPositionUpdater= new DefaultPositionUpdater(CATEGORY);
		/** The reference position. */
		private final Position fPosition= new Position(0);
		
		/**
		 * Called before document changes occur. It must be followed by a call to postReplace().
		 * 
		 * @param document the document on which to track the reference position.
		 */
		public void preReplace(IDocument document, int offset) throws BadLocationException {
			fPosition.setOffset(offset);
			try {
				document.addPositionCategory(CATEGORY);
				document.addPositionUpdater(fPositionUpdater);
				document.addPosition(CATEGORY, fPosition);
			} catch (BadPositionCategoryException e) {
				// Should not happen
				CUIPlugin.log(e);
			}
		}
	
		/**
		 * Called after the document changed occured. It must be preceded by a call to preReplace().
		 * 
		 * @param document the document on which to track the reference position.
		 */
		public int postReplace(IDocument document) {
			try {
				document.removePosition(CATEGORY, fPosition);
				document.removePositionUpdater(fPositionUpdater);
				document.removePositionCategory(CATEGORY);
			} catch (BadPositionCategoryException e) {
				// Should not happen
				CUIPlugin.log(e);
			}
			return fPosition.getOffset();
		}
	}	
	
	protected static class ExitPolicy implements IExitPolicy {
		final char fExitCharacter;
		
		public ExitPolicy(char exitCharacter) {
			fExitCharacter= exitCharacter;
		}

		@Override
		public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) {
			if (event.character == fExitCharacter) {
				if (environment.anyPositionContains(offset))
					return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
				
				return new ExitFlags(ILinkedModeListener.UPDATE_CARET, true);
			}	
			
			switch (event.character) {			
			case ';':
				return new ExitFlags(ILinkedModeListener.NONE, true);
								
			default:
				return null;
			}						
		}
	}	
	
	// #6410 - File unchanged but dirtied by code assist
	private void replace(IDocument document, int offset, int length, String string) throws BadLocationException {
		if (!document.get(offset, length).equals(string))
			document.replace(offset, length, string);
	}

	/*
	 * @see ICompletionProposal#apply
	 */
	@Override
	public void apply(IDocument document) {
		apply(document, (char) 0, fReplacementOffset + fReplacementLength);
	}
	
	@Override
	public Point getSelection(IDocument document) {
		return new Point(fReplacementOffset + fCursorPosition, 0);
	}

	@Override
	public IContextInformation getContextInformation() {
		return fContextInformation;
	}

	@Override
	public Image getImage() {
		return fImage;
	}

	@Override
	public String getDisplayString() {
		return fDisplayString;
	}

    /**
     * This method is used by the comparator to compare proposals. It ignores the return type of a function.
     * 
     * @return the string representing the display name without the return type (if any).
     */
    @Override
	public String getIdString() {
        return fIdString;
    }
    
	@Override
	public String getAdditionalProposalInfo() {
		if (fProposalInfo != null) {
			return fProposalInfo;
		}
		return null;
	}
	
	@Override
	public char[] getTriggerCharacters() {
		return fTriggerCharacters;
	}

	@Override
	public int getContextInformationPosition() {
		return fReplacementOffset + fContextInformationPosition;
	}
	
	/**
	 * Gets the replacement offset.
	 * @return Returns a int
	 */
	public int getReplacementOffset() {
		return fReplacementOffset;
	}
	
	@Override
	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
		return getReplacementOffset();
	}

	/**
	 * Sets the replacement offset.
	 * @param replacementOffset The replacement offset to set
	 */
	public void setReplacementOffset(int replacementOffset) {
		Assert.isTrue(replacementOffset >= 0);
		fReplacementOffset= replacementOffset;
	}	

	/**
	 * Gets the replacement length.
	 * @return Returns a int
	 */
	public int getReplacementLength() {
		return fReplacementLength;
	}

	/**
	 * Sets the replacement length.
	 * @param replacementLength The replacementLength to set
	 */
	public void setReplacementLength(int replacementLength) {
		Assert.isTrue(replacementLength >= 0);
		fReplacementLength= replacementLength;
	}

	/**
	 * Gets the replacement string.
	 * @return Returns a String
	 */
	public String getReplacementString() {
		return fReplacementString;
	}
	
	@Override
	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
		String string= getReplacementString();
		int pos= string.indexOf('(');
		if (pos > 0)
			return string.subSequence(0, pos);
		return string;
	}

	/**
	 * Sets the replacement string.
	 * @param replacementString The replacement string to set
	 */
	public void setReplacementString(String replacementString) {
		fReplacementString= replacementString;
	}

	/**
	 * Sets the image.
	 * @param image The image to set
	 */
	public void setImage(Image image) {
		fImage= image;
	}

	@Override
	public boolean isValidFor(IDocument document, int offset) {
		return validate(document, offset, null);
	}

	@Override
	public boolean validate(IDocument document, int offset, DocumentEvent event) {
		if (offset < fReplacementOffset)
			return false;
				
		boolean validated= match(document, offset, fReplacementString);	

		if (validated && event != null) {
			// Adapt replacement range to document change
			int delta= (event.fText == null ? 0 : event.fText.length()) - event.fLength;
			fReplacementLength += delta;	
		}

		return validated;
	}
	
	/**
	 * Gets the proposal's relevance.
	 * @return Returns a int
	 */
	@Override
	public int getRelevance() {
		return fRelevance;
	}

	/**
	 * Sets the proposal's relevance.
	 * @param relevance The relevance to set
	 */
	public void setRelevance(int relevance) {
		fRelevance= relevance;
	}

	/**
	 * Returns {@code true} if a words matches the code completion prefix in the document,
	 * {@code false} otherwise.
	 */	
	protected boolean match(IDocument document, int offset, String word) {
		if (word == null) 
			return false;
		
		final int wordLength= word.length();
		if (offset >  fReplacementOffset + wordLength)
			return false;
		
		try {
			int length= offset - fReplacementOffset;
			String pattern= document.get(fReplacementOffset, length);
			return ContentAssistMatcherFactory.getInstance().match(pattern.toCharArray(), word.toCharArray());
		} catch (BadLocationException x) {
		}
		
		return false;	
	}	

	private static boolean insertCompletion() {
		IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
		return preference.getBoolean(ContentAssistPreference.AUTOINSERT);
	}

	@Override
	public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
		IDocument document= viewer.getDocument();

		// Don't eat if not in preferences, XOR with modifier key 1 (Ctrl)
		// but: if there is a selection, replace it!
		Point selection= viewer.getSelectedRange();
		fToggleEating= (stateMask & SWT.MOD1) != 0;
		if (insertCompletion() ^ fToggleEating)
			fReplacementLength= selection.x + selection.y - fReplacementOffset;
		
		apply(document, trigger, offset);
		fToggleEating= false;
	}

	private static Color getForegroundColor(StyledText text) {
		IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
		RGB rgb= PreferenceConverter.getColor(preference, ContentAssistPreference.PROPOSALS_FOREGROUND);
		CTextTools textTools= CUIPlugin.getDefault().getTextTools();
		return textTools.getColorManager().getColor(rgb);
	}

	private static Color getBackgroundColor(StyledText text) {
		IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
		RGB rgb= PreferenceConverter.getColor(preference, ContentAssistPreference.PROPOSALS_BACKGROUND);
		CTextTools textTools= CUIPlugin.getDefault().getTextTools();
		return textTools.getColorManager().getColor(rgb);
	}
	
	private void repairPresentation(ITextViewer viewer) {
		if (fRememberedStyleRange != null) {
			 if (viewer instanceof ITextViewerExtension2) {
			 	// Attempts to reduce the redraw area
			 	ITextViewerExtension2 viewer2= (ITextViewerExtension2) viewer;
			 	
			 	if (viewer instanceof ITextViewerExtension5) {
			 		ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
			 		IRegion widgetRange= extension.modelRange2WidgetRange(new Region(fRememberedStyleRange.start, fRememberedStyleRange.length));
			 		if (widgetRange != null)
			 			viewer2.invalidateTextPresentation(widgetRange.getOffset(), widgetRange.getLength());
			 	} else {
					viewer2.invalidateTextPresentation(fRememberedStyleRange.start + viewer.getVisibleRegion().getOffset(), fRememberedStyleRange.length);
			 	}
			} else {
				viewer.invalidateTextPresentation();
			}
		}
	}

	private void updateStyle(ITextViewer viewer) {
		StyledText text= viewer.getTextWidget();
		if (text == null || text.isDisposed())
			return;

		int widgetCaret= text.getCaretOffset();
		
		int modelCaret= 0;
		if (viewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
			modelCaret= extension.widgetOffset2ModelOffset(widgetCaret);
		} else {
			IRegion visibleRegion= viewer.getVisibleRegion();
			modelCaret= widgetCaret + visibleRegion.getOffset();			
		}
		
		if (modelCaret >= fReplacementOffset + fReplacementLength) {
			repairPresentation(viewer);
			return;
		}
					
		int offset= widgetCaret;
		int length= fReplacementOffset + fReplacementLength - modelCaret;
	
		Color foreground= getForegroundColor(text);
		Color background= getBackgroundColor(text);

		StyleRange range= text.getStyleRangeAtOffset(offset);
		int fontStyle= range != null ? range.fontStyle : SWT.NORMAL;

		repairPresentation(viewer);
		fRememberedStyleRange= new StyleRange(offset, length, foreground, background, fontStyle);
		
		// http://dev.eclipse.org/bugs/show_bug.cgi?id=34754
		try {
			text.setStyleRange(fRememberedStyleRange);
		} catch (IllegalArgumentException x) {
			// Catching exception as offset + length might be outside of the text widget
			fRememberedStyleRange= null;
		}
	}

	@Override
	public void selected(ITextViewer viewer, boolean smartToggle) {
		if (!insertCompletion() ^ smartToggle) {
			updateStyle(viewer);
		} else {
			repairPresentation(viewer);
			fRememberedStyleRange= null;
		}
	}

	@Override
	public void unselected(ITextViewer viewer) {
		repairPresentation(viewer);
		fRememberedStyleRange= null;
	}

	@Override
	public IInformationControlCreator getInformationControlCreator() {
		return null;
	}

	public void updateReplacementOffset(int newOffset) {
		setReplacementOffset(newOffset);
	}

	public void updateReplacementLength(int length) {
		setReplacementLength(length);
	}

	@Override
	public int hashCode() {
		return fIdString.hashCode();
	}

	@Override
	public boolean equals(Object other) {
		if(!(other instanceof ICCompletionProposal))
			return false;
		return fIdString.equalsIgnoreCase(((ICCompletionProposal)other).getIdString());
	}
}
