/*******************************************************************************
 * Copyright (c) 2008, 2012 Institute for Software, HSR Hochschule fuer Technik  
 * Rapperswil, University of applied sciences 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: 
 *     Institute for Software - initial API and implementation
 *     Marc-Andre Laperle
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.implementmethod;

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

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;

import org.eclipse.cdt.core.dom.ast.ASTNodeFactoryFactory;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.IASTDeclSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTFunctionDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTFunctionDefinition;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTNode.CopyStyle;
import org.eclipse.cdt.core.dom.ast.IASTPointerOperator;
import org.eclipse.cdt.core.dom.ast.IASTSimpleDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTDeclSpecifier;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFunctionDeclarator;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTQualifiedName;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTemplateDeclaration;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTemplateParameter;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunction;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPNodeFactory;
import org.eclipse.cdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.ui.CUIPlugin;

import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPVisitor;

import org.eclipse.cdt.internal.ui.refactoring.CRefactoring;
import org.eclipse.cdt.internal.ui.refactoring.ModificationCollector;
import org.eclipse.cdt.internal.ui.refactoring.utils.Checks;
import org.eclipse.cdt.internal.ui.refactoring.utils.DefinitionFinder;
import org.eclipse.cdt.internal.ui.refactoring.utils.NameHelper;
import org.eclipse.cdt.internal.ui.refactoring.utils.NodeHelper;
import org.eclipse.cdt.internal.ui.refactoring.utils.SelectionHelper;

/**
 * Main class of the ImplementMethodRefactoring (Source generator).
 * Checks conditions, finds insert location and generates the ImplementationNode.
 * 
 * @author Mirko Stocker, Lukas Felber, Emanuel Graf
 */
public class ImplementMethodRefactoring extends CRefactoring {
	private ICPPASTFunctionDeclarator createdMethodDeclarator;
	private ImplementMethodData data;
	private MethodDefinitionInsertLocationFinder methodDefinitionInsertLocationFinder;
	private Map<IASTSimpleDeclaration, InsertLocation> insertLocations;
	private static ICPPNodeFactory nodeFactory = ASTNodeFactoryFactory.getDefaultCPPNodeFactory();
	
	public ImplementMethodRefactoring(ICElement element, ISelection selection, ICProject project) {
		super(element, selection, project);
		data = new ImplementMethodData();
		methodDefinitionInsertLocationFinder = new MethodDefinitionInsertLocationFinder();
		insertLocations = new HashMap<IASTSimpleDeclaration, InsertLocation>();
	}
	
	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
			throws CoreException, OperationCanceledException {
		SubMonitor sm = SubMonitor.convert(pm, 10);
		super.checkInitialConditions(sm.newChild(6));

		if (!initStatus.hasFatalError()) {
			List<IASTSimpleDeclaration> unimplementedMethodDeclarations =
					findUnimplementedMethodDeclarations(pm);
			if (unimplementedMethodDeclarations.isEmpty()) {
				initStatus.addFatalError(Messages.ImplementMethodRefactoring_NoMethodToImplement);
			} else {
				data.setMethodDeclarations(unimplementedMethodDeclarations);

				if (selectedRegion.getLength() > 0) {
					IASTSimpleDeclaration methodDeclaration =
							SelectionHelper.findFirstSelectedDeclaration(selectedRegion, getAST(tu, pm));
					if (NodeHelper.isMethodDeclaration(methodDeclaration)) {
						for (MethodToImplementConfig config : data.getMethodDeclarations()) {
							if (config.getDeclaration() == methodDeclaration) {
								config.setChecked(true);
							}
						}
					}
				}
			}
		}
		sm.done();
		return initStatus;
	}

	private List<IASTSimpleDeclaration> findUnimplementedMethodDeclarations(IProgressMonitor pm)
			throws OperationCanceledException, CoreException {
		final SubMonitor sm = SubMonitor.convert(pm, 2);
		IASTTranslationUnit ast = getAST(tu, sm.newChild(1));
		final List<IASTSimpleDeclaration> list = new ArrayList<IASTSimpleDeclaration>();
		ast.accept(new ASTVisitor() {
			{
				shouldVisitDeclarations = true;
			}

			@Override
			public int visit(IASTDeclaration declaration) {
				if (declaration instanceof IASTSimpleDeclaration) {
					IASTSimpleDeclaration simpleDeclaration = (IASTSimpleDeclaration) declaration;
					if (NodeHelper.isMethodDeclaration(simpleDeclaration)) {
						IASTDeclarator[] declarators = simpleDeclaration.getDeclarators();
						IBinding binding = declarators[0].getName().resolveBinding();
						if (isUnimplementedMethodBinding(binding, sm.newChild(0))) {
							list.add(simpleDeclaration);
							return ASTVisitor.PROCESS_SKIP;	
						}
					}
				}
				return ASTVisitor.PROCESS_CONTINUE;
			}
		});
		return list;
	}

