package bndtools.wizards.workspace;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;

import org.bndtools.core.ui.wizards.shared.ISkippingWizard;
import org.bndtools.core.ui.wizards.shared.TemplateParamsWizardPage;
import org.bndtools.core.ui.wizards.shared.TemplateSelectionWizardPage;
import org.bndtools.templating.Resource;
import org.bndtools.templating.ResourceMap;
import org.bndtools.templating.Template;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IProjectNature;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.ui.INewWizard;
import org.eclipse.ui.IPerspectiveDescriptor;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;

import aQute.bnd.build.Project;
import aQute.bnd.exceptions.Exceptions;
import aQute.lib.io.IO;
import bndtools.Plugin;
import bndtools.central.Central;

public class WorkspaceSetupWizard extends Wizard implements INewWizard, ISkippingWizard {

	private IWorkbench					workbench;

	private WorkspaceSetupWizardPage	setupPage;
	private TemplateSelectionWizardPage	templatePage;
	private TemplateParamsWizardPage	paramsPage;
	private WorkspacePreviewPage		previewPage;

	static private final String[]		EMPTY_PARAMS	= new String[0];

	public WorkspaceSetupWizard() {
		setNeedsProgressMonitor(true);
	}

	@Override
	public void init(IWorkbench workbench, IStructuredSelection selection) {
		this.workbench = workbench;

		setupPage = new WorkspaceSetupWizardPage();
		updateSetupPageForExistingProjects();
		templatePage = new TemplateSelectionWizardPage("workspaceTemplateSelection", "workspace", null);
		templatePage.setTitle("Select Workspace Template");
		paramsPage = new TemplateParamsWizardPage(EMPTY_PARAMS);
		previewPage = new WorkspacePreviewPage(paramsPage);
		previewPage.setTargetDir(setupPage.getLocation()
			.toFile());

		setupPage.addPropertyChangeListener(WorkspaceLocationPart.PROP_LOCATION,
			evt -> previewPage.setTargetDir(setupPage.getLocation()
				.toFile()));
		templatePage.addPropertyChangeListener(TemplateSelectionWizardPage.PROP_TEMPLATE, evt -> {
			Template template = templatePage.getTemplate();
			paramsPage.setTemplate(template);
			previewPage.setTemplate(template);
		});
	}

	@Override
	public void addPages() {
		addPage(setupPage);
		addPage(templatePage);
		addPage(paramsPage);
		addPage(previewPage);
	}

