/*******************************************************************************
 * Copyright (c) 2016, 2018 EfficiOS Inc., Alexandre Montplaisir and others
 *
 * All rights reserved. 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
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ui.project.handlers;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
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.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.internal.tmf.ui.project.operations.NewExperimentOperation;
import org.eclipse.tracecompass.internal.tmf.ui.project.operations.TmfWorkspaceModifyOperation;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.snapshot.StateSnapshot;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.component.ITmfEventProvider;
import org.eclipse.tracecompass.tmf.core.project.model.TmfTraceType;
import org.eclipse.tracecompass.tmf.core.project.model.TraceTypeHelper;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfAnalysisModuleWithStateSystems;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.core.trace.trim.ITmfTrimmableTrace;
import org.eclipse.tracecompass.tmf.ui.project.model.ITmfProjectModelElement;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfCommonProjectElement;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfExperimentElement;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfExperimentFolder;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfOpenTraceHelper;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfProjectElement;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfProjectRegistry;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfTraceElement;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfTraceFolder;
import org.eclipse.tracecompass.tmf.ui.project.model.TmfTraceTypeUIUtils;
import org.eclipse.tracecompass.tmf.ui.project.model.TraceUtils;
import org.eclipse.tracecompass.tmf.ui.project.wizards.TrimTraceDialog;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.handlers.IHandlerService;

import com.google.common.collect.Lists;

/**
 * Handler for the Trace Trim operation.
 *
 * @author Alexandre Montplaisir
 */
@NonNullByDefault
public class TrimTraceHandler extends AbstractHandler {

    private static final class TraceToTrim {
        private ITmfTrace fTrace;
        private @Nullable ITmfTrimmableTrace fTrimmable;
        private Path fDestinationPath;
        private Collection<TraceToTrim> fChildren = new ArrayList<>();
        private TmfCommonProjectElement fElement;
        private @Nullable IFolder fFolder = null;
        private @Nullable ITmfProjectModelElement fDestElement = null;
        private TmfCommonProjectElement fSourceElement;

        public static @Nullable TraceToTrim create(TmfCommonProjectElement element, Path destination) {
            ITmfTrace trace = element.getTrace();
            if (trace != null) {
                return new TraceToTrim(trace, element, element, destination);
            }
            if (element instanceof TmfTraceElement) {
                TmfTraceElement traceElement = (TmfTraceElement) element;
                trace = traceElement.getTrace();

                if (trace != null) {
                    return new TraceToTrim(trace, element, element, destination);
                }
            }
            return null;
        }

        private static final @Nullable TraceToTrim create(TraceToTrim parent, TmfTraceElement traceElement) {
            TmfTraceElement elementUnderTraceFolder = traceElement.getElementUnderTraceFolder();
            ITmfTrace trace = traceElement.getTrace();
            if (trace != null) {
                List<String> elems = new ArrayList<>();
                ITmfProjectModelElement tempTraceElement = elementUnderTraceFolder;
                while (tempTraceElement != null) {
                    elems.add(Objects.requireNonNull(tempTraceElement.getName()));
                    tempTraceElement = tempTraceElement.getParent();
                }
                TraceTypeHelper traceType = TmfTraceType.getTraceType(elementUnderTraceFolder.getTraceType());
                String end = elems.remove(0);
                elems = Lists.reverse(elems);
                TmfTraceFolder tracesFolder = elementUnderTraceFolder.getProject().getTracesFolder();
                if (tracesFolder == null) {
                    return null;
                }
                IFolder folder = tracesFolder.getResource();
                // remove project name
                elems.remove(0);
                // remove "Traces"
                elems.set(0, String.valueOf(parent.fDestinationPath.getFileName()));
                for (String elem : elems) {
                    folder = folder.getFolder(elem);
                    if (!folder.exists()) {
                        try {
                            folder.create(true, true, new NullProgressMonitor());
                        } catch (CoreException e) {
                            return null;
                        }
                    }
                }
                folder = folder.getFolder(end);
                if (!traceType.isDirectoryTraceType() && !folder.exists()) {
                    try {
                        folder.create(true, true, new NullProgressMonitor());
                        folder.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
                    } catch (CoreException e) {
                        return null;
                    }
                }
                TmfTraceElement te = new TmfTraceElement(elementUnderTraceFolder.getName(), folder, tracesFolder) {
                    @Override
                    public String getTraceType() {
                        return String.valueOf(traceElement.getTraceType());
                    }

                    @Override
                    public void refreshTraceType() {
                        // Do nothing, we have the tracetype above
                    }

                    @Override
                    public IResource[] getSupplementaryResources() {
                        return traceElement.getSupplementaryResources();
                    }
                };
                return new TraceToTrim(trace, elementUnderTraceFolder, te, (new File(te.getName())).toPath());
            }
            return null;
        }

