/*******************************************************************************
 * Copyright (c) 2011, 2015 Institute for Software, HSR Hochschule fuer Technik
 * Rapperswil, University of applied sciences 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:
 *     Martin Schwab & Thomas Kallenberg - initial API and implementation
 *     Sergey Prigogin (Google)
 *     Marc-Andre Laperle (Ericsson)
 *     Thomas Corbat (IFS)
 ******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.togglefunction;

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

import org.eclipse.cdt.core.CCProjectNature;
import org.eclipse.cdt.core.dom.ast.ASTNodeFactoryFactory;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.IASTArrayModifier;
import org.eclipse.cdt.core.dom.ast.IASTComment;
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.IASTEnumerationSpecifier.IASTEnumerator;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFunctionDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTFunctionDefinition;
import org.eclipse.cdt.core.dom.ast.IASTInitializer;
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.IASTParameterDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTPointerOperator;
import org.eclipse.cdt.core.dom.ast.IASTProblem;
import org.eclipse.cdt.core.dom.ast.IASTSimpleDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IASTTypeId;
import org.eclipse.cdt.core.dom.ast.c.ICASTDesignator;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCapture;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCatchHandler;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompositeTypeSpecifier.ICPPASTBaseSpecifier;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFunctionDefinition;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFunctionWithTryBlock;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTName;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTNameSpecifier;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTNamespaceDefinition;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTQualifiedName;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTemplateParameter;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPNodeFactory;
import org.eclipse.cdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.cdt.core.dom.rewrite.ASTRewrite.CommentPosition;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTNamespaceDefinition;
import org.eclipse.cdt.internal.core.dom.rewrite.ASTLiteralNode;
import org.eclipse.cdt.internal.ui.refactoring.Container;
import org.eclipse.cdt.internal.ui.refactoring.ModificationCollector;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.text.edits.TextEditGroup;

public class ToggleFromInHeaderToImplementationStrategy implements IToggleRefactoringStrategy {
	private class NamespaceFinderVisitor extends ASTVisitor {
		private final List<ICPPASTNamespaceDefinition> namespaces;
		private final Container<IASTNode> result;
		protected int namespaceIndex = -1;
		protected int deepestMatch = -1;

		private NamespaceFinderVisitor(List<ICPPASTNamespaceDefinition> namespaces, Container<IASTNode> result) {
			this.namespaces = namespaces;
			this.result = result;
			shouldVisitNamespaces = true;
		}

		@Override
		public int visit(ICPPASTNamespaceDefinition namespaceDefinition) {
			namespaceIndex++;
			String namespaceName = namespaceDefinition.getName().toString();
			if (namespaces.size() > namespaceIndex
					&& namespaces.get(namespaceIndex).getName().toString().equals(namespaceName)) {
				if (namespaceIndex > deepestMatch) {
					result.setObject(namespaceDefinition);
					deepestMatch = namespaceIndex;
				}
				return PROCESS_CONTINUE;
			}
			return PROCESS_SKIP;
		}

		@Override
		public int leave(ICPPASTNamespaceDefinition namespaceDefinition) {
			namespaceIndex--;
			return super.leave(namespaceDefinition);
		}
	}

	private IASTTranslationUnit implAst;
	private ToggleRefactoringContext context;
	private TextEditGroup infoText;
	private ASTLiteralNode includeNode;

	public ToggleFromInHeaderToImplementationStrategy(final ToggleRefactoringContext context) {
		this.infoText = new TextEditGroup(Messages.EditGroupName);
		this.context = context;
	}

	@Override
	public void run(ModificationCollector collector) throws CoreException {
		if (!newFileCheck()) {
			return;
		}
		ICPPASTFunctionDefinition newDefinition = getNewDefinition();
		if (context.getDeclaration() != null) {
			removeDefinitionFromHeader(collector);
		} else {
			replaceDefinitionWithDeclaration(collector);
		}

		ASTRewrite implRewrite = collector.rewriterForTranslationUnit(implAst);
		if (includeNode != null) {
			implRewrite.insertBefore(implAst, null, includeNode, infoText);
		}

		IASTNode insertionParent = implAst.getTranslationUnit();
		List<ICPPASTNamespaceDefinition> namespaces = getSurroundingNamespaces();

		if (!namespaces.isEmpty()) {
			IASTNode namespaceInImplementation = searchNamespaceInImplementation(namespaces);
			if (namespaceInImplementation != null) {
				insertionParent = namespaceInImplementation;
			}
			adaptQualifiedNameToNamespaceLevel(newDefinition, namespaces);

			List<ICPPASTNamespaceDefinition> namespacesToAdd = getNamespacesToAdd(namespaces);
			for (ICPPASTNamespaceDefinition namespace : namespacesToAdd) {
				ICPPASTNamespaceDefinition newNamespace = createNamespace(namespace);
				implRewrite = implRewrite.insertBefore(insertionParent, null, newNamespace, infoText);
				insertionParent = newNamespace;
			}
		}

		newDefinition.setParent(insertionParent);

		IASTNode insertionPoint = findInsertionPoint(insertionParent, context.getDeclarationAST());
		ASTRewrite newRewriter = implRewrite.insertBefore(insertionParent, insertionPoint, newDefinition, infoText);
		copyCommentsToNewFile(newDefinition, newRewriter,
				collector.rewriterForTranslationUnit(context.getDefinitionAST()));
		restoreLeadingComments(newDefinition, newRewriter, collector);
	}

	private void copyCommentsToNewFile(ICPPASTFunctionDefinition newDefinition, final ASTRewrite newRewriter,
			final ASTRewrite oldRewriter) {
		newDefinition.accept(new ASTVisitor(true) {
			@Override
			public int visit(IASTName name) {
				copy(name);
				return super.visit(name);
			}

			@Override
			public int visit(IASTDeclaration declaration) {
				copy(declaration);
				return super.visit(declaration);
			}

			@Override
			public int visit(IASTInitializer initializer) {
				copy(initializer);
				return super.visit(initializer);
			}

			@Override
			public int visit(IASTParameterDeclaration parameterDeclaration) {
				copy(parameterDeclaration);
				return super.visit(parameterDeclaration);
			}

			@Override
			public int visit(IASTDeclarator declarator) {
				copy(declarator);
				return super.visit(declarator);
			}

			@Override
			public int visit(IASTDeclSpecifier declSpec) {
				copy(declSpec);
				return super.visit(declSpec);
			}

			@Override
			public int visit(IASTArrayModifier arrayModifier) {
				copy(arrayModifier);
				return super.visit(arrayModifier);
			}

			@Override
			public int visit(IASTPointerOperator ptrOperator) {
				copy(ptrOperator);
				return super.visit(ptrOperator);
			}

			@Override
			public int visit(IASTExpression expression) {
				copy(expression);
				return super.visit(expression);
			}

			@Override
			public int visit(IASTStatement statement) {
				copy(statement);
				return super.visit(statement);
			}

			@Override
			public int visit(IASTTypeId typeId) {
				copy(typeId);
				return super.visit(typeId);
			}

			@Override
			public int visit(IASTEnumerator enumerator) {
				copy(enumerator);
				return super.visit(enumerator);
			}

			@Override
			public int visit(IASTProblem problem) {
				copy(problem);
				return super.visit(problem);
			}

			@Override
			public int visit(ICPPASTBaseSpecifier baseSpecifier) {
				copy(baseSpecifier);
				return super.visit(baseSpecifier);
			}

			@Override
			public int visit(ICPPASTNamespaceDefinition namespaceDefinition) {
				copy(namespaceDefinition);
				return super.visit(namespaceDefinition);
			}

			@Override
			public int visit(ICPPASTTemplateParameter templateParameter) {
				copy(templateParameter);
				return super.visit(templateParameter);
			}

			@Override
			public int visit(ICPPASTCapture capture) {
				copy(capture);
				return super.visit(capture);
			}

			@Override
			public int visit(ICASTDesignator designator) {
				copy(designator);
				return super.visit(designator);
			}

			private void copy(IASTNode node) {
				copyComments(node, newRewriter, oldRewriter, CommentPosition.leading);
				copyComments(node, newRewriter, oldRewriter, CommentPosition.trailing);
				copyComments(node, newRewriter, oldRewriter, CommentPosition.freestanding);
			}

			private void copyComments(IASTNode node, ASTRewrite newRewriter, ASTRewrite oldRewriter,
					CommentPosition pos) {
				IASTNode originalNode = node.getOriginalNode();
				if (originalNode != node) {
					List<IASTComment> comments = oldRewriter.getComments(originalNode, pos);
					for (IASTComment comment : comments) {
						newRewriter.addComment(node, comment, pos);
					}
				}
			}
		});
	}

	private boolean newFileCheck() throws CoreException {
		implAst = context.getASTForPartnerFile();
		if (implAst == null) {
			IProject project = context.getSelectionTU().getCProject().getProject();
			boolean isCC = project.hasNature(CCProjectNature.CC_NATURE_ID);
			ToggleFileCreator fileCreator = new ToggleFileCreator(context, isCC ? ".cpp" : ".c"); //$NON-NLS-1$ //$NON-NLS-2$
			if (fileCreator.askUserForFileCreation(context)) {
				IFile file = fileCreator.createNewFile();
				implAst = context.getAST(file, null);
				includeNode = new ASTLiteralNode(fileCreator.getIncludeStatement());
				return true;
			} else {
				return false;
			}
		}
		return true;
	}

	private List<ICPPASTNamespaceDefinition> getSurroundingNamespaces() {
		IASTNode toquery = context.getDeclaration();
		if (toquery == null) {
			toquery = context.getDefinition();
		}
		return ToggleNodeHelper.findSurroundingNamespaces(toquery);
	}

	private IASTNode findInsertionPoint(IASTNode insertionParent, IASTTranslationUnit unit) {
		IASTFunctionDeclarator declarator = context.getDeclaration();
		if (unit == null) {
			unit = context.getDefinitionAST();
		}
		if (declarator == null) {
			declarator = context.getDefinition().getDeclarator();
		}
		IASTNode insertion_point = InsertionPointFinder.findInsertionPoint(unit, insertionParent.getTranslationUnit(),
				declarator);
		return insertion_point;
	}

	private void restoreLeadingComments(ICPPASTFunctionDefinition newDefinition, ASTRewrite newRewriter,
			ModificationCollector collector) {
		ASTRewrite rw = collector.rewriterForTranslationUnit(context.getDefinitionAST());
		List<IASTComment> comments = rw.getComments(context.getDefinition(), CommentPosition.leading);
		if (comments != null) {
			for (IASTComment comment : comments) {
				newRewriter.addComment(newDefinition, comment, CommentPosition.leading);
				if (context.getDeclaration() != null) {
					rw.remove(comment, infoText);
				}
			}
		}
	}

	private void replaceDefinitionWithDeclaration(ModificationCollector collector) {
		IASTSimpleDeclaration newdeclarator = ToggleNodeHelper.createDeclarationFromDefinition(context.getDefinition());
		ASTRewrite rewrite = collector.rewriterForTranslationUnit(context.getDefinitionAST());
		rewrite.replace(context.getDefinition(), newdeclarator, infoText);
	}

	private ICPPASTFunctionDefinition getNewDefinition() {
		ICPPASTFunctionDefinition newDefinition = ToggleNodeHelper.createFunctionSignatureWithEmptyBody(
				context.getDefinition().getDeclSpecifier().copy(CopyStyle.withLocations),
				context.getDefinition().getDeclarator().copy(CopyStyle.withLocations),
				context.getDefinition().copy(CopyStyle.withLocations));
		newDefinition.getDeclSpecifier().setInline(false);
		newDefinition.setBody(context.getDefinition().getBody().copy(CopyStyle.withLocations));
		if (newDefinition instanceof ICPPASTFunctionWithTryBlock) {
			ICPPASTFunctionWithTryBlock newTryFun = (ICPPASTFunctionWithTryBlock) newDefinition;
			ICPPASTFunctionWithTryBlock oldTryFun = (ICPPASTFunctionWithTryBlock) context.getDefinition();
			for (ICPPASTCatchHandler catchHandler : oldTryFun.getCatchHandlers()) {
				newTryFun.addCatchHandler(catchHandler.copy(CopyStyle.withLocations));
			}
		}
		return newDefinition;
	}

	private void adaptQualifiedNameToNamespaceLevel(IASTFunctionDefinition new_definition,
			List<ICPPASTNamespaceDefinition> namespaces) {
		if (new_definition.getDeclarator().getName() instanceof ICPPASTQualifiedName && !namespaces.isEmpty()) {
			ICPPNodeFactory nodeFactory = ASTNodeFactoryFactory.getDefaultCPPNodeFactory();
			ICPPASTQualifiedName qname = (ICPPASTQualifiedName) new_definition.getDeclarator().getName();
			ICPPASTName lastNameCopy = nodeFactory.newName(qname.getLastName().toCharArray());
			ICPPASTQualifiedName qname_new = nodeFactory.newQualifiedName(lastNameCopy);
			boolean start = false;
			ICPPASTNameSpecifier[] qualifiers = qname.getQualifier();
			for (int i = 0; i < qualifiers.length; i++) {
				String qualifierName = qualifiers[i].toString();
				if (i < namespaces.size() && qualifierName.equals(namespaces.get(i).getName().toString())) {
					start = true;
				} else if (start) {
					qname_new.addNameSpecifier(qualifiers[i]);
				}
			}
			if (start) {
				new_definition.getDeclarator().setName(qname_new);
			}
		}
	}

	private CPPASTNamespaceDefinition createNamespace(ICPPASTNamespaceDefinition parent_namespace) {
		CPPASTNamespaceDefinition insertionParent = new CPPASTNamespaceDefinition(
				parent_namespace.getName().copy(CopyStyle.withLocations));
		insertionParent.setParent(implAst);
		return insertionParent;
	}

	private void removeDefinitionFromHeader(ModificationCollector collector) {
		ASTRewrite header_rewrite = collector.rewriterForTranslationUnit(context.getDefinitionAST());
		header_rewrite.remove(ToggleNodeHelper.getParentRemovePoint(context.getDefinition()), infoText);
	}

	private IASTNode searchNamespaceInImplementation(final List<ICPPASTNamespaceDefinition> namespaces) {
		final Container<IASTNode> result = new Container<>();
		ASTVisitor visitor = new NamespaceFinderVisitor(namespaces, result);
		this.implAst.accept(visitor);
		return result.getObject();
	}

	private List<ICPPASTNamespaceDefinition> getNamespacesToAdd(final List<ICPPASTNamespaceDefinition> namespaces) {
		final List<ICPPASTNamespaceDefinition> result = new ArrayList<>();
		this.implAst.accept(new NamespaceFinderVisitor(namespaces, new Container<IASTNode>()) {
			{
				shouldVisitTranslationUnit = true;
			}

			@Override
			public int leave(IASTTranslationUnit tu) {
				int startIndex = deepestMatch + 1;
				int namespacesSize = namespaces.size();
				if (startIndex < namespacesSize) {
					result.addAll(namespaces.subList(startIndex, namespacesSize));
				}
				return PROCESS_CONTINUE;
			}
		});
		return result;
	}
}
