/*******************************************************************************
 * Copyright (c) 2008, 2018 Red Hat, Inc. and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Elliott Baron <ebaron@redhat.com> - initial API and implementation
 * Martin Oberhuber (Wind River) - [360085] Fix valgrind problem marker lifecycle
 *******************************************************************************/
package org.eclipse.linuxtools.internal.valgrind.launch;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

import org.eclipse.cdt.debug.core.CDebugUtils;
import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.launch.AbstractCLaunchDelegate;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.ISourceLocator;
import org.eclipse.debug.core.sourcelookup.containers.LocalFileStorage;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.sourcelookup.ISourceLookupResult;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.linuxtools.internal.valgrind.core.ValgrindCommand;
import org.eclipse.linuxtools.internal.valgrind.core.ValgrindCoreParser;
import org.eclipse.linuxtools.internal.valgrind.core.ValgrindError;
import org.eclipse.linuxtools.internal.valgrind.core.ValgrindInfo;
import org.eclipse.linuxtools.internal.valgrind.core.ValgrindStackFrame;
import org.eclipse.linuxtools.internal.valgrind.ui.ValgrindUIPlugin;
import org.eclipse.linuxtools.internal.valgrind.ui.ValgrindViewPart;
import org.eclipse.linuxtools.valgrind.core.CommandLineConstants;
import org.eclipse.linuxtools.valgrind.core.IValgrindMessage;
import org.eclipse.linuxtools.valgrind.launch.IValgrindLaunchDelegate;
import org.eclipse.linuxtools.valgrind.launch.IValgrindOutputDirectoryProvider;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.Version;

public class ValgrindLaunchConfigurationDelegate extends AbstractCLaunchDelegate {

    private static final String NO = "no"; //$NON-NLS-1$
    private static final String YES = "yes"; //$NON-NLS-1$
    private static final String EQUALS = "="; //$NON-NLS-1$

    private static final String LOG_FILE = CommandLineConstants.LOG_PREFIX + "%p.txt"; //$NON-NLS-1$
    private static final Pattern CORE_PATTERN = Pattern.compile("^.*\\.txt\\.core\\.[0-9]+$");  //$NON-NLS-1$
    private static final FileFilter LOG_FILTER = pathname -> pathname.getName().startsWith(CommandLineConstants.LOG_PREFIX) && !CORE_PATTERN.matcher(pathname.getName()).matches();

    protected String toolID;
    protected ValgrindCommand command;
    protected IPath outputPath;
    protected IValgrindLaunchDelegate dynamicDelegate;
    protected ILaunchConfiguration config;
    protected ILaunch launch;
    protected IProcess process;
    protected String launchStr;
    protected Version valgrindVersion; // null if not used

