/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.gui.workflow.execute;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
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.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.ide.FileStoreEditorInput;
import org.eclipse.ui.part.FileEditorInput;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionService;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowFileException;
import de.rcenvironment.core.component.workflow.model.api.WorkflowDescription;
import de.rcenvironment.core.gui.workflow.GUIWorkflowDescriptionLoaderCallback;
import de.rcenvironment.core.gui.workflow.editor.WorkflowEditor;
import de.rcenvironment.core.utils.common.CrossPlatformFilenameUtils;
import de.rcenvironment.core.utils.common.InvalidFilenameException;
import de.rcenvironment.core.utils.common.JsonUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.ServiceRegistry;

/**
 * Opens the {@link WorkflowExecutionWizard}.
 * 
 * @author Christian Weiss
 * @author Sascha Zur
 * @author Doreen Seider
 */
public class ShowWorkflowExecutionWizardHandler extends AbstractHandler {

    private static final String DUPLICATE_ID_WARNING_MSG = "Could not determine duplicate WF ids";

    private static final String GETTING_ATTR_WARNING_MSG = " - failed to get file attributes for: ";

    private static final String NODES = "nodes";

    private static final String IDENTIFIER = "identifier";

    private static final Pattern WORKFLOW_FILENAME_PATTERN = Pattern.compile("^.*\\.wf$");

    private static final Log LOGGER = LogFactory.getLog(ShowWorkflowExecutionWizardHandler.class);

    private final ObjectMapper mapper = JsonUtils.getDefaultObjectMapper();

    private WorkflowExecutionService workflowExecutionService;

    public ShowWorkflowExecutionWizardHandler() {
        workflowExecutionService = ServiceRegistry.createPublisherAccessFor(this).getService(WorkflowExecutionService.class);
    }

    @Override
    public Object execute(final ExecutionEvent event) throws ExecutionException {

        final boolean wfFileOpenedInEditor;

        IFile wfFile = tryToGetWorkflowFileFromProjectExplorer(event);
        

        if (wfFile == null) {
            wfFile = tryToGetWorkflowFileFromWorkflowEditor();
            if (wfFile == null) {
                return null;
            } else {
                wfFileOpenedInEditor = true;
            }
        } else if (!checkForOpenRelatedDirtyWorkflowEditor(wfFile)) {
            return null;
        } else {
            wfFileOpenedInEditor = false;
        }

        searchAndReplaceDuplicateIDs(wfFile);

        // Check if the filename is valid. If this is not the case, execution may fail. 
        try {
            CrossPlatformFilenameUtils.throwExceptionIfFilenameNotValid(wfFile.getName());
        } catch (InvalidFilenameException e) {
            MessageDialog.open(MessageDialog.WARNING, null, "Error during Workflow preparation",
                "The workflow's file name is not valid on all supported platforms. Please rename the workflow.", SWT.NONE);
            return null;
        }
        
        if (wfFileOpenedInEditor) {
            openWorkflowExecutionWizardWithWfFileFromEditor(wfFile);
        } else {
            openWorkflowExecutionWizardWithWfFileFromProjectExplorer(wfFile);
        }
        return null;
    }

    private void openWorkflowExecutionWizardWithWfFileFromEditor(IFile wfFile) {
        WorkflowDescription wfDescription = null;
        try {
            File wfFile2 = new File(wfFile.getLocation().toOSString());
            if (wfFile2.exists()) {
                wfDescription = workflowExecutionService.loadWorkflowDescriptionFromFile(
                    wfFile2, new GUIWorkflowDescriptionLoaderCallback());
            }
        } catch (RuntimeException | WorkflowFileException e) {
            // caught and only logged as an error dialog already pops up if an error occur
            LogFactory.getLog(getClass()).error("Failed to load workflow: " + wfFile.getLocation().toOSString(), e);
            return;
        }
        if (wfDescription != null) {
            openWorkflowExecutionWizard(wfFile, wfDescription);

        } else {
            MessageDialog.open(MessageDialog.WARNING, null, "Error Loading Workflow",
                "The workflow file could not be found.\nMaybe it was renamed?", SWT.NONE);
        }
    }

