/*******************************************************************************
 * Copyright (c) 2000, 2008 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.correction;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
import org.eclipse.wst.jsdt.core.IJavaScriptProject;
import org.eclipse.wst.jsdt.core.dom.AST;
import org.eclipse.wst.jsdt.core.dom.ASTNode;
import org.eclipse.wst.jsdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.wst.jsdt.core.dom.Assignment;
import org.eclipse.wst.jsdt.core.dom.Block;
import org.eclipse.wst.jsdt.core.dom.BodyDeclaration;
import org.eclipse.wst.jsdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
import org.eclipse.wst.jsdt.core.dom.Expression;
import org.eclipse.wst.jsdt.core.dom.ExpressionStatement;
import org.eclipse.wst.jsdt.core.dom.FieldAccess;
import org.eclipse.wst.jsdt.core.dom.FieldDeclaration;
import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
import org.eclipse.wst.jsdt.core.dom.IVariableBinding;
import org.eclipse.wst.jsdt.core.dom.Initializer;
import org.eclipse.wst.jsdt.core.dom.FunctionDeclaration;
import org.eclipse.wst.jsdt.core.dom.Modifier;
import org.eclipse.wst.jsdt.core.dom.SimpleName;
import org.eclipse.wst.jsdt.core.dom.SingleVariableDeclaration;
import org.eclipse.wst.jsdt.core.dom.Statement;
import org.eclipse.wst.jsdt.core.dom.Type;
import org.eclipse.wst.jsdt.core.dom.VariableDeclarationExpression;
import org.eclipse.wst.jsdt.core.dom.VariableDeclarationFragment;
import org.eclipse.wst.jsdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.wst.jsdt.internal.corext.codemanipulation.StubUtility;
import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodes;
import org.eclipse.wst.jsdt.internal.corext.dom.Bindings;
import org.eclipse.wst.jsdt.internal.corext.util.Messages;
import org.eclipse.wst.jsdt.internal.ui.JavaPluginImages;

/**
 * Proposals for 'Assign to variable' quick assist
 * - Assign an expression from an ExpressionStatement to a local or field
 * - Assign a parameter to a field
 * */
public class AssignToVariableAssistProposal extends LinkedCorrectionProposal {

	public static final int LOCAL= 1;
	public static final int FIELD= 2;

	private final String KEY_NAME= "name";  //$NON-NLS-1$
	private final String KEY_TYPE= "type";  //$NON-NLS-1$

	private final int  fVariableKind;
	private final ASTNode fNodeToAssign; // ExpressionStatement or SingleVariableDeclaration
	private final ITypeBinding fTypeBinding;

	private VariableDeclarationFragment fExistingFragment;
	