        public TraceToTrim(ITmfTrace trace, TmfCommonProjectElement sourceElement, TmfCommonProjectElement element, Path destination) {
            fTrace = trace;
            fSourceElement = sourceElement;
            fElement = element;
            if (trace instanceof ITmfTrimmableTrace) {
                fTrimmable = (ITmfTrimmableTrace) trace;
            } else {
                fTrimmable = null;
            }
            fFolder = (IFolder) element.getResource();
            // Note: getParent.getLocation is almost never a link. The
            // folder.getLocation may be a link.
            fDestinationPath = fFolder != null ? fFolder.getParent().getLocation().append(destination.toString()).toFile().toPath() : destination;
            // go through children
            for (TmfTraceElement childElement : element.getTraces()) {
                TraceToTrim create = TraceToTrim.create(this, childElement);
                if (create != null) {
                    fChildren.add(create);
                }
            }
        }

        public IStatus trim(TmfTimeRange tr, IProgressMonitor mon) {
            if (mon.isCanceled()) {
                return Status.CANCEL_STATUS;
            }
            SubMonitor monitor = SubMonitor.convert(mon, 3);
            List<@NonNull ITmfAnalysisModuleWithStateSystems> statesystemModules = new ArrayList<>();
            for (IAnalysisModule module : fTrace.getAnalysisModules()) {
                if (module instanceof ITmfAnalysisModuleWithStateSystems) {
                    statesystemModules.add((ITmfAnalysisModuleWithStateSystems) module);
                }
            }
            monitor.worked(1);
            long snapshotTime = tr.getStartTime().toNanos();
            /*
             * Perform the trace-specific trim operation. This should create the
             * trace file(s) in the destination path.
             */
            Path returnPath = fDestinationPath;
            if (fTrimmable != null) {
                try {
                    returnPath = fTrimmable.trim(tr, returnPath, monitor);
                } catch (CoreException e) {
                    return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "An error occurred writing the state systems snapshots", e); //$NON-NLS-1$
                }
            }
            if (returnPath == null) {
                return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to trim trace. " + fTrimmable); //$NON-NLS-1$
            }
            if (monitor.isCanceled()) {
                return Status.CANCEL_STATUS;
            }
            monitor.worked(1);
            SubMonitor ssMon = SubMonitor.convert(mon, statesystemModules.size());
            /* Write the snapshot files in the new trace's location. */
            try {
                for (ITmfAnalysisModuleWithStateSystems module : statesystemModules) {
                    ssMon.split(1);
                    Map<String, Integer> versions = module.getProviderVersions();
                    Iterable<ITmfStateSystem> sss = module.getStateSystems();
                    for (ITmfStateSystem ss : sss) {
                        Integer version = versions.get(ss.getSSID());
                        long currentEndTime = ss.getCurrentEndTime();
                        if (snapshotTime <= currentEndTime && version != null) {
                            StateSnapshot snapshot = new StateSnapshot(ss, Math.max(snapshotTime, ss.getStartTime()), Math.min(currentEndTime, tr.getEndTime().toNanos()), version);
                            snapshot.write(returnPath);
                        }
                    }
                }

            } catch (IOException e) {
                return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "An error occurred writing the state systems snapshots", e); //$NON-NLS-1$
            }
            for (TraceToTrim child : fChildren) {
                IStatus status = child.trim(tr, monitor);
                if (!status.isOK()) {
                    return status;
                }
            }
            return Status.OK_STATUS;
        }

        public IStatus importTrace(IProgressMonitor monitor) {
            try {
                IFolder folder = fFolder;
                if (folder == null) {
                    return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Folder is null"); //$NON-NLS-1$
                }
                ITmfProjectModelElement destElement;
                for (TraceToTrim child : fChildren) {
                    IStatus status = child.importTrace(monitor);
                    if (!status.isOK()) {
                        return status;
                    }
                }

                TraceTypeHelper traceTypeHelper = TmfTraceType.getTraceType(fElement.getTraceType());
                if (fElement instanceof TmfExperimentElement) {
                    TmfExperimentElement experimentElement = (TmfExperimentElement) fElement;
                    // create an experiment
                    TmfExperimentFolder experimentsFolder = experimentElement.getProject().getExperimentsFolder();
                    if (experimentsFolder == null) {
                        return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error getting experiment folder"); //$NON-NLS-1$
                    }
                    List<TmfTraceElement> children = new ArrayList<>();
                    for (TraceToTrim child : fChildren) {
                        ITmfProjectModelElement childDest = child.fDestElement;
                        if (childDest instanceof TmfTraceElement) {
                            children.add((TmfTraceElement) childDest);
                        }
                    }

                    String name = fDestinationPath.getFileName().toString();
                    if (name == null) {
                        return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "could not create element"); //$NON-NLS-1$
                    }
                    NewExperimentOperation newExperimentOperation = new NewExperimentOperation(experimentsFolder, name, null, children);
                    newExperimentOperation.run(monitor);
                    experimentsFolder.refresh();
                    destElement = experimentsFolder.getChild(name);
                    if (destElement == null) {
                        return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "could not create element"); //$NON-NLS-1$
                    }
                    TmfTraceTypeUIUtils.setTraceType(destElement.getResource(), traceTypeHelper);
                    if (destElement instanceof TmfExperimentElement) {
                        TmfExperimentElement expElement = (TmfExperimentElement) destElement;
                        for (TraceToTrim child : fChildren) {
                            ITmfProjectModelElement childDest = child.fDestElement;
                            if (childDest instanceof TmfTraceElement && expElement.getChild(childDest.getName()) == null) {
                                expElement.addTrace((TmfTraceElement) childDest, false);
                            }
                        }
                        fSourceElement.copySupplementaryFolder(destElement.getName());
                        destElement.refresh();
                        fDestElement = destElement;
                    }

                } else if (fElement instanceof TmfTraceElement) {
                    // create a trace
                    String path = (traceTypeHelper.isDirectoryTraceType() ? fDestinationPath : fDestinationPath.getParent()).toAbsolutePath().toString();
                    IPath destinationPath = org.eclipse.core.runtime.Path.fromOSString(path);
                    IResource traceResource = null;
                    String lastSegment = destinationPath.lastSegment();
                    IContainer tempFolder = folder.getParent();
                    tempFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
                    // Look for a common parent. In case the folders are oddly
                    // configured.
                    while (tempFolder != null && traceResource == null) {
                        traceResource = tempFolder.findMember(lastSegment);
                        tempFolder = tempFolder.getParent();
                    }
                    if (traceResource == null) {
                        return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error finding common parent"); //$NON-NLS-1$
                    }
                    IStatus ret = TmfTraceTypeUIUtils.setTraceType(traceResource, traceTypeHelper);
                    if (!ret.isOK()) {
                        return ret;
                    }
                    ITmfProjectModelElement findElement = TmfProjectRegistry.findElement(traceResource, true);
                    if (findElement instanceof TmfCommonProjectElement) {
                        fSourceElement.copySupplementaryFolder(((TmfCommonProjectElement) findElement).getElementPath());
                    }
                    fDestElement = findElement;
                }
            } catch (CoreException e) {
                return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error creating link", e); //$NON-NLS-1$
            }
            if (monitor.isCanceled()) {
                return Status.CANCEL_STATUS;
            }
            return Status.OK_STATUS;
        }

        public void open() {
            if (fDestElement instanceof TmfCommonProjectElement) {
                IStatus status = TmfOpenTraceHelper.openFromElement((TmfCommonProjectElement) fDestElement);
                if (!status.isOK()) {
                    Activator.getDefault().logError("Error trimming trace: " + status.getMessage()); //$NON-NLS-1$
                }
            }
        }

        public void createDir() throws IOException {
            if (fElement instanceof TmfExperimentElement) {
                for (TraceToTrim child : fChildren) {
                    child.createDir();
                }
            } else {
                fDestinationPath.toAbsolutePath().toFile().mkdirs();
            }
        }
    }

    /**
     * Can this trace be trimmed?
     *
     * @param element
     *            the element to test
     * @return <code>true</code> if the trace or all its children can be
     *         trimmed.
     */
    private static boolean isValid(@Nullable Object element) {
        if (element instanceof ITmfTrimmableTrace) {
            return true;
        }
        if (!(element instanceof ITmfTrace)) {
            return false;
        }
        ITmfTrace trace = (ITmfTrace) element;
        if (trace.getChildren().isEmpty()) {
            return false;
        }
        for (ITmfEventProvider child : trace.getChildren()) {
            if (!isValid(child)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public boolean isEnabled() {
        IWorkbench workbench = PlatformUI.getWorkbench();
        if (workbench == null) {
            return false;
        }
        @SuppressWarnings("null")
        IHandlerService service = workbench.getService(IHandlerService.class);
        // we need the current state, and the map, but the command and the
        // trigger are
        // not necessary for getCurrentSelection
        ExecutionEvent executionEvent = new ExecutionEvent(null, Collections.emptyMap(), null, service.getCurrentState());
        final Object element = HandlerUtil.getCurrentSelection(executionEvent);

        if (!(element instanceof TreeSelection)) {
            return false;
        }
        /*
         * plugin.xml should have done type/count verification already
         */
        Object firstElement = ((TreeSelection) element).getFirstElement();
        ITmfTrace trace = null;
        if (firstElement instanceof TmfCommonProjectElement) {
            TmfCommonProjectElement traceElem = (TmfCommonProjectElement) firstElement;
            if (traceElem instanceof TmfTraceElement) {
                traceElem = ((TmfTraceElement) traceElem).getElementUnderTraceFolder();
            }
            trace = traceElem.getTrace();
        }
        if (trace == null || !isValid(trace)) {
            return false;
        }

        /* Only enable the action if a time range is currently selected */
        TmfTraceManager tm = TmfTraceManager.getInstance();
        TmfTimeRange selectionRange = tm.getTraceContext(trace).getSelectionRange();
        return !(selectionRange.getStartTime().equals(selectionRange.getEndTime()));
    }

    @Override
    public @Nullable Object execute(@Nullable ExecutionEvent event) throws ExecutionException {
        ISelection selection = HandlerUtil.getCurrentSelectionChecked(event);
        Object firstElement = ((IStructuredSelection) selection).getFirstElement();
        final TmfCommonProjectElement traceElem = (firstElement instanceof TmfTraceElement) ?
                ((TmfTraceElement) firstElement).getElementUnderTraceFolder() : (TmfCommonProjectElement) firstElement;

        ITmfTrace trace = traceElem.getTrace();

        if (trace == null || !isValid(trace)) {
            /* That trace is not currently opened */
            return null;
        }

        /* Retrieve the current time range */
        final Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
        TmfTraceManager tm = TmfTraceManager.getInstance();
        TmfTimeRange timeRange = tm.getTraceContext(trace).getSelectionRange();
        if (Objects.equals(timeRange.getStartTime(), timeRange.getEndTime())) {
            MessageDialog.openError(shell, Messages.TrimTraces_InvalidTimeRange_DialogTitle, Messages.TrimTraces_InvalidTimeRange_DialogText);
            return null;
        }

        /* Ensure the time range is in the right direction */
        final TmfTimeRange tr = ((timeRange.getStartTime().compareTo(timeRange.getEndTime()) > 0) ? new TmfTimeRange(timeRange.getEndTime(), timeRange.getStartTime()) : timeRange);

        /*
         * Pop a dialog asking the user to select a parent directory for the new
         * trace.
         */
        TrimTraceDialog dialog = new TrimTraceDialog(shell, traceElem);
        if (dialog.open() != Window.OK) {
            return null;
        }
        Object result = dialog.getFirstResult();
        if (result == null) {
            /* Dialog was cancelled, take no further action. */
            return null;
        }

        /* Verify that the selected path is valid and writeable */
        final Path destinationPath = checkNotNull(Paths.get(result.toString()));

        if (destinationPath.toFile().exists()) {
            MessageDialog.openError(shell, Messages.TrimTraces_InvalidDirectory_DialogTitle, Messages.TrimTraces_InvalidDirectory_DialogText);
            return null;
        }
        TraceToTrim toTrim = TraceToTrim.create(traceElem, destinationPath);
        if (toTrim == null) {
            return null;
        }
        try {
            toTrim.createDir();
        } catch (IOException e) {
            /* Should not happen since we have checked permissions, etc. */
            throw new ExecutionException(e.getMessage(), e);
        }

        TmfWorkspaceModifyOperation trimOperation = new TmfWorkspaceModifyOperation() {
            @Override
            public void execute(@Nullable IProgressMonitor monitor) throws CoreException {
                SubMonitor mon = SubMonitor.convert(monitor, 2);

                toTrim.trim(tr, mon);
                /*
                 * Import the new trace into the current project, at the
                 * top-level.
                 */
                TmfProjectElement currentProjectElement = traceElem.getProject();
                TmfTraceFolder traceFolder = currentProjectElement.getTracesFolder();
                toTrim.importTrace(mon);
                if (mon.isCanceled()) {
                    return;
                }
                if (traceFolder != null) {
                    Display.getDefault().asyncExec(toTrim::open);
                } else {
                    Activator.getDefault().logWarning("Trace folder does not exist: " + toTrim.fDestinationPath); //$NON-NLS-1$
                }
            }
        };

        try {
            PlatformUI.getWorkbench().getProgressService().run(true, true, trimOperation);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (InvocationTargetException e) {
            TraceUtils.displayErrorMsg(Messages.TrimTraceHandler_failMsg, e.getMessage(), e);
        }

        return null;
    }

}
