/*******************************************************************************
 * Copyright (c) 2000, 2018 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:
 *     IBM Corporation - initial API and implementation
 *     Stephan Herrmann - Contribution for Bug 463360 - [override method][null] generating method override should not create redundant null annotations
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;

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

import org.eclipse.swt.graphics.Image;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;

import org.eclipse.text.edits.MalformedTreeException;

import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.window.Window;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.LinkedModeModel;

import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ITrackedNodePosition;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;

import org.eclipse.jdt.internal.core.manipulation.util.Strings;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2Core;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;

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

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.dialogs.OverrideMethodDialog;
import org.eclipse.jdt.internal.ui.preferences.JavaPreferencesSettings;
import org.eclipse.jdt.internal.ui.preferences.formatter.FormatterProfileManager;


public class AnonymousTypeCompletionProposal extends JavaTypeCompletionProposal implements ICompletionProposalExtension4 {

	private final String fDeclarationSignature;
	private final IType fSuperType;

	private boolean fIsContextInformationComputed;
	private int fContextInformationPosition;

	private ImportRewrite fImportRewrite;


	public AnonymousTypeCompletionProposal(IJavaProject jproject, ICompilationUnit cu, JavaContentAssistInvocationContext invocationContext, int start, int length, String constructorCompletion, StyledString displayName, String declarationSignature, IType superType, int relevance) {
		super(constructorCompletion, cu, start, length, null, displayName, relevance, null, invocationContext);
		Assert.isNotNull(declarationSignature);
		Assert.isNotNull(jproject);
		Assert.isNotNull(cu);
		Assert.isNotNull(superType);

		fDeclarationSignature= declarationSignature;
		fSuperType= superType;

		setCursorPosition(constructorCompletion.indexOf('(') + 1);
	}

	private String createDummyType(String name) throws JavaModelException {
		StringBuilder buffer= new StringBuilder();

		buffer.append("abstract class "); //$NON-NLS-1$
		buffer.append(name);
		if (fSuperType.isInterface())
			buffer.append(" implements "); //$NON-NLS-1$
		else
			buffer.append(" extends "); //$NON-NLS-1$

		if (fDeclarationSignature != null)
			buffer.append(Signature.toString(fDeclarationSignature));
		else
			buffer.append(fSuperType.getFullyQualifiedParameterizedName());
		buffer.append(" {"); //$NON-NLS-1$
		buffer.append("\n"); // Using newline is ok since source is used in dummy compilation unit //$NON-NLS-1$
		buffer.append("}"); //$NON-NLS-1$
		return buffer.toString();
	}

	private String createNewBody(ImportRewrite importRewrite, int offset) throws CoreException {
		if (importRewrite == null)
			return null;

		ICompilationUnit workingCopy= null;
		try {
			String name= "Type" + System.currentTimeMillis(); //$NON-NLS-1$
			workingCopy= fCompilationUnit.getPrimary().getWorkingCopy(null);


			ISourceRange range= fSuperType.getSourceRange();
			boolean sameUnit= range != null && fCompilationUnit.equals(fSuperType.getCompilationUnit());

			// creates a type that extends the super type
			String dummyClassContent= createDummyType(name);

			StringBuilder workingCopyContents= new StringBuilder(fCompilationUnit.getSource());
			int insertPosition;
			if (sameUnit) {
				insertPosition= range.getOffset() + range.getLength();
			} else {
				ISourceRange firstTypeRange= fCompilationUnit.getTypes()[0].getSourceRange();
				insertPosition= firstTypeRange.getOffset();
			}
			if (fSuperType.isLocal()) {
				// add an extra block: helps the AST to recover
				workingCopyContents.insert(insertPosition, '{' + dummyClassContent + '}');
				insertPosition++;
				if(offset > insertPosition) {
					offset += dummyClassContent.length()+2;
				}
			} else {
				/*
				 * The two empty lines are added because the trackedDeclaration uses the covered range
				 * and hence would also included comments that directly follow the dummy class.
				 */
				workingCopyContents.insert(insertPosition, dummyClassContent + "\n\n"); //$NON-NLS-1$
				if(offset > insertPosition) {
					offset += dummyClassContent.length()+2;
				}
			}

			workingCopy.getBuffer().setContents(workingCopyContents.toString());

			ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
			parser.setResolveBindings(true);
			parser.setStatementsRecovery(true);
			parser.setSource(workingCopy);

			CompilationUnit astRoot= (CompilationUnit) parser.createAST(new NullProgressMonitor());
			ImportRewriteContext context=new ContextSensitiveImportRewriteContext(astRoot, offset, importRewrite);
			ASTNode newType= NodeFinder.perform(astRoot, insertPosition, dummyClassContent.length());
			if (!(newType instanceof AbstractTypeDeclaration))
				return null;

			AbstractTypeDeclaration declaration= (AbstractTypeDeclaration) newType;
			ITypeBinding dummyTypeBinding= declaration.resolveBinding();
			if (dummyTypeBinding == null)
				return null;

			IMethodBinding[] bindings= StubUtility2Core.getOverridableMethods(astRoot.getAST(), dummyTypeBinding, true);

			if (fSuperType.isInterface()) {
				ITypeBinding[] dummySuperInterfaces= dummyTypeBinding.getInterfaces();
				if (dummySuperInterfaces.length == 0 || dummySuperInterfaces.length == 1 && dummySuperInterfaces[0].isRawType())
					bindings= new IMethodBinding[0];
			} else {
				ITypeBinding dummySuperclass= dummyTypeBinding.getSuperclass();
				if (dummySuperclass == null || dummySuperclass.isRawType())
					bindings= new IMethodBinding[0];
			}

			CodeGenerationSettings settings= JavaPreferencesSettings.getCodeGenerationSettings(fSuperType.getJavaProject());

			IMethodBinding[] methodsToOverride= null;

			IType type= null;
			if (!fSuperType.isInterface() && !fSuperType.isAnnotation()) {
				IJavaElement typeElement= dummyTypeBinding.getJavaElement();
				// add extra checks here as the recovered code is fragile
				if (typeElement instanceof IType && name.equals(typeElement.getElementName()) && typeElement.exists()) {
					type= (IType) typeElement;
				}
			}

			if (type != null) {
				OverrideMethodDialog dialog= new OverrideMethodDialog(JavaPlugin.getActiveWorkbenchShell(), null, type, true);
				dialog.setGenerateComment(false);
				dialog.setElementPositionEnabled(false);
				if (dialog.open() == Window.OK) {
					Object[] selection= dialog.getResult();
					ArrayList<Object> result= new ArrayList<>(selection.length);
					for (Object s : selection) {
						if (s instanceof IMethodBinding) {
							result.add(s);
						}
					}
					methodsToOverride= result.toArray(new IMethodBinding[result.size()]);
					settings.createComments= dialog.getGenerateComment();
				} else {
					// cancelled
					setReplacementString(""); //$NON-NLS-1$
					setReplacementLength(0);
					return null;
				}
			} else {
				settings.createComments= false;
				List<IMethodBinding> result= new ArrayList<>();
				for (IMethodBinding curr : bindings) {
					if (Modifier.isAbstract(curr.getModifiers()))
						result.add(curr);
				}
				methodsToOverride= result.toArray(new IMethodBinding[result.size()]);
			}
			ASTNode focusNode; // used to find @NonNullByDefault effective
			if (JavaCore.ENABLED.equals(fCompilationUnit.getJavaProject().getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true))) {
				focusNode= NodeFinder.perform(astRoot, getReplacementOffset()+dummyClassContent.length(), 0);
			} else {
				focusNode= null;
			}
			ASTRewrite rewrite= ASTRewrite.create(astRoot.getAST());
			ITrackedNodePosition trackedDeclaration= rewrite.track(declaration);

			ListRewrite rewriter= rewrite.getListRewrite(declaration, declaration.getBodyDeclarationsProperty());
			for (IMethodBinding curr : methodsToOverride) {
				MethodDeclaration stub= StubUtility2.createImplementationStub(workingCopy, rewrite, importRewrite, context, curr, dummyTypeBinding, settings, dummyTypeBinding.isInterface(), focusNode);
				rewriter.insertFirst(stub, null);
			}


			IDocument document= new Document(workingCopy.getSource());
			try {
				rewrite.rewriteAST().apply(document);

				int bodyStart= trackedDeclaration.getStartPosition() + dummyClassContent.indexOf('{');
				int bodyEnd= trackedDeclaration.getStartPosition() + trackedDeclaration.getLength();
				return document.get(bodyStart, bodyEnd - bodyStart);
			} catch (MalformedTreeException | BadLocationException exception) {
				JavaPlugin.log(exception);
			}
			return null;
		} finally {
			if (workingCopy != null)
				workingCopy.discardWorkingCopy();
		}
	}

	protected Image getImageForType(IType type) {
		String imageName= JavaPluginImages.IMG_OBJS_CLASS; // default
		try {
			if (type.isAnnotation()) {
				imageName= JavaPluginImages.IMG_OBJS_ANNOTATION;
			} else if (type.isInterface()) {
				imageName= JavaPluginImages.IMG_OBJS_INTERFACE;
			}
		} catch (JavaModelException e) {
			JavaPlugin.log(e);
		}
		return JavaPluginImages.get(imageName);
	}

	@Override
	public Image getImage() {
		Image image= super.getImage();
		if (image == null) {
			image= getImageForType(fSuperType);
			setImage(image);
		}
		return image;
	}

	/*
	 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension4#isAutoInsertable()
	 */
	@Override
	public boolean isAutoInsertable() {
		return false;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#isOffsetValid(int)
	 * @since 3.5
	 */
	@Override
	protected boolean isOffsetValid(int offset) {
		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
			return super.isOffsetValid(offset);

		return coreProposal.getRequiredProposals()[0].getReplaceStart() <= offset;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#getPrefixCompletionStart(org.eclipse.jface.text.IDocument, int)
	 * @since 3.5
	 */
	@Override
	public int getPrefixCompletionStart(IDocument document, int completionOffset) {
		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
			return super.getPrefixCompletionStart(document, completionOffset);

		return coreProposal.getRequiredProposals()[0].getReplaceStart();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#getPrefixCompletionText(org.eclipse.jface.text.IDocument, int)
	 * @since 3.5
	 */
	@Override
	public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
			return super.getPrefixCompletionText(document, completionOffset);

		return String.valueOf(coreProposal.getName());
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#getPrefix(org.eclipse.jface.text.IDocument, int)
	 * @since 3.5
	 */
	@Override
	protected String getPrefix(IDocument document, int offset) {
		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
			return super.getPrefix(document, offset);

		int replacementOffset= coreProposal.getRequiredProposals()[0].getReplaceStart();

		try {
			int length= offset - replacementOffset;
			if (length > 0)
				return document.get(replacementOffset, length);
		} catch (BadLocationException x) {
		}
		return ""; //$NON-NLS-1$

	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#isValidPrefix(java.lang.String)
	 * @since 3.5
	 */
	@Override
	protected boolean isValidPrefix(String prefix) {
		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
			return super.isValidPrefix(prefix);

		return super.isValidPrefix(prefix) || isPrefix(prefix, String.valueOf(coreProposal.getName()));
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#apply(org.eclipse.jface.text.IDocument, char, int)
	 * @since 3.5
	 */
	@Override
	public void apply(IDocument document, char trigger, int offset) {
		super.apply(document, trigger, offset);
		LinkedModeModel.closeAllModels(document);
	}


	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#updateReplacementString(org.eclipse.jface.text.IDocument, char, int, org.eclipse.jdt.core.dom.rewrite.ImportRewrite)
	 */
	@Override
	protected boolean updateReplacementString(IDocument document, char trigger, int offset, ImportRewrite impRewrite) throws CoreException, BadLocationException {
		fImportRewrite= impRewrite;
		String newBody= createNewBody(impRewrite, offset);
		if (newBody == null)
			return false;

		CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		boolean isAnonymousConstructorInvoc= coreProposal.getKind() == CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION;

		boolean replacementStringEndsWithParentheses= isAnonymousConstructorInvoc || getReplacementString().endsWith(")"); //$NON-NLS-1$

		// construct replacement text: an expression to be formatted
		StringBuilder buf= new StringBuilder("new A("); //$NON-NLS-1$
		if (!replacementStringEndsWithParentheses || isAnonymousConstructorInvoc)
			buf.append(')');
		buf.append(newBody);

		// use the code formatter
		String lineDelim= TextUtilities.getDefaultLineDelimiter(document);
		final IJavaProject project= fCompilationUnit.getJavaProject();
		IRegion lineInfo= document.getLineInformationOfOffset(getReplacementOffset());
		int indent= Strings.computeIndentUnits(document.get(lineInfo.getOffset(), lineInfo.getLength()), project);

		Map<String, String> options= project != null ? FormatterProfileManager.getProjectSettings(project) : JavaCore.getOptions();
		options.put(DefaultCodeFormatterConstants.FORMATTER_INDENT_EMPTY_LINES, DefaultCodeFormatterConstants.TRUE);
		String replacementString= CodeFormatterUtil.format(CodeFormatter.K_EXPRESSION, buf.toString(), 0, lineDelim, options);

		int lineEndOffset= lineInfo.getOffset() + lineInfo.getLength();

		int p= offset;
		char ch= document.getChar(p);
		while (p < lineEndOffset) {
			if (ch == '(' || ch == ')' || ch == ';' || ch == ',')
				break;
			ch= document.getChar(++p);
		}

		if (ch != ';' && ch != ',' && ch != ')')
			replacementString= replacementString + ';';

		replacementString= Strings.changeIndent(replacementString, 0, project, CodeFormatterUtil.createIndentString(indent, project), lineDelim);

		int beginIndex= replacementString.indexOf('(');
		if (!isAnonymousConstructorInvoc)
			beginIndex++;
		replacementString= replacementString.substring(beginIndex);

		int pos= offset;
		if (isAnonymousConstructorInvoc && (insertCompletion() ^ isInsertModeToggled())) {
			// Keep existing code
			int endPos= pos;
			ch= document.getChar(endPos);
			while (endPos < lineEndOffset && ch != '(' && ch != ')' && ch != ';' && ch != ',' && !Character.isWhitespace(ch))
				ch= document.getChar(++endPos);

			int keepLength= endPos - pos;
			if (keepLength > 0) {
				String keepStr= document.get(pos, keepLength);
				replacementString= replacementString + keepStr;
				setCursorPosition(replacementString.length() - keepLength);
			}
		} else
			setCursorPosition(replacementString.length());

		setReplacementString(replacementString);

		if (pos < document.getLength() && document.getChar(pos) == ')') {
			int currentLength= getReplacementLength();
			if (replacementStringEndsWithParentheses)
				setReplacementLength(currentLength + pos - offset);
			else
				setReplacementLength(currentLength + pos - offset + 1);
		}
		return false;
	}

	/*
	 * @see ICompletionProposalExtension#getContextInformationPosition()
	 * @since 3.4
	 */
	@Override
	public int getContextInformationPosition() {
		if (!fIsContextInformationComputed)
			setContextInformation(computeContextInformation());
		return fContextInformationPosition;
	}


	/*
	 * @see ICompletionProposal#getContextInformation()
	 * @since 3.4
	 */
	@Override
	public final IContextInformation getContextInformation() {
		if (!fIsContextInformationComputed)
			setContextInformation(computeContextInformation());
		return super.getContextInformation();
	}

	protected IContextInformation computeContextInformation() {
		try {
			fContextInformationPosition= getReplacementOffset() - 1;

			CompletionProposal proposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
			// no context information for METHOD_NAME_REF proposals (e.g. for static imports)
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=94654
			if (hasParameters() && (getReplacementString().endsWith(")") || getReplacementString().length() == 0)) { //$NON-NLS-1$
				ProposalContextInformation contextInformation= new ProposalContextInformation(proposal);
				fContextInformationPosition= getReplacementOffset() + getCursorPosition();
				if (fContextInformationPosition != 0 && proposal.getCompletion().length == 0)
					contextInformation.setContextInformationPosition(fContextInformationPosition);
				return contextInformation;
			}
			return null;
		} finally {
			fIsContextInformationComputed= true;
		}
	}

	/**
	 * Returns <code>true</code> if the method being inserted has at least one parameter. Note
	 * that this does not say anything about whether the argument list should be inserted.
	 *
	 * @return <code>true</code> if the method has any parameters, <code>false</code> if it has no parameters
	 * @since 3.4
	 */
	private boolean hasParameters() {
		CompletionProposal proposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
		return Signature.getParameterCount(proposal.getSignature()) > 0;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#createLazyJavaTypeCompletionProposal(org.eclipse.jdt.core.CompletionProposal, org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext)
	 * @since 3.5
	 */
	@Override
	protected LazyJavaCompletionProposal createRequiredTypeCompletionProposal(CompletionProposal completionProposal, JavaContentAssistInvocationContext invocationContext) {
		LazyJavaCompletionProposal proposal= super.createRequiredTypeCompletionProposal(completionProposal, invocationContext);
		if (proposal instanceof LazyJavaTypeCompletionProposal)
			((LazyJavaTypeCompletionProposal)proposal).setImportRewrite(fImportRewrite);
		return proposal;
	}

}