	@Override
	public boolean performFinish() {
		final IWorkspace workspace = ResourcesPlugin.getWorkspace();

		final File targetDir = previewPage.getTargetDir();
		final Set<String> checkedPaths = previewPage.getCheckedPaths();
		final boolean cleanBuild = setupPage.isCleanBuild();

		try {
			// Expand the template
			ResourceMap outputs = previewPage.getTemplateOutputs();
			final Set<File> topLevelFolders = new HashSet<>();
			for (Entry<String, Resource> entry : outputs.entries()) {
				String path = entry.getKey();
				if (checkedPaths.contains(path)) {
					Resource resource = entry.getValue();

					// Create the folder or file resource
					File file = new File(targetDir, path);
					switch (resource.getType()) {
						case Folder :
							Files.createDirectories(file.toPath());
							break;
						case File :
							File parentDir = file.getParentFile();
							Files.createDirectories(parentDir.toPath());
							try (InputStream in = resource.getContent()) {
								IO.copy(in, file);
							}
							break;
						default :
							throw new IllegalArgumentException("Unknown resource type " + resource.getType());
					}

					// Remember the top-level folders we create, for importing
					// below
					if (file.getParentFile()
						.equals(targetDir))
						topLevelFolders.add(file);
				}
			}

			// Import anything that looks like an Eclipse project & do a full
			// rebuild
			final IWorkspaceRunnable importProjectsRunnable = monitor -> {
				File[] children = targetDir.listFiles();
				if (children != null) {
					int work = children.length;
					if (cleanBuild)
						work += 2;

					SubMonitor progress = SubMonitor.convert(monitor, work);
					for (File folder : children) {
						if (folder.isDirectory() && topLevelFolders.contains(folder)) {
							String projectName = folder.getName();
							File projectFile = new File(folder, IProjectDescription.DESCRIPTION_FILE_NAME);
							if (projectFile.exists()) {
								IProject project = workspace.getRoot()
									.getProject(projectName);
								if (!project.exists()) {

									// No existing project in the workspace, so
									// import the generated project.
									SubMonitor subProgress1 = progress.split(1);
									project.create(subProgress1.split(1));
									project.open(subProgress1.split(1));

									// Now make sure it is associated with the
									// right location
									IProjectDescription description = project.getDescription();
									IPath path = Path.fromOSString(projectFile.getParentFile()
										.getAbsolutePath());
									description.setLocation(path);
									project.move(description, IResource.REPLACE, progress);

								} else {
									// If a project with the same name exists,
									// does it live in the same location? If
									// not, we can't import the generated
									// project.
									File existingLocation = project.getLocation()
										.toFile();
									if (!existingLocation.equals(folder)) {
										String message = String.format(
											"Cannot import generated project from %s. A project named %s already exists in the workspace and is mapped to location %s",
											folder.getAbsolutePath(), projectName, existingLocation);
										throw new CoreException(
											new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, message, null));
									}

									SubMonitor subProgress2 = progress.split(1);

									// Open it if closed
									project.open(subProgress2.split(1));
									// Refresh, as the template may have
									// generated new content
									project.refreshLocal(IResource.DEPTH_INFINITE, subProgress2.split(1));
								}
							}
						}
					}
					if (cleanBuild)
						workspace.build(IncrementalProjectBuilder.CLEAN_BUILD, progress.split(2));
				}
			};
			getContainer().run(true, true, monitor -> {
				try {
					workspace.run(importProjectsRunnable, monitor);
				} catch (CoreException e) {
					throw new InvocationTargetException(e);
				}

				new WorkspaceJob("Load Repositories") {
					@Override
					public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
						try {
							Central.refreshPlugins();
						} catch (Exception e) {
							// There may be no workspace yet
						}
						return Status.OK_STATUS;
					}
				}.schedule();
			});

			// Prompt to switch to the bndtools perspective
			IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
			IPerspectiveDescriptor currentPerspective = window.getActivePage()
				.getPerspective();
			if (!"bndtools.perspective".equals(currentPerspective.getId())) {
				if (MessageDialog.openQuestion(getShell(), "Bndtools Perspective",
					"Switch to the Bndtools perspective?")) {
					workbench.showPerspective("bndtools.perspective", window);
				}
			}
			return true;
		} catch (InvocationTargetException e) {
			ErrorDialog.openError(getShell(), "Error", null, new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0,
				"Error generating template output", Exceptions.unrollCause(e, InvocationTargetException.class)));
			return false;
		} catch (Exception e) {
			ErrorDialog.openError(getShell(), "Error", null,
				new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error generating template output", e));
			return false;
		}

	}

	@Override
	public IWizardPage getUnfilteredPreviousPage(IWizardPage page) {
		return super.getPreviousPage(page);
	}

	@Override
	public IWizardPage getUnfilteredNextPage(IWizardPage page) {
		return super.getNextPage(page);
	}

	@Override
	public IWizardPage getPreviousPage(IWizardPage page) {
		return ISkippingWizard.super.getPreviousPage(page);
	}

	@Override
	public IWizardPage getNextPage(IWizardPage page) {
		return ISkippingWizard.super.getNextPage(page);
	}

	private void updateSetupPageForExistingProjects() {
		// find existing bnd projects
		File existingBndLocation = null;
		for (IProject project : ResourcesPlugin.getWorkspace()
			.getRoot()
			.getProjects()) {
			if (!project.isOpen())
				continue;

			try {
				IProjectNature bndNature = project.getNature(Plugin.BNDTOOLS_NATURE);
				if (bndNature != null || project.getName()
					.equals(Project.BNDCNF)) {
					File projectLocation = project.getLocation()
						.toFile();
					existingBndLocation = projectLocation.getParentFile();
					break;
				}
			} catch (CoreException ex) {
				Plugin.getDefault()
					.getLog()
					.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Failed to load project nature", ex));
			}
		}
		if (existingBndLocation != null) {
			setupPage.setLocation(new LocationSelection(false, existingBndLocation.getAbsolutePath()));
		}
	}

}
