/*************************************************************************************
 * Copyright (c) 2014 Red Hat, Inc. 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:
 *     JBoss by Red Hat - Initial implementation.
 ************************************************************************************/
package org.jboss.tools.arquillian.ui.internal.refactoring;

import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.PROPERTIES;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.createElement;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.findChild;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.format;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.getChild;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.getTextValue;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.performOnDOMDocument;
import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.setText;
import static org.eclipse.m2e.core.ui.internal.editing.PomHelper.addOrUpdateDependency;

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

import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Plugin;
import org.apache.maven.project.MavenProject;
import org.eclipse.compare.rangedifferencer.IRangeComparator;
import org.eclipse.compare.rangedifferencer.RangeDifference;
import org.eclipse.compare.rangedifferencer.RangeDifferencer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.DocumentChange;
import org.eclipse.ltk.core.refactoring.NullChange;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.m2e.core.MavenPlugin;
import org.eclipse.m2e.core.internal.IMavenConstants;
import org.eclipse.m2e.core.project.IMavenProjectFacade;
import org.eclipse.m2e.core.ui.internal.editing.PomEdits;
import org.eclipse.m2e.core.ui.internal.editing.PomEdits.CompoundOperation;
import org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation;
import org.eclipse.m2e.core.ui.internal.editing.PomEdits.OperationTuple;
import org.eclipse.m2e.core.ui.internal.editing.PomHelper;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.jboss.forge.arquillian.container.Container;
import org.jboss.tools.arquillian.core.ArquillianCoreActivator;
import org.jboss.tools.arquillian.core.internal.ArquillianConstants;
import org.jboss.tools.arquillian.core.internal.container.ContainerParser;
import org.jboss.tools.arquillian.core.internal.util.ArquillianUtility;
import org.jboss.tools.arquillian.ui.ArquillianUIActivator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * A refactoring to add Arquillian support to project
 * 
 * @author snjeza
 * 
 */
public class AddArquillianSupportRefactoring extends Refactoring {

	private IProject project;
	private String version;
	private boolean updatePom;
	private boolean addProfiles;
	private boolean updateBuild;
	private boolean updateDependencies;