    @Override
    public void launch(ILaunchConfiguration config, String mode,
            ILaunch launch, IProgressMonitor m) throws CoreException {
        if (m == null) {
            m = new NullProgressMonitor();
        }

        SubMonitor monitor = SubMonitor.convert(m, Messages.getString("ValgrindLaunchConfigurationDelegate.Profiling_Local_CCPP_Application"), 10); //$NON-NLS-1$
        // check for cancellation
        if (monitor.isCanceled()) {
            return;
        }

        this.config = config;
        this.launch    = launch;
        try {
            IProject project = CDebugUtils.verifyCProject(config).getProject();
            ValgrindUIPlugin.getDefault().setProfiledProject(project);
            command = getValgrindCommand();

            // remove any output from previous run
            ValgrindUIPlugin.getDefault().resetView();
            // reset stored launch data
            getPlugin().setCurrentLaunchConfiguration(null);
            getPlugin().setCurrentLaunch(null);

            String valgrindCommand= getValgrindCommand().getValgrindCommand();
            // also ensure Valgrind version is usable
        	try {
        		valgrindVersion = getPlugin().getValgrindVersion(project);
        	} catch(CoreException e) {
        		// if versioning failed, issue an error dialog and return
        		errorDialog(Messages.getString("ValgrindLaunchConfigurationDelegate.Valgrind_version_failed_msg"), //$NON-NLS-1$
        				e.getMessage());
        		return;
        	}

            monitor.worked(1);
            IPath exePath = CDebugUtils.verifyProgramPath(config);
            String[] arguments = getProgramArgumentsArray(config);
            File workDir = getWorkingDirectory(config);
            if (workDir == null) {
                workDir = new File(System.getProperty("user.home", ".")); //$NON-NLS-1$ //$NON-NLS-2$
            }

            // set output directory in config
            IValgrindOutputDirectoryProvider provider = getPlugin().getOutputDirectoryProvider();
            setOutputPath(config, provider.getOutputPath());
            outputPath = verifyOutputPath(config);
            // create/empty output directory
            createDirectory(outputPath);

            // tool that was launched
            toolID = getTool(config);
            // ask tool extension for arguments
            dynamicDelegate = getDynamicDelegate(toolID);
            String[] opts = getValgrindArgumentsArray(config);

            // set the default source locator if required
            setDefaultSourceLocator(launch, config);

            ArrayList<String> cmdLine = new ArrayList<>(1 + arguments.length);
            cmdLine.add(valgrindCommand);
            cmdLine.addAll(Arrays.asList(opts));
            cmdLine.add(exePath.toPortableString());
            cmdLine.addAll(Arrays.asList(arguments));
            String[] commandArray = cmdLine.toArray(new String[cmdLine.size()]);
            boolean usePty = config.getAttribute(ICDTLaunchConfigurationConstants.ATTR_USE_TERMINAL, ICDTLaunchConfigurationConstants.USE_TERMINAL_DEFAULT);
            monitor.worked(1);

            // check for cancellation
            if (monitor.isCanceled()) {
                return;
            }
            // call Valgrind

            command.execute(commandArray, getEnvironment(config), workDir, usePty, project);
            monitor.worked(3);
            process = createNewProcess(launch, command.getProcess(), commandArray[0]);
            // set the command line used
            process.setAttribute(IProcess.ATTR_CMDLINE, command.getCommandLine());
            while (!process.isTerminated()) {
                Thread.sleep(100);
            }

            // store these for use by other classes
            getPlugin().setCurrentLaunchConfiguration(config);
            getPlugin().setCurrentLaunch(launch);

            // parse Valgrind logs
            IValgrindMessage[] messages = parseLogs(outputPath);

            // create launch summary string to distinguish this launch
            launchStr = createLaunchStr();

            // create view
            ValgrindUIPlugin.getDefault().createView(launchStr, toolID);
            // set log messages
            ValgrindViewPart view = ValgrindUIPlugin.getDefault().getView();
            view.setMessages(messages);
            monitor.worked(1);

            // pass off control to extender
            dynamicDelegate.handleLaunch(config, launch, outputPath, monitor.newChild(2));

            // initialize tool-specific part of view
            dynamicDelegate.initializeView(view.getDynamicView(), launchStr, monitor.newChild(1));

            // refresh view
            ValgrindUIPlugin.getDefault().refreshView();

            // show view
            ValgrindUIPlugin.getDefault().showView();

            // set up resource listener for post-build events.
            ResourcesPlugin.getWorkspace().addResourceChangeListener(
                    new ProjectBuildListener(project), IResourceChangeEvent.POST_BUILD);

            monitor.worked(1);
        } catch (IOException e) {
            abort(Messages.getString("ValgrindLaunchConfigurationDelegate.Error_starting_process"), e, ICDTLaunchConfigurationConstants.ERR_INTERNAL_ERROR); //$NON-NLS-1$
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            m.done();
        }
    }

    protected IValgrindMessage[] parseLogs(IPath outputPath) throws IOException, CoreException {
        List<IValgrindMessage> messages = new ArrayList<>();

        for (File log : outputPath.toFile().listFiles(LOG_FILTER)) {
            ValgrindCoreParser parser = new ValgrindCoreParser(log, launch);
            IValgrindMessage[] results = parser.getMessages();

            if (results.length == 0){
                results = new IValgrindMessage[1];
                results[0] = new ValgrindInfo(null, Messages.getString("ValgrindOutputView.No_output"), launch); //$NON-NLS-1$
            }
            messages.addAll(Arrays.asList(results));
            createMarkers(results);
        }

        return messages.toArray(new IValgrindMessage[messages.size()]);
    }

