package bndtools.wizards.workspace;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
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.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.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.IStructuredSelection;
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.lib.exceptions.Exceptions;
import aQute.lib.io.IO;
import bndtools.Plugin;
import bndtools.central.Central;

public class WorkspaceSetupWizard extends Wizard implements INewWizard {

    private IWorkbench workbench;

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

    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");
        previewPage = new WorkspacePreviewPage();
        previewPage.setTargetDir(setupPage.getLocation()
            .toFile());

        setupPage.addPropertyChangeListener(WorkspaceLocationPart.PROP_LOCATION, new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                previewPage.setTargetDir(setupPage.getLocation()
                    .toFile());
            }
        });
        templatePage.addPropertyChangeListener(TemplateSelectionWizardPage.PROP_TEMPLATE, new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                Template template = templatePage.getTemplate();
                previewPage.setTemplate(template);
            }
        });
    }

    @Override
    public void addPages() {
        addPage(setupPage);
        addPage(templatePage);
        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 = new IWorkspaceRunnable() {
                @Override
                public void run(IProgressMonitor monitor) throws CoreException {
                    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 subProgress = progress.newChild(1);
                                        project.create(subProgress.newChild(1));
                                        project.open(subProgress.newChild(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 subProgress = progress.newChild(1);

                                        // Open it if closed
                                        project.open(subProgress.newChild(1));
                                        // Refresh, as the template may have generated new content
                                        project.refreshLocal(IResource.DEPTH_INFINITE, subProgress.newChild(1));
                                    }
                                }
                            }
                        }
                        if (cleanBuild)
                            workspace.build(IncrementalProjectBuilder.CLEAN_BUILD, progress.newChild(2));
                    }
                }
            };
            getContainer().run(true, true, new IRunnableWithProgress() {
                @Override
                public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
                    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;
        }

    }

    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()));
        }
    }

}