	/**
	 * @param project
	 */
	public AddArquillianSupportRefactoring(IProject project) {
		super();
		this.project = project;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ltk.core.refactoring.Refactoring#getName()
	 */
	@Override
	public String getName() {
		return "Add Arquillian support";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org
	 * .eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
			throws CoreException, OperationCanceledException {
		boolean isMavenProject = project != null
				&& project.hasNature(IMavenConstants.NATURE_ID);
		if (!isMavenProject) {
			IStatus status = new Status(IStatus.ERROR,
					ArquillianUIActivator.PLUGIN_ID,
					"The project is not a valid maven project");
			return RefactoringStatus.create(status);
		}
		return RefactoringStatus.create(Status.OK_STATUS);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(org
	 * .eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public RefactoringStatus checkFinalConditions(IProgressMonitor pm)
			throws CoreException, OperationCanceledException {
		RefactoringStatus status = checkInitialConditions(pm);
		if (!status.isOK()) {
			return status;
		}
		if (isUpdatePom()) {
			IFile file = getFile();
			if (file == null || !file.exists()) {
				IStatus s = new Status(IStatus.ERROR, ArquillianUIActivator.PLUGIN_ID,
						"The pom.xml file does not exist");
				return RefactoringStatus.create(s);
			}
			IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().create(project, new NullProgressMonitor());
			if (facade == null) {
				IStatus s = new Status(IStatus.ERROR, ArquillianUIActivator.PLUGIN_ID,
						"The project is not a valid maven project");
				return RefactoringStatus.create(s);
			}
		}
		return status;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse
	 * .core.runtime.IProgressMonitor)
	 */
	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException,
			OperationCanceledException {
		if (!isUpdatePom()) {
			return new NullChange();
		}
		IFile file = getFile();
		IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().create(project, new NullProgressMonitor());
		MavenProject mavenProject = facade.getMavenProject(new NullProgressMonitor());
		String version = ArquillianUtility.getArquillianVersion(mavenProject);
		if (version == null) {
			List<Operation> operations = new ArrayList<Operation>();

			Model model = ArquillianUtility.getArquilianModel(true);
			if (isUpdateDependencies()) {
				operations.add(new AddProperties(
						ArquillianUtility.ARQUILLIAN_VERSION, getVersion()));

				List<Dependency> dependencies = model.getDependencies();
				operations.add(new AddDependencies(dependencies, mavenProject));

				DependencyManagement dependencyMgmt = model.getDependencyManagement();
				if (dependencyMgmt != null) {
					dependencies = dependencyMgmt.getDependencies();
					if (dependencies != null || dependencies.size() > 0) {
						operations.add(new AddDependencyManagement(dependencies, mavenProject));
					}
				}
			}
			if (isUpdateBuild()) {
				Build build = model.getBuild();
				operations.add(new AddPlugins(build, file));
			}

			if (isAddProfiles()) {
				operations.add(new AddProfiles(mavenProject));
			}
			CompoundOperation compound = new CompoundOperation(operations.toArray(new Operation[0]));
			
			return createChange(file, compound, getName());
		}
		return new NullChange();
	}

	private Change createChange(IFile file, Operation operation, String label) throws CoreException {
		IStructuredModel model = null;
		try {
			model = StructuredModelManager.getModelManager().getModelForRead(file);
			IDocument document = model.getStructuredDocument();
			boolean existing = isOpened(document);
			IStructuredModel tempModel = StructuredModelManager.getModelManager().createUnManagedStructuredModelFor(
							"org.eclipse.m2e.core.pomFile"); //$NON-NLS-1$
			tempModel.getStructuredDocument().setText(StructuredModelManager.getModelManager(), document.get());
			IDocument tempDocument = tempModel.getStructuredDocument();
			performOnDOMDocument(new OperationTuple((IDOMModel) tempModel, operation));
			TextChange change = createChange(existing ? null : file, document, tempDocument, label);
			return change;
		} catch (Exception e) {
			ArquillianUIActivator.log(e);
			throw new CoreException(new Status(IStatus.ERROR,
					ArquillianUIActivator.PLUGIN_ID,
					"An error occurred creating change", e));
		} finally {
			if (model != null) {
				model.releaseFromRead();
			}
		}
	}
	
	private TextChange createChange(IFile oldFile, IDocument oldDocument,
			IDocument newDocument, String label) {
		TextChange change = oldFile == null ? new DocumentChange(label,
				oldDocument) : new TextFileChange(label, oldFile);
		MultiTextEdit textEdit = new MultiTextEdit();
		change.setEdit(textEdit);
		String newText = newDocument.get();
		String oldText = oldDocument.get();
		if (!newText.equals(oldText)) {
			IRangeComparator right = new RangeComparator(oldText);
			IRangeComparator left = new RangeComparator(newText);
			RangeDifference[] differences = RangeDifferencer.findDifferences(right, left);
			for (RangeDifference difference : differences) {
				int rightStart = difference.rightStart();
				int rightEnd = difference.rightEnd();
				String text = newText.substring(rightStart, rightEnd);
				int leftStart = difference.leftStart();
				int leftLength = difference.leftLength();
				textEdit.addChild(new ReplaceEdit(leftStart, leftLength, text));
			}
		}
		return change;
	}

	private static boolean isOpened(IDocument document) {
		for (IWorkbenchWindow window : PlatformUI.getWorkbench()
				.getWorkbenchWindows()) {
			for (IWorkbenchPage page : window.getPages()) {
				for (IEditorReference ref : page.getEditorReferences()) {
					IEditorPart editor = ref.getEditor(false);
					if (editor != null) {
						IDocument doc = (IDocument) editor
								.getAdapter(IDocument.class);
						if (doc != null && doc.equals(document)) {
							return true;
						}
					}
				}
			}
		}
		return false;
	}

	private IFile getFile() throws CoreException {
		if (project == null || !project.hasNature(IMavenConstants.NATURE_ID)) {
			return null;
		}
		return project.getFile(IMavenConstants.POM_FILE_NAME);
	}

	public String getVersion() {
		return version;
	}

	public void setVersion(String version) {
		this.version = version;
	}

	public boolean isUpdatePom() {
		return updatePom;
	}

	public void setUpdatePom(boolean updatePom) {
		this.updatePom = updatePom;
	}

	public boolean isAddProfiles() {
		return addProfiles;
	}

	public void setAddProfiles(boolean addProfiles) {
		this.addProfiles = addProfiles;
	}

	public IProject getProject() {
		return project;
	}

	private static class AddProperties implements Operation {

		private final String name;

		private final String value;

		public AddProperties(String name, String value) {
			this.name = name;
			this.value = value;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation#process
		 * (org.w3c.dom.Document)
		 */
		public void process(Document document) {
			Element props = getChild(document.getDocumentElement(),
					PROPERTIES);
			
			Element existing = findChild(props, name);
			if (existing == null) {
				existing = document.createElement(name);
				existing.appendChild(document.createTextNode(value));
				props.appendChild(existing);
				PomEdits.format(existing);
			} else {
				setText(existing, value);
			}
		}
	}
	
	private static class AddDependencyManagement implements Operation {

		private List<Dependency> dependencies;
		private MavenProject mavenProject;

		public AddDependencyManagement(List<Dependency> dependencies, MavenProject mavenProject) {
			this.dependencies = dependencies;
			this.mavenProject = mavenProject;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation#process
		 * (org.w3c.dom.Document)
		 */
		public void process(Document document) {
			if (dependencies == null || dependencies.size() <= 0) {
				return;
			}
			List<Dependency> allDependencies = new ArrayList<Dependency>();
			DependencyManagement pdMgmt = mavenProject.getDependencyManagement();
			if (pdMgmt != null) {
				List<Dependency> pmd = pdMgmt.getDependencies();
				if (pmd != null) {
					allDependencies.addAll(pmd);
				}
			}
			Parent parent = mavenProject.getModel().getParent();
			if (parent != null) {
				try {
					ArquillianUtility.addManagedDependencies(allDependencies, parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
				} catch (CoreException e) {
					ArquillianUIActivator.log(e);
				}
			}
			
			Element root = document.getDocumentElement();
			Element dependencyMgmtEl = getChild(root, PomEdits.DEPENDENCY_MANAGEMENT);
			Element dependenciesEl = getChild(dependencyMgmtEl, PomEdits.DEPENDENCIES);
			for (Dependency dependency:dependencies) {
				if (!ArquillianUtility.managedDependencyExists(dependency, allDependencies)) {
					addOrUpdateDependency(dependenciesEl,
							dependency.getGroupId(), dependency.getArtifactId(),
							dependency.getVersion(), dependency.getType(),
							dependency.getScope(), dependency.getClassifier());
				}
			}
		}
	}

	private static class AddPlugins implements Operation {

		private Build build;
		private IFile file;

		public AddPlugins(Build build, IFile file) {
			this.build = build;
			this.file = file;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation#process
		 * (org.w3c.dom.Document)
		 */
		public void process(Document document) {
			if (build == null) {
				return;
			}
			List<Plugin> plugins = build.getPlugins();
			Element root = document.getDocumentElement();
			Element buildEl = getChild(root, PomEdits.BUILD);
			Element pluginsEl = getChild(buildEl, PomEdits.PLUGINS);
			IDOMModel model = null;
			try {
				model = (IDOMModel) StructuredModelManager.getModelManager().getExistingModelForRead(file);
				if (model == null) {
					model = (IDOMModel) StructuredModelManager.getModelManager().getModelForRead(file);
				}
			for (Plugin plugin:plugins) {
				if (!ArquillianUtility.pluginExists(model, plugin)) {
					PomHelper.createPlugin(pluginsEl, plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
				}
			}
			Element compiler = null;
			List<Element> elements = PomEdits.findChilds(pluginsEl, PomEdits.PLUGIN);
			for (Element element:elements) {
				Element pluginArtifactId = findChild(element, PomEdits.ARTIFACT_ID);
				String aid = getTextValue(pluginArtifactId);
				Element pluginGroupId = findChild(element, PomEdits.GROUP_ID);
				String gid = pluginGroupId == null ? ArquillianUtility.MAVEN_GROUP_ID : getTextValue(pluginGroupId);
				if (ArquillianUtility.MAVEN_COMPILER_ARTIFACT_ID.equals(aid) && (ArquillianUtility.MAVEN_GROUP_ID.equals(gid))) {
					compiler = element;
					break;
				}
			}
			ArquillianUtility.fixCompilerPlugin(compiler);
			} catch (Exception e) {
				ArquillianCoreActivator.log(e);
			} finally {
				if (model != null) {
					model.releaseFromRead();
				}
			}
			format(buildEl);
		}
	}
	
	private static class AddProfiles implements Operation {

		private MavenProject mavenProject;
		
		public AddProfiles(MavenProject mavenProject) {
			this.mavenProject = mavenProject;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation#process
		 * (org.w3c.dom.Document)
		 */
		public void process(Document document) {
			List<String> selectedProfiles = ArquillianUtility.getProfilesFromPreferences(ArquillianConstants.SELECTED_ARQUILLIAN_PROFILES);
			if (selectedProfiles == null || selectedProfiles.size() <= 0) {
				return;
			}
			List<Container> selectedContainers = new ArrayList<Container>();
			Model projectModel = mavenProject.getModel();
			List<String> allProfiles;
			try {
				allProfiles = ArquillianUtility.getProfiles(projectModel);
			} catch (CoreException e) {
				ArquillianUIActivator.log(e);
				return;
			}
			for(Container container:ContainerParser.getContainers()) {
				if (selectedProfiles.contains(container.getId()) &&
						!allProfiles.contains(container.getId())) {
					selectedContainers.add(container);
				}
			}
			if (selectedContainers.size() <= 0) {
				return;
			}
			
			Element root = document.getDocumentElement();
			Element profiles = getChild(root, PomEdits.PROFILES);
			for (Container container:selectedContainers) {
				//org.eclipse.m2e.model.edit.pom.Profile profile = ProfileGenerator.getProfile(container);
				generateProfile(profiles, container);
			}
		}

		private void generateProfile(Element profiles, Container container) {
			if (container == null) {
				return;
			}
			String id = container.getId();
			Element profile = createElement(profiles, PomEdits.PROFILE);
			Element idEl = getChild(profile, PomEdits.ID);
			setText(idEl, id);
			Element dependencies = getChild(profile, PomEdits.DEPENDENCIES);
			String version = getVersion(container.getGroup_id(),
					container.getArtifact_id());
			PomHelper.addOrUpdateDependency(dependencies,
					container.getGroup_id(), container.getArtifact_id(),
					version, null, null, null);
			List<org.jboss.forge.arquillian.container.Dependency> deps = container
					.getDependencies();
			if (deps != null) {
				for (org.jboss.forge.arquillian.container.Dependency fd : deps) {
					version = getVersion(fd.getGroup_id(), fd.getArtifact_id());
					PomHelper.addOrUpdateDependency(dependencies,
							fd.getGroup_id(), fd.getArtifact_id(), version,
							null, null, null);
				}
			}
			format(profile);
		}
		
		private String getVersion(String gid, String aid) {
			String coords = gid + ":" + aid + ":[0,)";  //$NON-NLS-1$//$NON-NLS-2$
			return ArquillianUtility.getHighestVersion(coords);
		}
		
	}

	public boolean isUpdateBuild() {
		return updateBuild;
	}

	public void setUpdateBuild(boolean updateBuild) {
		this.updateBuild = updateBuild;
	}

	public boolean isUpdateDependencies() {
		return updateDependencies;
	}

	public void setUpdateDependencies(boolean updateDependencies) {
		this.updateDependencies = updateDependencies;
	}


}
