/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *	   Andrew McCullough - initial API and implementation
 *	   IBM Corporation  - general improvement and bug fixes, partial reimplementation
 *     Mohamed Azab (Mentor Graphics) - Bug 438549. Add mechanism for parameter guessing.
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.text.contentassist;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.cdt.core.dom.ast.IASTCompletionNode;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IFunction;
import org.eclipse.cdt.core.dom.ast.IParameter;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPSemantics;
import org.eclipse.cdt.internal.core.model.ASTCache.ASTRunnable;
import org.eclipse.cdt.internal.ui.editor.ASTProvider;
import org.eclipse.cdt.internal.ui.editor.CEditor;
import org.eclipse.cdt.internal.ui.editor.EditorHighlightingSynchronizer;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPositionUpdater;
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.link.ILinkedModeListener;
import org.eclipse.jface.text.link.InclusivePositionUpdater;
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.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;

/**
 * This class is based on org.eclipse.jdt.internal.ui.text.java.ParameterGuessingProposal
 *
 * Extents the basic Function Completion Proposal to add a linked mode for each of the function parameters
 * with a list of suggestions for each parameter.
 */
public class ParameterGuessingProposal extends FunctionCompletionProposal {
	private ICompletionProposal[][] fChoices; // Initialized by guessParameters()
	private Position[] fPositions; // Initialized by guessParameters()
	private IRegion fSelectedRegion; // Initialized by apply()
	private IPositionUpdater fUpdater;
	private String fFullPrefix; // The string from the start of the statement to the invocation offset.
	private CEditor fCEditor;
	private char[][] fParametersNames;
	private IType[] fParametersTypes;
	private List<IBinding> fAssignableElements;
	private IASTCompletionNode fCompletionNode;

	public static ParameterGuessingProposal createProposal(CContentAssistInvocationContext context,
			List<IBinding> availableElements, CCompletionProposal proposal, IFunction function, String prefix) {
		String replacement = getParametersList(function);
		String fullPrefix = function.getName() + "("; //$NON-NLS-1$
		int replacementOffset = proposal.getReplacementOffset();
		int replacementLength = 0;
		int invocationOffset = context.getInvocationOffset();
		int parseOffset = context.getParseOffset();
		IDocument document = context.getDocument();
		/*
		 * Adjust the replacement offset, the replacement string and the replacement length for the case of
		 * invoking after '('. - The replacement offset will be calculated to point to the start of the
		 * function call statement, as in that case the proposal.getReplacementOffset() doesn't point to that.
		 * - The replacement string will contain the in-editor prefix instead of the function name only, to
		 * handle the case of C++ function templates. - The length will be updated after changing the
		 * replacement string.
		 */
		if (isInsideBracket(invocationOffset, parseOffset)) {
			replacementOffset = parseOffset - prefix.length();
			try {
				fullPrefix = document.get(replacementOffset, invocationOffset - replacementOffset);
				replacement = fullPrefix + replacement + ")"; //$NON-NLS-1$
			} catch (BadLocationException e1) {
			}
			try {
				// Remove ')' from the replacement string if it is auto appended.
				if (document.getChar(invocationOffset) == ')')
					replacement = replacement.substring(0, replacement.length() - 1);
			} catch (BadLocationException e) {
			}
		} else {
			replacement = fullPrefix + replacement + ")"; //$NON-NLS-1$
			replacementOffset = proposal.getReplacementOffset();
		}
		replacementLength = prefix.length();
		ParameterGuessingProposal ret = new ParameterGuessingProposal(replacement, replacementOffset, replacementLength,
				proposal.getImage(), proposal.getDisplayString(), proposal.getIdString(), proposal.getRelevance(),
				context.getViewer(), function, invocationOffset, parseOffset, context.getTranslationUnit(), document,
				context.getCompletionNode());
		ret.setContextInformation(proposal.getContextInformation());
		ret.fFullPrefix = fullPrefix;
		ret.fCEditor = getCEditor(context.getEditor());
		/*
		 * Get all defined elements before the start of the statement. ex: int a = foo( ^ --> We don't want
		 * 'a' as a suggestion. ex2: char* foo(int a, int b) {return NULL;} void bar(char* name){} ...
		 * bar(foo( ^ --> If this offset is used, the only defined name will be "bar(char*)".
		 */
		ret.fAssignableElements = availableElements;
		return ret;
	}