	public AssignToVariableAssistProposal(IJavaScriptUnit cu, int variableKind, ExpressionStatement node, ITypeBinding typeBinding, int relevance) {
		super("", cu, null, relevance, null); //$NON-NLS-1$

		fVariableKind= variableKind;
		fNodeToAssign= node;
		if (typeBinding.isWildcardType()) {
			typeBinding= ASTResolving.normalizeWildcardType(typeBinding, true, node.getAST());
		}
		
		fTypeBinding= typeBinding;
		if (variableKind == LOCAL) {
			setDisplayName(CorrectionMessages.AssignToVariableAssistProposal_assigntolocal_description);
			setImage(JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_LOCAL));
		} else {
			setDisplayName(CorrectionMessages.AssignToVariableAssistProposal_assigntofield_description);
			setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
		}
		createImportRewrite((JavaScriptUnit) node.getRoot());
	}

	public AssignToVariableAssistProposal(IJavaScriptUnit cu, SingleVariableDeclaration parameter, VariableDeclarationFragment existingFragment, ITypeBinding typeBinding, int relevance) {
		super("", cu, null, relevance, null); //$NON-NLS-1$

		fVariableKind= FIELD;
		fNodeToAssign= parameter;
		fTypeBinding= typeBinding;
		fExistingFragment= existingFragment;
		
		if (existingFragment == null) {
			setDisplayName(CorrectionMessages.AssignToVariableAssistProposal_assignparamtofield_description);
		} else {
			setDisplayName(Messages.format(CorrectionMessages.AssignToVariableAssistProposal_assigntoexistingfield_description, existingFragment.getName().getIdentifier()));
		}
		setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
	}

	protected ASTRewrite getRewrite() throws CoreException {
		if (fVariableKind == FIELD) {
			return doAddField();
		} else { // LOCAL
			return doAddLocal();
		}
	}

	private ASTRewrite doAddLocal() throws CoreException {
		Expression expression= ((ExpressionStatement) fNodeToAssign).getExpression();
		AST ast= fNodeToAssign.getAST();

		ASTRewrite rewrite= ASTRewrite.create(ast);
		
		createImportRewrite((JavaScriptUnit) fNodeToAssign.getRoot());

		String[] varNames= suggestLocalVariableNames(fTypeBinding, expression);
		for (int i= 0; i < varNames.length; i++) {
			addLinkedPositionProposal(KEY_NAME, varNames[i], null);
		}

		VariableDeclarationFragment newDeclFrag= ast.newVariableDeclarationFragment();
		newDeclFrag.setName(ast.newSimpleName(varNames[0]));
		newDeclFrag.setInitializer((Expression) rewrite.createCopyTarget(expression));

		// trick for bug 43248: use an VariableDeclarationExpression and keep the ExpressionStatement
		VariableDeclarationExpression newDecl= ast.newVariableDeclarationExpression(newDeclFrag);

		Type type= evaluateType(ast);
		newDecl.setType(type);

		rewrite.replace(expression, newDecl, null);

		addLinkedPosition(rewrite.track(newDeclFrag.getName()), true, KEY_NAME);
		addLinkedPosition(rewrite.track(newDecl.getType()), false, KEY_TYPE);
		setEndPosition(rewrite.track(fNodeToAssign)); // set cursor after expression statement

		return rewrite;
	}

	private ASTRewrite doAddField() throws CoreException {
		boolean isParamToField= fNodeToAssign.getNodeType() == ASTNode.SINGLE_VARIABLE_DECLARATION;

		ASTNode newTypeDecl= ASTResolving.findParentType(fNodeToAssign);
		if (newTypeDecl == null) {
			return null;
		}

		Expression expression= isParamToField ? ((SingleVariableDeclaration) fNodeToAssign).getName() : ((ExpressionStatement) fNodeToAssign).getExpression();

		AST ast= newTypeDecl.getAST();
		ASTRewrite rewrite= ASTRewrite.create(ast);

		createImportRewrite((JavaScriptUnit) fNodeToAssign.getRoot());
		
		BodyDeclaration bodyDecl= ASTResolving.findParentBodyDeclaration(fNodeToAssign);
		Block body;
		if (bodyDecl instanceof FunctionDeclaration) {
			body= ((FunctionDeclaration) bodyDecl).getBody();
		} else if (bodyDecl instanceof Initializer) {
			body= ((Initializer) bodyDecl).getBody();
		} else {
			return null;
		}

		boolean isAnonymous= newTypeDecl.getNodeType() == ASTNode.ANONYMOUS_CLASS_DECLARATION;
		boolean isStatic= Modifier.isStatic(bodyDecl.getModifiers()) && !isAnonymous;
		boolean isConstructorParam= isParamToField && fNodeToAssign.getParent() instanceof FunctionDeclaration && ((FunctionDeclaration) fNodeToAssign.getParent()).isConstructor();
		int modifiers= Modifier.PRIVATE;
		if (isStatic) {
			modifiers |= Modifier.STATIC;
		} else if (isConstructorParam) {
			modifiers |= Modifier.FINAL;
		}

		VariableDeclarationFragment newDeclFrag= addFieldDeclaration(rewrite, newTypeDecl, modifiers, expression);
		String varName= newDeclFrag.getName().getIdentifier();

		Assignment assignment= ast.newAssignment();
		assignment.setRightHandSide((Expression) rewrite.createCopyTarget(expression));

		boolean needsThis= StubUtility.useThisForFieldAccess(getCompilationUnit().getJavaScriptProject());
		if (isParamToField) {
			needsThis |= varName.equals(((SimpleName) expression).getIdentifier());
		}

		SimpleName accessName= ast.newSimpleName(varName);
		if (needsThis) {
			FieldAccess fieldAccess= ast.newFieldAccess();
			fieldAccess.setName(accessName);
			if (isStatic) {
				String typeName= ((AbstractTypeDeclaration) newTypeDecl).getName().getIdentifier();
				fieldAccess.setExpression(ast.newSimpleName(typeName));
			} else {
				fieldAccess.setExpression(ast.newThisExpression());
			}
			assignment.setLeftHandSide(fieldAccess);
		} else {
			assignment.setLeftHandSide(accessName);
		}

		ASTNode selectionNode;
		if (isParamToField) {
			// assign parameter to field
			ExpressionStatement statement= ast.newExpressionStatement(assignment);
			int insertIdx=  findAssignmentInsertIndex(body.statements());
			rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, insertIdx, null);
			selectionNode= statement;

		} else {
			rewrite.replace(expression, assignment, null);
			selectionNode= fNodeToAssign;
		}

		addLinkedPosition(rewrite.track(newDeclFrag.getName()), false, KEY_NAME);
		if (!isParamToField) {
			FieldDeclaration fieldDeclaration= (FieldDeclaration) newDeclFrag.getParent();
			addLinkedPosition(rewrite.track(fieldDeclaration.getType()), false, KEY_TYPE);
		}
		addLinkedPosition(rewrite.track(accessName), true, KEY_NAME);
		setEndPosition(rewrite.track(selectionNode));

		return rewrite;
	}

	private VariableDeclarationFragment addFieldDeclaration(ASTRewrite rewrite, ASTNode newTypeDecl, int modifiers, Expression expression) throws CoreException {
		if (fExistingFragment != null) {
			return fExistingFragment;
		}
		
		ChildListPropertyDescriptor property= ASTNodes.getBodyDeclarationsProperty(newTypeDecl);
		List decls= (List) newTypeDecl.getStructuralProperty(property);
		AST ast= newTypeDecl.getAST();
		String[] varNames= suggestFieldNames(fTypeBinding, expression, modifiers);
		for (int i= 0; i < varNames.length; i++) {
			addLinkedPositionProposal(KEY_NAME, varNames[i], null);
		}
		String varName= varNames[0];

		VariableDeclarationFragment newDeclFrag= ast.newVariableDeclarationFragment();
		newDeclFrag.setName(ast.newSimpleName(varName));

		FieldDeclaration newDecl= ast.newFieldDeclaration(newDeclFrag);

		Type type= evaluateType(ast);
		newDecl.setType(type);
		newDecl.modifiers().addAll(ASTNodeFactory.newModifiers(ast, modifiers));
		
		ModifierCorrectionSubProcessor.installLinkedVisibilityProposals(getLinkedProposalModel(), rewrite, newDecl.modifiers(), false);

		int insertIndex= findFieldInsertIndex(decls, fNodeToAssign.getStartPosition());
		rewrite.getListRewrite(newTypeDecl, property).insertAt(newDecl, insertIndex, null);
		
		return newDeclFrag;
	}
	
	
	private Type evaluateType(AST ast) throws CoreException {
		ITypeBinding[] proposals= ASTResolving.getRelaxingTypes(ast, fTypeBinding);
		for (int i= 0; i < proposals.length; i++) {
			addLinkedPositionProposal(KEY_TYPE, proposals[i]);
		}
		return getImportRewrite().addImport(fTypeBinding, ast);
	}

	private String[] suggestLocalVariableNames(ITypeBinding binding, Expression expression) {
		IJavaScriptProject project= getCompilationUnit().getJavaScriptProject();
		return StubUtility.getVariableNameSuggestions(StubUtility.LOCAL, project, binding, expression, getUsedVariableNames());
	}

	private String[] suggestFieldNames(ITypeBinding binding, Expression expression, int modifiers) {
		IJavaScriptProject project= getCompilationUnit().getJavaScriptProject();
		int varKind= Modifier.isStatic(modifiers) ? StubUtility.STATIC_FIELD : StubUtility.INSTANCE_FIELD;
		return StubUtility.getVariableNameSuggestions(varKind, project, binding, expression, getUsedVariableNames());
	}

	private Collection getUsedVariableNames() {
		return Arrays.asList(ASTResolving.getUsedVariableNames(fNodeToAssign));
	}

	private int findAssignmentInsertIndex(List statements) {

		HashSet paramsBefore= new HashSet();
		List params = ((FunctionDeclaration) fNodeToAssign.getParent()).parameters();
		for (int i = 0; i < params.size() && (params.get(i) != fNodeToAssign); i++) {
			SingleVariableDeclaration decl= (SingleVariableDeclaration) params.get(i);
			paramsBefore.add(decl.getName().getIdentifier());
		}

		int i= 0;
		for (i = 0; i < statements.size(); i++) {
			Statement curr= (Statement) statements.get(i);
			switch (curr.getNodeType()) {
				case ASTNode.CONSTRUCTOR_INVOCATION:
				case ASTNode.SUPER_CONSTRUCTOR_INVOCATION:
					break;
				case ASTNode.EXPRESSION_STATEMENT:
					Expression expr= ((ExpressionStatement) curr).getExpression();
					if (expr instanceof Assignment) {
						Assignment assignment= (Assignment) expr;
						Expression rightHand = assignment.getRightHandSide();
						if (rightHand instanceof SimpleName && paramsBefore.contains(((SimpleName) rightHand).getIdentifier())) {
							IVariableBinding binding = Bindings.getAssignedVariable(assignment);
							if (binding == null || binding.isField()) {
								break;
							}
						}
					}
					return i;
				default:
					return i;

			}
		}
		return i;

	}

	private int findFieldInsertIndex(List decls, int currPos) {
		for (int i= decls.size() - 1; i >= 0; i--) {
			ASTNode curr= (ASTNode) decls.get(i);
			if (curr instanceof FieldDeclaration && currPos > curr.getStartPosition() + curr.getLength()) {
				return i + 1;
			}
		}
		return 0;
	}

	/**
	 * Returns the variable kind.
	 * @return int
	 */
	public int getVariableKind() {
		return fVariableKind;
	}


}