	private boolean isUnimplementedMethodBinding(IBinding binding, IProgressMonitor pm) {
		if (binding instanceof ICPPFunction) {
			if (binding instanceof ICPPMethod) {
				ICPPMethod methodBinding = (ICPPMethod) binding;
				if (methodBinding.isPureVirtual()) {
					return false; // Pure virtual not handled for now, see bug 303870
				}
			}
			
			try {
				return !DefinitionFinder.hasDefinition(binding, refactoringContext, pm);
			} catch (CoreException e) {
				CUIPlugin.log(e);
			}
		}
		
		return false;
	}

	@Override
	protected void collectModifications(IProgressMonitor pm, ModificationCollector collector)
			throws CoreException, OperationCanceledException {
		List<MethodToImplementConfig> methodsToImplement = data.getMethodsToImplement();
		SubMonitor sm = SubMonitor.convert(pm, 4 * methodsToImplement.size());
		for (MethodToImplementConfig config : methodsToImplement) {
			createDefinition(collector, config, sm.newChild(4));
		}
	}

	protected void createDefinition(ModificationCollector collector, MethodToImplementConfig config,
			IProgressMonitor subMonitor) throws CoreException, OperationCanceledException {
		if (subMonitor.isCanceled()) {
			throw new OperationCanceledException();
		}
		IASTSimpleDeclaration decl = config.getDeclaration();
		InsertLocation insertLocation = findInsertLocation(decl, subMonitor);
		if (subMonitor.isCanceled()) {
			throw new OperationCanceledException();
		}
		
		subMonitor.worked(1);
		IASTNode parent = insertLocation.getParentOfNodeToInsertBefore();
		IASTTranslationUnit ast = parent.getTranslationUnit();
		ASTRewrite translationUnitRewrite = collector.rewriterForTranslationUnit(ast);
		subMonitor.worked(1);
		if (subMonitor.isCanceled()) {
			throw new OperationCanceledException();
		}

		IASTNode nodeToInsertBefore = insertLocation.getNodeToInsertBefore();
		IASTNode createdMethodDefinition = createFunctionDefinition(ast, decl, insertLocation);
		subMonitor.worked(1);
		ASTRewrite methodRewrite = translationUnitRewrite.insertBefore(parent, nodeToInsertBefore, createdMethodDefinition , null);
		createParameterModifications(methodRewrite, config.getParaHandler());
		subMonitor.done();
	}
	
	private void createParameterModifications(ASTRewrite methodRewrite, ParameterHandler handler) {
		for (ParameterInfo actParameterInfo : handler.getParameterInfos()) {
			ASTRewrite parameterRewrite = methodRewrite.insertBefore(createdMethodDeclarator, null, actParameterInfo.getParameter(), null);
			createNewNameInsertModification(actParameterInfo, parameterRewrite);
			createRemoveDefaultValueModification(actParameterInfo, parameterRewrite);
		}
	}

	private void createRemoveDefaultValueModification(ParameterInfo parameterInfo, ASTRewrite parameterRewrite) {
		if (parameterInfo.hasDefaultValue()) {
			parameterRewrite.remove(parameterInfo.getDefaultValueNode(), null);
		}
	}

	private void createNewNameInsertModification(ParameterInfo parameterInfo, ASTRewrite parameterRewrite) {
		if (parameterInfo.hasNewName()) {
			IASTNode insertNode = parameterInfo.getNewNameNode();
			IASTName replaceNode = parameterInfo.getNameNode();
			parameterRewrite.replace(replaceNode, insertNode, null);
		}
	}

	private InsertLocation findInsertLocation(IASTSimpleDeclaration methodDeclaration, IProgressMonitor subMonitor) throws CoreException {
		if (insertLocations.containsKey(methodDeclaration)) {
			return insertLocations.get(methodDeclaration);
		}
		InsertLocation insertLocation =
				methodDefinitionInsertLocationFinder.find(tu, methodDeclaration.getFileLocation(),
						methodDeclaration.getParent(), refactoringContext, subMonitor);
		
		if (insertLocation.getTranslationUnit() == null || NodeHelper.isContainedInTemplateDeclaration(methodDeclaration)) {
			insertLocation.setNodeToInsertAfter(NodeHelper.findTopLevelParent(methodDeclaration), tu);
		}
		insertLocations.put(methodDeclaration, insertLocation);
		return insertLocation;
	}