    private void createMarkers(IValgrindMessage[] messages) throws CoreException {
        // find the topmost stack frame within the workspace to annotate with marker
        // traverse nested errors as well
        Stack<IValgrindMessage> messageStack = new Stack<>();
        messageStack.addAll(Arrays.asList(messages));
        while (!messageStack.isEmpty()) {
            IValgrindMessage message = messageStack.pop();
            IMarker marker = null;
            IValgrindMessage[] children = message.getChildren();
            for (int i = 0; i < children.length; i++) {
                // if we've found our marker we don't care about any further frames in this stack
                if (children[i] instanceof ValgrindStackFrame && marker == null) {
                    ValgrindStackFrame frame = (ValgrindStackFrame) children[i];
                    if (frame.getLine() > 0) {
                        ISourceLocator locator = frame.getSourceLocator();
                        ISourceLookupResult result = DebugUITools.lookupSource(frame.getFile(), locator);
                        Object sourceElement = result.getSourceElement();

                        if (sourceElement != null) {
                            // Resolve IResource in case we get a LocalFileStorage object
                            if (sourceElement instanceof LocalFileStorage) {
                                IPath filePath = ((LocalFileStorage) sourceElement).getFullPath();
                                URI fileURI = URIUtil.toURI(filePath);
                                IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
                                IFile[] files = root.findFilesForLocationURI(fileURI);
                                if (files.length > 0) {
                                    // Take the first match
                                    sourceElement = files[0];
                                }
                            }

                            if (sourceElement instanceof IResource) {
                                IResource resource = (IResource) sourceElement;
                                marker = resource.createMarker(ValgrindLaunchPlugin.MARKER_TYPE);
                                marker.setAttribute(IMarker.MESSAGE, message.getText());
                                marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
                                marker.setAttribute(IMarker.LINE_NUMBER, frame.getLine());
                            }
                        }
                    }
                }
                else if (children[i] instanceof ValgrindError) {
                    // nested error
                    messageStack.push(children[i]);
                }
            }
        }
    }

    protected IProcess createNewProcess(ILaunch launch, Process systemProcess, String programName) {
        return DebugPlugin.newProcess(launch, systemProcess, renderProcessLabel(programName));
    }

    protected ValgrindCommand getValgrindCommand() {
        return getPlugin().getValgrindCommand();
    }

    protected ValgrindLaunchPlugin getPlugin() {
        return ValgrindLaunchPlugin.getDefault();
    }

    protected IValgrindLaunchDelegate getDynamicDelegate(String toolID) throws CoreException {
        return getPlugin().getToolDelegate(toolID);
    }

    private IPath verifyOutputPath(ILaunchConfiguration config) throws CoreException {
        IPath result = null;
        String strPath = config.getAttribute(LaunchConfigurationConstants.ATTR_INTERNAL_OUTPUT_DIR, (String) null);
        if (strPath != null) {
            result = Path.fromPortableString(strPath);
        }
        if (result == null) {
            abort(Messages.getString("ValgrindLaunchConfigurationDelegate.Retrieving_location_failed"), null, ICDTLaunchConfigurationConstants.ERR_INTERNAL_ERROR); //$NON-NLS-1$
        }
        return result;
    }

    protected void setOutputPath(ILaunchConfiguration config, IPath outputPath) throws CoreException {
        ILaunchConfigurationWorkingCopy wc = config.getWorkingCopy();
        wc.setAttribute(LaunchConfigurationConstants.ATTR_INTERNAL_OUTPUT_DIR, outputPath.toPortableString());
        wc.doSave();
    }

    protected void createDirectory(IPath path) throws IOException {
        File outputDir = path.toFile();

        if (outputDir.exists()) {
            // delete any preexisting files
            for (File outputFile : outputDir.listFiles()) {
                if (outputFile.isFile() && !outputFile.delete()) {
                    throw new IOException(NLS.bind(Messages.getString("ValgrindOutputDirectory.Couldnt_delete"), outputFile.getAbsolutePath())); //$NON-NLS-1$
                }
            }
        } else if (!outputDir.mkdir()) {
            throw new IOException(NLS.bind(Messages.getString("ValgrindOutputDirectory.Couldnt_create"), outputDir.getAbsolutePath())); //$NON-NLS-1$
        }
    }

    private String createLaunchStr() {
        return config.getName() + " [" + getPlugin().getToolName(toolID) + "] " + process.getLabel(); //$NON-NLS-1$ //$NON-NLS-2$
    }