    private void openWorkflowExecutionWizardWithWfFileFromProjectExplorer(IFile wfFile) {
        final IFile finalWfFile = wfFile;
        Job job = new Job("Executing workflow") {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    monitor.beginTask("Loading workflow components", 2);
                    monitor.worked(1);
                    final WorkflowDescription wfDescription = workflowExecutionService
                        .loadWorkflowDescriptionFromFileConsideringUpdates(
                            new File(finalWfFile.getRawLocation().toOSString()), new GUIWorkflowDescriptionLoaderCallback());
                    monitor.worked(1);
                    Display.getDefault().asyncExec(new Runnable() {

                        @Override
                        public void run() {
                            openWorkflowExecutionWizard(finalWfFile, wfDescription);
                        }
                    });
                } catch (final WorkflowFileException e) {
                    // caught and only logged as an error dialog already pops up if an error occur
                    LogFactory.getLog(getClass()).error("Failed to load workflow: " + finalWfFile.getRawLocation().toOSString(), e);
                    Display.getDefault().asyncExec(new Runnable() {

                        @Override
                        public void run() {
                            // do not use Display.getDefault().getActiveShell() as this might return
                            // the progress monitor dialog
                            MessageDialog.openError(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
                                "Workflow File Error", e.getMessage());
                        }
                    });
                } finally {
                    monitor.done();
                }
                return Status.OK_STATUS;
            };

        };
        job.setUser(true);
        job.schedule();
    }

    private void openWorkflowExecutionWizard(IFile wfFile, WorkflowDescription wfDescription) {
        final Wizard wfExecutionWizard = new WorkflowExecutionWizard(wfFile, wfDescription);
        // do not use Display.getDefault().getActiveShell() as this might return the progress
        // monitor dialog
        final WorkflowWizardDialog wizardDialog = new WorkflowWizardDialog(
            PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), wfExecutionWizard);
        wizardDialog.setBlockOnOpen(false);
        wizardDialog.open();
    }

    private boolean checkForOpenRelatedDirtyWorkflowEditor(IFile wfFile) {
        // Compares the project explorer selection with dirty editors and triggers saveChangesDialog
        // if selection is dirty
        IEditorReference[] currentEditors = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getEditorReferences();
        for (IEditorReference editor : currentEditors) {
            IEditorPart editorPart = (IEditorPart) editor.getPart(true);
            if (editorPart instanceof WorkflowEditor) {
                WorkflowEditor workflowEditor = (WorkflowEditor) editorPart;
                IFile editorFile = ((FileEditorInput) workflowEditor.getEditorInput()).getFile();
                if (wfFile.getProject().equals(editorFile.getProject())) {
                    if (wfFile.getName().equals(editorPart.getTitle()) && editorPart.isDirty()) {
                        return saveChangesDialog(editorPart);
                    }
                }
            }
        }
        return true;
    }

    private IFile tryToGetWorkflowFileFromWorkflowEditor() {
        final IWorkbenchPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
        if (part instanceof IEditorPart) {
            IEditorPart editor = (IEditorPart) part;
            if (editor instanceof WorkflowEditor) {
                WorkflowEditor workflowEditor = (WorkflowEditor) editor;
                IEditorInput input = workflowEditor.getEditorInput();
                if (input instanceof FileEditorInput) {
                    if (!workflowEditor.isDirty() || saveChangesDialog(workflowEditor)) {
                        return ((FileEditorInput) input).getFile();
                    }
                } else if (input instanceof FileStoreEditorInput) {
                    MessageDialog.openInformation(part.getSite().getShell(), "Workflow Run",
                        "Workflow file can not be executed. Please put the workflow into a project of your workspace first.\n\n"
                            + "Drag the workflow from the file system into the project explorer to a project of your choice. "
                            + "You can decide to either copy or only link it.\n\nIf you don't have a project yet, "
                            + "create one first via File->New->Project->General.");
                }
            }
        }
        return null;
    }

    private IFile tryToGetWorkflowFileFromProjectExplorer(final ExecutionEvent event) {
        final ISelection selection = HandlerUtil.getCurrentSelection(event);
        if (selection instanceof IStructuredSelection) {
            final IStructuredSelection structuredSelection = (IStructuredSelection) selection;
            for (Iterator<?> iter = structuredSelection.iterator(); iter.hasNext();) {
                Object next = iter.next();
                if (!(next instanceof IFile)) {
                    continue;
                }
                final IFile file = (IFile) next;
                String filename = file.getName();
                if (!WORKFLOW_FILENAME_PATTERN.matcher(filename).matches()) {
                    continue;
                }
                return file;
            }
        }
        return null;
    }

    private boolean saveChangesDialog(IEditorPart editor) {
        final IWorkbenchPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
        if (MessageDialog.openConfirm(part.getSite().getShell(), Messages.askToSaveUnsavedEditorChangesTitle,
            StringUtils.format(Messages.askToSaveUnsavedEditorChangesMessage, editor.getTitle()))) {
            editor.doSave(null);
            return true;
        }
        return false;

    }

    private void searchAndReplaceDuplicateIDs(IFile workflowFile) {
        String absoluteFilePath = workflowFile.getLocation().toString();
        Set<IResource> wfsWithDuplicateId = searchForDuplicateWFIdentifier(absoluteFilePath);
        if (wfsWithDuplicateId != null && !wfsWithDuplicateId.isEmpty()) {
            try {
                Path orig = Paths.get(absoluteFilePath);
                BasicFileAttributeView origAttrView = Files.getFileAttributeView(orig, BasicFileAttributeView.class);
                if (origAttrView == null) {
                    LOGGER.warn(DUPLICATE_ID_WARNING_MSG + GETTING_ATTR_WARNING_MSG + absoluteFilePath);
                    return;
                }
                BasicFileAttributes attrOrig = origAttrView.readAttributes();
                for (IResource duplicate : wfsWithDuplicateId) {
                    Path duplicatePath = Paths.get(duplicate.getLocationURI());
                    BasicFileAttributeView attrViewDuplicate = Files.getFileAttributeView(duplicatePath, BasicFileAttributeView.class);
                    if (attrViewDuplicate == null) {
                        LOGGER.warn(DUPLICATE_ID_WARNING_MSG + GETTING_ATTR_WARNING_MSG + absoluteFilePath);
                        continue;
                    }
                    BasicFileAttributes attrDuplicate = attrViewDuplicate.readAttributes();

                    // Comparing the creation time is buggy on linux machines.
                    // It returns the modification time which can be the same as in the original and
                    // thus the original file is modified, doesn't find its placeholder and
                    // falls back to an old placeholder
                    // The bug is fixed in java 8:
                    // http://hg.openjdk.java.net/jdk8/jdk8/jdk/rev/296c9ec816c6
                    if (attrOrig.creationTime().compareTo(attrDuplicate.creationTime()) > 0) {
                        replaceIdentifierInWorkflowFile(absoluteFilePath);
                        workflowFile.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
                    } else {
                        replaceIdentifierInWorkflowFile(duplicate.getLocation().toString());
                        duplicate.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
                    }
                }
            } catch (CoreException e) {
                LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
            } catch (IOException e) {
                LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void replaceIdentifierInWorkflowFile(String filePath) {

        try {
            File wfToEdit = new File(filePath);
            Map<String, Object> wfContent = mapper.readValue(wfToEdit, new HashMap<String, Object>().getClass());

            String wfContentString = FileUtils.readFileToString(wfToEdit);

            String idWorkflow = (String) wfContent.get(IDENTIFIER);
            wfContentString = wfContentString.replace(idWorkflow, UUID.randomUUID().toString());
            List<Map<String, Object>> nodes = (List<Map<String, Object>>) wfContent.get(NODES);
            if (nodes != null) {
                for (Map<String, Object> node : nodes) {
                    String nodeID = (String) node.get(IDENTIFIER);
                    wfContentString = wfContentString.replaceAll(nodeID, UUID.randomUUID().toString());
                }
            }
            FileUtils.write(wfToEdit, wfContentString);
        } catch (IOException e) {
            LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
        }
    }

    private Set<IResource> searchForDuplicateWFIdentifier(String absoluteFilePath) {
        IWorkspace workspace = ResourcesPlugin.getWorkspace();
        IWorkspaceRoot root = workspace.getRoot();
        Set<IResource> duplicates = new HashSet<IResource>();
        try {
            File file = new File(absoluteFilePath);
            if (file.exists()) {
                @SuppressWarnings("unchecked") Map<String, Object> wfContent =
                    mapper.readValue(file, new HashMap<String, Object>().getClass());

                for (IProject p : root.getProjects()) {
                    if (p.isOpen()) {
                        try {
                            for (IResource res : p.members()) {
                                if (res instanceof IFile && WORKFLOW_FILENAME_PATTERN
                                    .matcher(((IFile) res).getName()).matches()) {
                                    IResource possibleDuplicate = checkFileForDuplicateID(wfContent, absoluteFilePath, res);
                                    if (possibleDuplicate != null && !absoluteFilePath.equals(possibleDuplicate.getLocation().toString())) {
                                        duplicates.add(possibleDuplicate);
                                    }
                                }
                                if (res instanceof IFolder) {
                                    Set<IResource> possibleDuplicate = checkFolderForDuplicateId(wfContent, absoluteFilePath, res);
                                    if (possibleDuplicate != null) {
                                        duplicates.addAll(possibleDuplicate);
                                    }
                                }
                            }
                        } catch (CoreException e) {
                            LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
                        } catch (IOException e) {
                            LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
                        }
                    }
                }
            }
        } catch (IOException e) {
            LOGGER.warn(DUPLICATE_ID_WARNING_MSG, e);
        }
        return duplicates;
    }

    private Set<IResource> checkFolderForDuplicateId(Map<String, Object> wfContent, String absoluteFilePath, IResource res)
        throws JsonParseException,
        JsonMappingException, IOException, CoreException {
        Set<IResource> possibleDuplicates = new HashSet<IResource>();
        for (IResource resource : ((IFolder) res).members()) {
            if (resource instanceof IFolder) {
                Set<IResource> possibleDuplicate = checkFolderForDuplicateId(wfContent, absoluteFilePath, resource);
                if (possibleDuplicate != null) {
                    possibleDuplicates.addAll(possibleDuplicate);
                }
            } else if (resource instanceof IFile
                && WORKFLOW_FILENAME_PATTERN.matcher(((IFile) resource).getName()).matches()) {
                IResource possibleDuplicate = checkFileForDuplicateID(wfContent, absoluteFilePath, resource);
                if (possibleDuplicate != null && !absoluteFilePath.equals(possibleDuplicate.getLocation().toString())) {
                    possibleDuplicates.add(possibleDuplicate);
                }
            }
        }
        return possibleDuplicates;
    }

    @SuppressWarnings("unchecked")
    private IResource checkFileForDuplicateID(Map<String, Object> wfContent, String absoluteFilePath, IResource res) {
        try {
            File file = new File(res.getLocation().toString());
            if (file.exists()) {
                Map<String, Object> wfContentExisting =
                    mapper.readValue(file, new HashMap<String, Object>().getClass());
                if (!absoluteFilePath.equals(res.getLocation().toString())
                    && wfContent.get(IDENTIFIER).equals(wfContentExisting.get(IDENTIFIER))) {
                    return res;
                }
            }
        } catch (IOException e) {
            LOGGER.warn(StringUtils.format("Skipped corrupted wf file: %s (%s)", e.getMessage(), res.getLocation().toString()));
        }
        return null;
    }

    /**
     * {@link WizardDialog} sub type to be adapted to the needs of a workflow execution.
     * 
     * @author Christian Weiss
     */
    private static final class WorkflowWizardDialog extends WizardDialog {

        /**
         * The Constructor.
         * 
         * @param activeShell the parent shell
         * @param workflowExecutionWizard the wizard this dialog is working on
         */
        WorkflowWizardDialog(Shell activeShell, Wizard workflowExecutionWizard) {
            super(activeShell, workflowExecutionWizard);
        }

        @Override
        protected void createButtonsForButtonBar(Composite parent) {
            super.createButtonsForButtonBar(parent);
            Button okButton = getButton(IDialogConstants.FINISH_ID);
            if (okButton != null) {
                okButton.setText(Messages.executionWizardFinishButtonLabel);
            }
        }

    }

}