	/**
	 * Returns a comma-separated list of parameters
	 */
	private static String getParametersList(IFunction method) {
		StringBuilder params = new StringBuilder();
		for (IParameter param : method.getParameters()) {
			if (params.length() != 0)
				params.append(", "); //$NON-NLS-1$
			params.append(param.getName());
		}
		return params.toString();
	}

	public ParameterGuessingProposal(String replacementString, int replacementOffset, int replacementLength,
			Image image, String displayString, String idString, int relevance, ITextViewer viewer, IFunction function,
			int invocationOffset, int parseOffset, ITranslationUnit tu, IDocument document,
			IASTCompletionNode completionNode) {
		super(replacementString, replacementOffset, replacementLength, image, displayString, idString, relevance,
				viewer, function, invocationOffset, parseOffset, tu, document);
		fCompletionNode = completionNode;
	}

	/**
	 * Checks if the invocation of content assist was after open bracket.
	 */
	private static boolean isInsideBracket(int invocationOffset, int parseOffset) {
		return invocationOffset - parseOffset != 0;
	}

	@Override
	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
		if (isInsideBracket(fInvocationOffset, fParseOffset)) {
			try {
				return fDocument.get(getReplacementOffset(), fInvocationOffset - getReplacementOffset());
			} catch (BadLocationException e) {
			}
		}
		return super.getPrefixCompletionText(document, completionOffset);
	}

	@Override
	public void apply(final IDocument document, char trigger, int offset) {
		super.apply(document, trigger, offset);

		generateParameterGuesses();

		int baseOffset = getReplacementOffset();
		String replacement = getReplacementString();
		try {
			if (fPositions != null && fTextViewer != null) {
				LinkedModeModel model = new LinkedModeModel();

				for (int i = 0; i < fPositions.length; i++) {
					LinkedPositionGroup group = new LinkedPositionGroup();
					int positionOffset = fPositions[i].getOffset();
					int positionLength = fPositions[i].getLength();

					if (fChoices[i].length == 0) {
						group.addPosition(new LinkedPosition(document, positionOffset, positionLength,
								LinkedPositionGroup.NO_STOP));
					} else {
						ensurePositionCategoryInstalled(document, model);
						document.addPosition(getCategory(), fPositions[i]);
						group.addPosition(new ProposalPosition(document, positionOffset, positionLength,
								LinkedPositionGroup.NO_STOP, fChoices[i]));
					}
					model.addGroup(group);
				}

				model.forceInstall();
				if (fCEditor != null) {
					model.addLinkingListener(new EditorHighlightingSynchronizer(fCEditor));
				}

				LinkedModeUI ui = new EditorLinkedModeUI(model, fTextViewer);
				ui.setExitPosition(fTextViewer, baseOffset + replacement.length(), 0, Integer.MAX_VALUE);
				// Exit character can be either ')' or ';'
				final char exitChar = replacement.charAt(replacement.length() - 1);
				ui.setExitPolicy(new ExitPolicy(exitChar) {
					@Override
					public ExitFlags doExit(LinkedModeModel model2, VerifyEvent event, int offset2, int length) {
						if (event.character == ',') {
							for (int i = 0; i < fPositions.length - 1; i++) { // not for the last one
								Position position = fPositions[i];
								if (position.offset <= offset2
										&& offset2 + length <= position.offset + position.length) {
									event.character = '\t';
									event.keyCode = SWT.TAB;
									return null;
								}
							}
						} else if (event.character == ')' && exitChar != ')') {
							// exit from link mode when user is in the last ')' position.
							Position position = fPositions[fPositions.length - 1];
							if (position.offset <= offset2 && offset2 + length <= position.offset + position.length) {
								return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
							}
						}
						return super.doExit(model2, event, offset2, length);
					}
				});
				ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT);
				ui.setDoContextInfo(true);
				ui.enter();
				fSelectedRegion = ui.getSelectedRegion();
			} else {
				fSelectedRegion = new Region(baseOffset + replacement.length(), 0);
			}
		} catch (Exception e) {
			ensurePositionCategoryRemoved(document);
			CUIPlugin.log(e);
		}
	}

	@Override
	public Point getSelection(IDocument document) {
		if (fSelectedRegion == null)
			return new Point(getReplacementOffset(), 0);

		return new Point(fSelectedRegion.getOffset(), fSelectedRegion.getLength());
	}

	public void generateParameterGuesses() {
		IStatus status = ASTProvider.getASTProvider().runOnAST(fTranslationUnit, ASTProvider.WAIT_NO,
				new NullProgressMonitor(), new ASTRunnable() {
					@Override
					public IStatus runOnAST(ILanguage lang, IASTTranslationUnit astRoot) throws CoreException {
						try {
							CPPSemantics.pushLookupPoint(fCompletionNode.getTranslationUnit());
							guessParameters();
						} catch (Exception e) {
							CUIPlugin.log(e);
							return Status.CANCEL_STATUS;
						} finally {
							CPPSemantics.popLookupPoint();
						}
						return Status.OK_STATUS;
					}
				});
		if (Status.CANCEL_STATUS == status)
			return;
	}

	void guessParameters() throws CModelException {
		// Initialize necessary fields.
		fParametersNames = getFunctionParametersNames(fFunctionParameters);
		fParametersTypes = getFunctionParametersTypes(fFunctionParameters);

		int count = fParametersNames.length;
		fPositions = new Position[count];
		fChoices = new ICompletionProposal[count][];

		ParameterGuesser guesser = new ParameterGuesser();

		for (int i = 0; i < count; i++) {
			String paramName = new String(fParametersNames[i]);
			Position position = new Position(0, 0);

			boolean isLastParameter = i == count - 1;
			ArrayList<ICompletionProposal> allProposals = new ArrayList<>();
			ICompletionProposal[] argumentProposals = guesser.parameterProposals(fParametersTypes[i], paramName,
					position, fAssignableElements, isLastParameter);
			allProposals.addAll(Arrays.asList(argumentProposals));
			fPositions[i] = position;
			fChoices[i] = argumentProposals;
		}
		updateProposalsPositions();
	}

	private void updateProposalsPositions() {
		StringBuilder buffer = new StringBuilder();
		buffer.append(fFullPrefix);
		setCursorPosition(buffer.length());

		int count = fParametersNames.length;
		int replacementOffset = getReplacementOffset();

		for (int i = 0; i < count; i++) {
			if (i != 0)
				buffer.append(", "); //$NON-NLS-1$

			String argument = new String(fParametersNames[i]);

			Position position = fPositions[i];
			position.setOffset(replacementOffset + buffer.length());
			position.setLength(argument.length());

			buffer.append(argument);
		}
	}

	private static IType[] getFunctionParametersTypes(IParameter[] functionParameters) {
		IType[] ret = new IType[functionParameters.length];
		for (int i = 0; i < functionParameters.length; i++) {
			ret[i] = functionParameters[i].getType();
		}
		return ret;
	}

	private static char[][] getFunctionParametersNames(IParameter[] functionParameters) {
		char[][] parameterNames = new char[functionParameters.length][];
		for (int i = 0; i < functionParameters.length; i++) {
			parameterNames[i] = functionParameters[i].getNameCharArray();
		}
		return parameterNames;
	}

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

			model.addLinkingListener(new ILinkedModeListener() {
				@Override
				public void left(LinkedModeModel environment, int flags) {
					ensurePositionCategoryRemoved(document);
				}

				@Override
				public void suspend(LinkedModeModel environment) {
				}

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

	private void ensurePositionCategoryRemoved(IDocument document) {
		if (document.containsPositionCategory(getCategory())) {
			try {
				document.removePositionCategory(getCategory());
			} catch (BadPositionCategoryException e) {
				// ignore
			}
			document.removePositionUpdater(fUpdater);
		}
	}

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

	/**
	 * Returns the C/C++ editor, or {@code null} if it cannot be determined.
	 */
	private static CEditor getCEditor(IEditorPart editorPart) {
		if (editorPart instanceof CEditor) {
			return (CEditor) editorPart;
		}
		return null;
	}

	/**
	 * Returns the guesses for each parameter
	 */
	public ICompletionProposal[][] getParametersGuesses() {
		return fChoices;
	}
}
