content: /*******************************************************************************
   * Copyright (c) 2000, 2009 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.wst.jsdt.internal.ui.text.java;
  
  import org.eclipse.jface.dialogs.MessageDialog;
  import org.eclipse.jface.text.BadLocationException;
  import org.eclipse.jface.text.IDocument;
  import org.eclipse.jface.text.IRegion;
  import org.eclipse.jface.text.Region;
  import org.eclipse.jface.text.contentassist.IContextInformation;
  import org.eclipse.jface.text.contentassist.IContextInformationExtension;
  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.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;
  import org.eclipse.wst.jsdt.core.CompletionProposal;
  import org.eclipse.wst.jsdt.core.IType;
  import org.eclipse.wst.jsdt.core.JavaScriptModelException;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
  import org.eclipse.wst.jsdt.internal.ui.javaeditor.JavaEditor;
  import org.eclipse.wst.jsdt.ui.text.java.JavaContentAssistInvocationContext;
  
  /**
   * Proposal for generic types.
   * <p>
   * Only used when compliance is set to 5.0 or higher.
   * </p>
   */
  public final class LazyGenericTypeProposal extends LazyJavaTypeCompletionProposal {
  	/** Triggers for types. Do not modify. */
  	private final static char[] GENERIC_TYPE_TRIGGERS= new char[] { '.', '\t', '[', '(', '<', ' ' };
  
  	/**
  	 * Short-lived context information object for generic types. Currently, these
  	 * are only created after inserting a type proposal, as core doesn't give us
  	 * the correct type proposal from within SomeType<|>.
  	 */
  	private static class ContextInformation implements IContextInformation, IContextInformationExtension {
  		private final String fInformationDisplayString;
  		private final String fContextDisplayString;
  		private final Image fImage;
  		private final int fPosition;
  		
  		ContextInformation(LazyGenericTypeProposal proposal) {
  			// don't cache the proposal as content assistant
  			// might hang on to the context info
  			fContextDisplayString= proposal.getDisplayString();
  			fInformationDisplayString= computeContextString(proposal);
  			fImage= proposal.getImage();
  			fPosition= proposal.getReplacementOffset() + proposal.getReplacementString().indexOf('<') + 1;
  		}
  		
  		/*
  		 * @see org.eclipse.jface.text.contentassist.IContextInformation#getContextDisplayString()
  		 */
  		public String getContextDisplayString() {
  			return fContextDisplayString;
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.contentassist.IContextInformation#getImage()
  		 */
  		public Image getImage() {
  			return fImage;
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.contentassist.IContextInformation#getInformationDisplayString()
  		 */
  		public String getInformationDisplayString() {
  			return fInformationDisplayString;
  		}
  
  		private String computeContextString(LazyGenericTypeProposal proposal) {
  			try {
  				TypeArgumentProposal[] proposals= proposal.computeTypeArgumentProposals();
  				if (proposals.length == 0)
  					return null;
  				
  				StringBuffer buf= new StringBuffer();
  				for (int i= 0; i < proposals.length; i++) {
  					buf.append(proposals[i].getDisplayName());
  					if (i < proposals.length - 1)
  						buf.append(", "); //$NON-NLS-1$
  				}
  				return buf.toString();
  				
  			} catch (JavaScriptModelException e) {
  				return null;
  			}
  		}
  
  		/*
  		 * @see org.eclipse.jface.text.contentassist.IContextInformationExtension#getContextInformationPosition()
  		 */
  		public int getContextInformationPosition() {
  			return fPosition;
  		}
  		
  		/*
  		 * @see java.lang.Object#equals(java.lang.Object)
  		 */
  		public boolean equals(Object obj) {
  			if (obj instanceof ContextInformation) {
  				ContextInformation ci= (ContextInformation) obj;
  				return getContextInformationPosition() == ci.getContextInformationPosition() && getInformationDisplayString().equals(ci.getInformationDisplayString());
  			}
  			return false;
  		}
  	}
  
  	private static final class TypeArgumentProposal {
  		private final boolean fIsAmbiguous;
  		private final String fProposal;
  		private final String fTypeDisplayName;
  
  		TypeArgumentProposal(String proposal, boolean ambiguous, String typeDisplayName) {
  			fIsAmbiguous= ambiguous;
  			fProposal= proposal;
  			fTypeDisplayName= typeDisplayName;
  		}
  
  		public String getDisplayName() {
  			return fTypeDisplayName;
  		}
  
  		boolean isAmbiguous() {
  			return fIsAmbiguous;
  		}
  
  		String getProposals() {
  			return fProposal;
  		}
  
  		public String toString() {
  			return fProposal;
  		}
  	}
  
  	private IRegion fSelectedRegion; // initialized by apply()
  	private TypeArgumentProposal[] fTypeArgumentProposals;
  
  	public LazyGenericTypeProposal(CompletionProposal typeProposal, JavaContentAssistInvocationContext context) {
  		super(typeProposal, context);
  	}
  
  	/*
  	 * @see ICompletionProposalExtension#apply(IDocument, char)
  	 */
  	public void apply(IDocument document, char trigger, int offset) {
  
  		if (shouldAppendArguments(document, offset, trigger)) {
  			try {
  				TypeArgumentProposal[] typeArgumentProposals= computeTypeArgumentProposals();
  				if (typeArgumentProposals.length > 0) {
  
  					int[] offsets= new int[typeArgumentProposals.length];
  					int[] lengths= new int[typeArgumentProposals.length];
  					StringBuffer buffer= createParameterList(typeArgumentProposals, offsets, lengths);
  
  					// set the generic type as replacement string
  					boolean insertClosingParenthesis= trigger == '(' && autocloseBrackets();
  					if (insertClosingParenthesis)
  						updateReplacementWithParentheses(buffer);
  					super.setReplacementString(buffer.toString());
  
  					// add import & remove package, update replacement offset
  					super.apply(document, '\0', offset);
  
  					if (getTextViewer() != null) {
  						if (hasAmbiguousProposals(typeArgumentProposals)) {
  							adaptOffsets(offsets, buffer);
  							installLinkedMode(document, offsets, lengths, typeArgumentProposals, insertClosingParenthesis);
  						} else {
  							if (insertClosingParenthesis)
  								setUpLinkedMode(document, ')');
  							else
  								fSelectedRegion= new Region(getReplacementOffset() + getReplacementString().length(), 0);
  						}
  					}
  
  					return;
  				}
  			} catch (JavaScriptModelException e) {
  				// log and continue
  				JavaScriptPlugin.log(e);
  			}
  		}
  
  		// default is to use the super implementation
  		// reasons:
  		// - not a parameterized type,
  		// - already followed by <type arguments>
  		// - proposal type does not inherit from expected type
  		super.apply(document, trigger, offset);
  	}
  	
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.java.LazyJavaTypeCompletionProposal#computeTriggerCharacters()
  	 */
  	protected char[] computeTriggerCharacters() {
  		return GENERIC_TYPE_TRIGGERS;
  	}
  
  	/**
  	 * Adapt the parameter offsets to any modification of the replacement
  	 * string done by <code>apply</code>. For example, applying the proposal
  	 * may add an import instead of inserting the fully qualified name.
  	 * <p>
  	 * This assumes that modifications happen only at the beginning of the
  	 * replacement string and do not touch the type arguments list.
  	 * </p>
  	 *
  	 * @param offsets the offsets to modify
  	 * @param buffer the original replacement string
  	 */
  	private void adaptOffsets(int[] offsets, StringBuffer buffer) {
  		String replacementString= getReplacementString();
  		int delta= buffer.length() - replacementString.length(); // due to using an import instead of package
  		for (int i= 0; i < offsets.length; i++) {
  			offsets[i]-= delta;
  		}
  	}
  
  	/**
  	 * Computes the type argument proposals for this type proposals. If there is
  	 * an expected type binding that is a super type of the proposed type, the
  	 * wildcard type arguments of the proposed type that can be mapped through
  	 * to type the arguments of the expected type binding are bound accordingly.
  	 * <p>
  	 * For type arguments that cannot be mapped to arguments in the expected
  	 * type, or if there is no expected type, the upper bound of the type
  	 * argument is proposed.
  	 * </p>
  	 * <p>
  	 * The argument proposals have their <code>isAmbiguos</code> flag set to
  	 * <code>false</code> if the argument can be mapped to a non-wildcard type
  	 * argument in the expected type, otherwise the proposal is ambiguous.
  	 * </p>
  	 *
  	 * @return the type argument proposals for the proposed type
  	 * @throws JavaScriptModelException if accessing the java model fails
  	 */
  	private TypeArgumentProposal[] computeTypeArgumentProposals() throws JavaScriptModelException {
  		if (fTypeArgumentProposals == null) {
  			
  			IType type= (IType) getJavaElement();
  			if (type == null)
  				return new TypeArgumentProposal[0];
  			
  			
  			return new TypeArgumentProposal[0];
  		}
  		return fTypeArgumentProposals;
  	}
  
  	/**
  	 * Returns <code>true</code> if type arguments should be appended when
  	 * applying this proposal, <code>false</code> if not (for example if the
  	 * document already contains a type argument list after the insertion point.
  	 *
  	 * @param document the document
  	 * @param offset the insertion offset
  	 * @param trigger the trigger character
  	 * @return <code>true</code> if arguments should be appended
  	 */
  	private boolean shouldAppendArguments(IDocument document, int offset, char trigger) {
  		/*
  		 * No argument list if there were any special triggers (for example a period to qualify an
  		 * inner type).
  		 */
  		if (trigger != '\0' && trigger != '<' && trigger != '(')
  			return false;
  		
  		/* No argument list if the completion is empty (already within the argument list). */
  		char[] completion= fProposal.getCompletion();
  		if (completion.length == 0)
  			return false;
  
  		/* No argument list if there already is a generic signature behind the name. */
  		try {
  			IRegion region= document.getLineInformationOfOffset(offset);
  			String line= document.get(region.getOffset(), region.getLength());
  
  			int index= offset - region.getOffset();
  			while (index != line.length() && Character.isUnicodeIdentifierPart(line.charAt(index)))
  				++index;
  
  			if (index == line.length())
  				return true;
  
  			char ch= line.charAt(index);
  			return ch != '<';
  
  		} catch (BadLocationException e) {
  			return true;
  		}
  	}
  
  	private StringBuffer createParameterList(TypeArgumentProposal[] typeArguments, int[] offsets, int[] lengths) {
  		StringBuffer buffer= new StringBuffer();
  		buffer.append(getReplacementString());
  
  		FormatterPrefs prefs= getFormatterPrefs();
  		final char LESS= '<';
  		final char GREATER= '>';
  		if (prefs.beforeOpeningBracket)
  			buffer.append(SPACE);
  		buffer.append(LESS);
  		if (prefs.afterOpeningBracket)
  			buffer.append(SPACE);
  		StringBuffer separator= new StringBuffer(3);
  		if (prefs.beforeTypeArgumentComma)
  			separator.append(SPACE);
  		separator.append(COMMA);
  		if (prefs.afterTypeArgumentComma)
  			separator.append(SPACE);
  
  		for (int i= 0; i != typeArguments.length; i++) {
  			if (i != 0)
  				buffer.append(separator);
  
  			offsets[i]= buffer.length();
  			buffer.append(typeArguments[i]);
  			lengths[i]= buffer.length() - offsets[i];
  		}
  		if (prefs.beforeClosingBracket)
  			buffer.append(SPACE);
  		buffer.append(GREATER);
  
  		return buffer;
  	}
  
  	private void installLinkedMode(IDocument document, int[] offsets, int[] lengths, TypeArgumentProposal[] typeArgumentProposals, boolean withParentheses) {
  		int replacementOffset= getReplacementOffset();
  		String replacementString= getReplacementString();
  
  		try {
  			LinkedModeModel model= new LinkedModeModel();
  			for (int i= 0; i != offsets.length; i++) {
  				if (typeArgumentProposals[i].isAmbiguous()) {
  					LinkedPositionGroup group= new LinkedPositionGroup();
  					group.addPosition(new LinkedPosition(document, replacementOffset + offsets[i], lengths[i]));
  					model.addGroup(group);
  				}
  			}
  			if (withParentheses) {
  				LinkedPositionGroup group= new LinkedPositionGroup();
  				group.addPosition(new LinkedPosition(document, replacementOffset + getCursorPosition(), 0));
  				model.addGroup(group);
  			}
  
  			model.forceInstall();
  			JavaEditor editor= getJavaEditor();
  			if (editor != null) {
  				model.addLinkingListener(new EditorHighlightingSynchronizer(editor));
  			}
  
  			LinkedModeUI ui= new EditorLinkedModeUI(model, getTextViewer());
  			ui.setExitPolicy(new ExitPolicy(withParentheses ? ')' : '>', document));
  			ui.setExitPosition(getTextViewer(), replacementOffset + replacementString.length(), 0, Integer.MAX_VALUE);
  			ui.setDoContextInfo(true);
  			ui.enter();
  
  			fSelectedRegion= ui.getSelectedRegion();
  
  		} catch (BadLocationException e) {
  			JavaScriptPlugin.log(e);
  			openErrorDialog(e);
  		}
  	}
  
  	private boolean hasAmbiguousProposals(TypeArgumentProposal[] typeArgumentProposals) {
  		boolean hasAmbiguousProposals= false;
  		for (int i= 0; i < typeArgumentProposals.length; i++) {
  			if (typeArgumentProposals[i].isAmbiguous()) {
  				hasAmbiguousProposals= true;
  				break;
  			}
  		}
  		return hasAmbiguousProposals;
  	}
  
  	/**
  	 * 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 JavaEditor getJavaEditor() {
  		IEditorPart part= JavaScriptPlugin.getActivePage().getActiveEditor();
  		if (part instanceof JavaEditor)
  			return (JavaEditor) part;
  		else
  			return null;
  	}
  
  	/*
  	 * @see ICompletionProposal#getSelection(IDocument)
  	 */
  	public Point getSelection(IDocument document) {
  		if (fSelectedRegion == null)
  			return super.getSelection(document);
  
  		return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
  	}
  
  	private void openErrorDialog(BadLocationException e) {
  		Shell shell= getTextViewer().getTextWidget().getShell();
  		MessageDialog.openError(shell, JavaTextMessages.FilledArgumentNamesMethodProposal_error_msg, e.getMessage());
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.java.LazyJavaCompletionProposal#computeContextInformation()
  	 */
  	protected IContextInformation computeContextInformation() {
  		try {
  			if (hasParameters()) {
  				TypeArgumentProposal[] proposals= computeTypeArgumentProposals();
  				if (hasAmbiguousProposals(proposals))
  					return new ContextInformation(this);
  			}
  		} catch (JavaScriptModelException e) {
  		}
  		return super.computeContextInformation();
  	}
  	
  	protected int computeCursorPosition() {
  		if (fSelectedRegion != null)
  			return fSelectedRegion.getOffset() - getReplacementOffset();
  		return super.computeCursorPosition();
  	}
  	
  	private boolean hasParameters() {
  		IType type= (IType) getJavaElement();
  		if (type == null)
  			return false;
  
  		return false;
  	}
  }
  