    protected String[] getValgrindArgumentsArray(ILaunchConfiguration config) throws CoreException {
        ArrayList<String> opts = new ArrayList<>();
        opts.add(CommandLineConstants.OPT_TOOL + EQUALS + getPlugin().getToolName(toolID));
        opts.add(CommandLineConstants.OPT_QUIET); // suppress uninteresting output
        opts.add(CommandLineConstants.OPT_LOGFILE + EQUALS + outputPath.append(LOG_FILE).toPortableString());

        opts.add(CommandLineConstants.OPT_TRACECHILD + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_TRACECHILD, LaunchConfigurationConstants.DEFAULT_GENERAL_TRACECHILD) ? YES : NO));
        opts.add(CommandLineConstants.OPT_CHILDSILENT + EQUALS + YES); // necessary for parsing
        opts.add(CommandLineConstants.OPT_FREERES + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_FREERES, LaunchConfigurationConstants.DEFAULT_GENERAL_FREERES) ? YES : NO));

        opts.add(CommandLineConstants.OPT_DEMANGLE + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_DEMANGLE, LaunchConfigurationConstants.DEFAULT_GENERAL_DEMANGLE) ? YES : NO));
        opts.add(CommandLineConstants.OPT_NUMCALLERS + EQUALS + config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_NUMCALLERS, LaunchConfigurationConstants.DEFAULT_GENERAL_NUMCALLERS));
        opts.add(CommandLineConstants.OPT_ERRLIMIT + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_ERRLIMIT, LaunchConfigurationConstants.DEFAULT_GENERAL_ERRLIMIT) ? YES : NO));
        opts.add(CommandLineConstants.OPT_BELOWMAIN + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_BELOWMAIN, LaunchConfigurationConstants.DEFAULT_GENERAL_BELOWMAIN) ? YES : NO));
        opts.add(CommandLineConstants.OPT_MAXFRAME + EQUALS + config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_MAXFRAME, LaunchConfigurationConstants.DEFAULT_GENERAL_MAXFRAME));

        // 3.4.0 specific
        if (valgrindVersion == null || valgrindVersion.compareTo(ValgrindLaunchPlugin.VER_3_4_0) >= 0) {
            boolean useMainStack = config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_MAINSTACK_BOOL, LaunchConfigurationConstants.DEFAULT_GENERAL_MAINSTACK_BOOL);
            if (useMainStack) {
                opts.add(CommandLineConstants.OPT_MAINSTACK + EQUALS + config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_MAINSTACK, LaunchConfigurationConstants.DEFAULT_GENERAL_MAINSTACK));
            }
        }

        // 3.6.0 specific
        if (valgrindVersion == null || valgrindVersion.compareTo(ValgrindLaunchPlugin.VER_3_6_0) >= 0) {
            if (config.getAttribute(LaunchConfigurationConstants.ATTR_FULLPATH_AFTER, LaunchConfigurationConstants.DEFAULT_FULLPATH_AFTER) == LaunchConfigurationConstants.DEFAULT_FULLPATH_AFTER)
                opts.add("--fullpath-after" + EQUALS); //$NON-NLS-1$ //TODO: Make this API in CommandLineConstants Interface.
            if (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_DSYMUTIL, LaunchConfigurationConstants.DEFAULT_GENERAL_DSYMUTIL) != LaunchConfigurationConstants.DEFAULT_GENERAL_DSYMUTIL)
                opts.add(CommandLineConstants.OPT_DSYMUTIL + EQUALS + (config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_DSYMUTIL, LaunchConfigurationConstants.DEFAULT_GENERAL_DSYMUTIL) ? YES : NO));
        }

        List<?> suppFiles = config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_SUPPFILES, LaunchConfigurationConstants.DEFAULT_GENERAL_SUPPFILES);
        for (Object strpath : suppFiles) {
            IPath suppfile = getPlugin().parseWSPath((String) strpath);
            if (suppfile != null) {
                opts.add(CommandLineConstants.OPT_SUPPFILE + EQUALS + suppfile.toPortableString());
            }
        }
        opts.addAll(Arrays.asList(dynamicDelegate.getCommandArray(config, valgrindVersion, outputPath)));
        String otherOptions = config.getAttribute(LaunchConfigurationConstants.ATTR_GENERAL_EXTRA_OPTIONS,"").trim(); //$NON-NLS-1$
		if (!otherOptions.isEmpty()) {
			opts.addAll(Arrays.asList(otherOptions.split("\\s+"))); //$NON-NLS-1$
		}
        String[] ret = new String[opts.size()];
        return opts.toArray(ret);
    }

    protected String getTool(ILaunchConfiguration config) throws CoreException {
        return config.getAttribute(LaunchConfigurationConstants.ATTR_TOOL, LaunchConfigurationConstants.DEFAULT_TOOL);
    }

    @Override
    protected String getPluginID() {
        return ValgrindLaunchPlugin.PLUGIN_ID;
    }

    @Override
    public boolean finalLaunchCheck(ILaunchConfiguration configuration,
            String mode, IProgressMonitor monitor) throws CoreException {
        //Delete our own problem markers
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        root.deleteMarkers(ValgrindLaunchPlugin.MARKER_TYPE, true, IResource.DEPTH_INFINITE);
        return super.finalLaunchCheck(configuration, mode, monitor);
    }

    // Display an error dialog to denote an error scenario.
    private void errorDialog(final String title, final String message) {
        ValgrindLaunchPlugin.getShell().getDisplay().asyncExec( () -> MessageDialog.openError(ValgrindLaunchPlugin.getShell(), title, message));
    }


}