	private IASTDeclaration createFunctionDefinition(IASTTranslationUnit unit, IASTSimpleDeclaration methodDeclaration, InsertLocation insertLocation) throws CoreException {
		IASTDeclSpecifier declSpecifier = methodDeclaration.getDeclSpecifier().copy(CopyStyle.withLocations);
		ICPPASTFunctionDeclarator functionDeclarator = (ICPPASTFunctionDeclarator) methodDeclaration.getDeclarators()[0];
		IASTNode declarationParent = methodDeclaration.getParent();
		
		if (declSpecifier instanceof ICPPASTDeclSpecifier) {
			((ICPPASTDeclSpecifier) declSpecifier).setVirtual(false);
			((ICPPASTDeclSpecifier) declSpecifier).setExplicit(false);
		}
		
		String currentFileName = declarationParent.getNodeLocations()[0].asFileLocation().getFileName();
		if (Path.fromOSString(currentFileName).equals(insertLocation.getFile().getLocation())) {
			declSpecifier.setInline(true);
		}
		
		if (declSpecifier.getStorageClass() == IASTDeclSpecifier.sc_static) {
			declSpecifier.setStorageClass(IASTDeclSpecifier.sc_unspecified);
		}
		
		ICPPASTQualifiedName qName = createQualifiedNameFor(functionDeclarator, declarationParent, insertLocation);
		
		createdMethodDeclarator = nodeFactory.newFunctionDeclarator(qName);
		createdMethodDeclarator.setConst(functionDeclarator.isConst());
		for (IASTPointerOperator pop : functionDeclarator.getPointerOperators()) {
			createdMethodDeclarator.addPointerOperator(pop.copy(CopyStyle.withLocations));
		}
		
		IASTFunctionDefinition functionDefinition = nodeFactory.newFunctionDefinition(declSpecifier, createdMethodDeclarator, nodeFactory.newCompoundStatement());
		functionDefinition.setParent(unit);
		
		ICPPASTTemplateDeclaration templateDeclaration = CPPVisitor.findAncestorWithType(declarationParent, ICPPASTTemplateDeclaration.class);
		if (templateDeclaration != null) {
			ICPPASTTemplateDeclaration newTemplateDeclaration = nodeFactory.newTemplateDeclaration(functionDefinition);
			newTemplateDeclaration.setParent(unit);
			
			for (ICPPASTTemplateParameter templateParameter : templateDeclaration.getTemplateParameters()) {
				newTemplateDeclaration.addTemplateParameter(templateParameter.copy(CopyStyle.withLocations));
			}
			
			return newTemplateDeclaration;
		}
		return functionDefinition;
	}

	private ICPPASTQualifiedName createQualifiedNameFor(IASTFunctionDeclarator functionDeclarator,
			IASTNode declarationParent, InsertLocation insertLocation)	throws CoreException {
		int insertOffset = insertLocation.getInsertPosition();
		return NameHelper.createQualifiedNameFor(
				functionDeclarator.getName(), tu, functionDeclarator.getFileLocation().getNodeOffset(),
				insertLocation.getTranslationUnit(), insertOffset, refactoringContext);
	}
	
	public ImplementMethodData getRefactoringData() {
		return data;
	}

	@Override
	protected RefactoringDescriptor getRefactoringDescriptor() {
		// TODO egraf add Descriptor
		return null;
	}
	
	private IFile[] getAllFilesToModify() {
		List<IFile> files = new ArrayList<IFile>(2);
		IFile file = (IFile) tu.getResource();
		if (file != null) {
			files.add(file);
		}
		
		for (InsertLocation insertLocation : insertLocations.values()) {
			if (insertLocation != null) {
				file = insertLocation.getFile();
				if (file != null) {
					files.add(file);
				}
			}			
		}

		return files.toArray(new IFile[files.size()]);
	}

	@Override
	protected RefactoringStatus checkFinalConditions(IProgressMonitor subProgressMonitor,
			CheckConditionsContext checkContext) throws CoreException, OperationCanceledException {
		RefactoringStatus result = new RefactoringStatus();
		if (isOneOrMoreImplementationInHeader(subProgressMonitor)) {
			result.addInfo(Messages.ImplementMethodRefactoring_NoImplFile);
		}
		Checks.addModifiedFilesToChecker(getAllFilesToModify(), checkContext);
		return result;
	}
	
	private boolean isOneOrMoreImplementationInHeader(IProgressMonitor subProgressMonitor) throws CoreException {
		for (MethodToImplementConfig config : data.getMethodsToImplement()) {
			IASTSimpleDeclaration decl = config.getDeclaration();
			findInsertLocation(decl, subProgressMonitor);	
		}
		
		if (insertLocations.isEmpty()) {
			return true;
		}
		
		for (InsertLocation insertLocation : insertLocations.values()) {
			if (insertLocation != null && tu.equals(insertLocation.getTranslationUnit())) {
				return true;
			}
		}
		return false;
	}
}
