/*******************************************************************************
 * Copyright (c) 2009, 2018 Red Hat, Inc.
 *
 * 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:
 *     Red Hat - initial API and implementation
 *******************************************************************************/

package org.eclipse.linuxtools.internal.callgraph.launch;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

import org.eclipse.cdt.launch.AbstractCLaunchDelegate;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
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.IStreamListener;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamMonitor;
import org.eclipse.linuxtools.internal.callgraph.core.DocWriter;
import org.eclipse.linuxtools.internal.callgraph.core.Helper;
import org.eclipse.linuxtools.internal.callgraph.core.LaunchConfigurationConstants;
import org.eclipse.linuxtools.internal.callgraph.core.PluginConstants;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapCommandGenerator;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapErrorHandler;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapParser;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapUIErrorMessages;
import org.eclipse.linuxtools.tools.launch.core.factory.CdtSpawnerProcessFactory;

/**
 * Delegate for Stap scripts. The Delegate generates part of the command string
 * and schedules a job to finish generation of the command and execute.
 *
 */
public class SystemTapLaunchConfigurationDelegate extends AbstractCLaunchDelegate {

    private static final String [] escapableChars = new String []  {"(", ")", " "}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

    private static final String TEMP_ERROR_OUTPUT =
        PluginConstants.getDefaultOutput() + "stapTempError.error"; //$NON-NLS-1$
    private String cmd;
    private File temporaryScript = null;
    private String arguments = ""; //$NON-NLS-1$
    private String scriptPath = ""; //$NON-NLS-1$
    private String binaryPath = ""; //$NON-NLS-1$
    private String outputPath = ""; //$NON-NLS-1$
    private boolean needsBinary = false; // Set to false if we want to use SystemTap
    private boolean needsArguments = false;
    private String binaryArguments = ""; //$NON-NLS-1$
    private String partialCommand = ""; //$NON-NLS-1$
    private String stap = ""; //$NON-NLS-1$

    @Override
    protected String getPluginID() {
        return null;
    }

    /**
     * Sets strings to blank, booleans to false and everything else to null
     */
    private void initialize() {
         temporaryScript = null;
         arguments = ""; //$NON-NLS-1$
         scriptPath = ""; //$NON-NLS-1$
         binaryPath = ""; //$NON-NLS-1$
         outputPath = ""; //$NON-NLS-1$
         needsBinary = false; // Set to false if we want to use SystemTap
         needsArguments = false;
         binaryArguments = ""; //$NON-NLS-1$
    }

    @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,
                "SystemTap runtime monitor", 5); //$NON-NLS-1$
        initialize();

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

        /*
         * Set variables
         */
        if (!config.getAttribute(LaunchConfigurationConstants.ARGUMENTS,
                LaunchConfigurationConstants.DEFAULT_ARGUMENTS).equals(
                LaunchConfigurationConstants.DEFAULT_ARGUMENTS)) {
            arguments = config.getAttribute(
                    LaunchConfigurationConstants.ARGUMENTS,
                    LaunchConfigurationConstants.DEFAULT_ARGUMENTS);
            needsArguments = true;
        }
        if (!config.getAttribute(LaunchConfigurationConstants.BINARY_PATH,
                LaunchConfigurationConstants.DEFAULT_BINARY_PATH).equals(
                LaunchConfigurationConstants.DEFAULT_BINARY_PATH)) {
            binaryPath = config.getAttribute(
                    LaunchConfigurationConstants.BINARY_PATH,
                    LaunchConfigurationConstants.DEFAULT_BINARY_PATH);
            needsBinary = true;
        }
        if (!config.getAttribute(LaunchConfigurationConstants.BINARY_ARGUMENTS,
                LaunchConfigurationConstants.DEFAULT_BINARY_ARGUMENTS).equals(
                LaunchConfigurationConstants.DEFAULT_BINARY_ARGUMENTS)) {
            binaryArguments = config.getAttribute(
                    LaunchConfigurationConstants.BINARY_ARGUMENTS,
                    LaunchConfigurationConstants.DEFAULT_BINARY_ARGUMENTS);
        }
        if (!config.getAttribute(LaunchConfigurationConstants.SCRIPT_PATH,
                LaunchConfigurationConstants.DEFAULT_SCRIPT_PATH).equals(
                LaunchConfigurationConstants.DEFAULT_SCRIPT_PATH)) {
            scriptPath = config.getAttribute(
                    LaunchConfigurationConstants.SCRIPT_PATH,
                    LaunchConfigurationConstants.DEFAULT_SCRIPT_PATH);
        }
        // Generate script if needed
        if (config.getAttribute(LaunchConfigurationConstants.NEED_TO_GENERATE,
                LaunchConfigurationConstants.DEFAULT_NEED_TO_GENERATE)) {
            temporaryScript = new File(scriptPath);
            temporaryScript.delete();
            try {
                temporaryScript.createNewFile();
                try (FileWriter fstream = new FileWriter(temporaryScript);
                        BufferedWriter out = new BufferedWriter(fstream)) {
                    out.write(config
                            .getAttribute(
                                    LaunchConfigurationConstants.GENERATED_SCRIPT,
                                    LaunchConfigurationConstants.DEFAULT_GENERATED_SCRIPT));
                }
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }

        stap = config.getAttribute(LaunchConfigurationConstants.COMMAND,
                PluginConstants.STAP_PATH);

        /**
         * Generate partial command
         */
        partialCommand = ConfigurationOptionsSetter.setOptions(config);

        outputPath = config.getAttribute(
                LaunchConfigurationConstants.OUTPUT_PATH,
                PluginConstants.getDefaultOutput());

        boolean fileExists = true;
        try {
            //Make sure the output file exists
            File tempFile = new File(outputPath);
            tempFile.delete();
            fileExists = tempFile.createNewFile();
        } catch (IOException e1) {
            fileExists = false;
        }

        // check for cancellation
        if ( !fileExists || monitor.isCanceled() ) {
            SystemTapUIErrorMessages mess = new SystemTapUIErrorMessages(Messages.getString("SystemTapLaunchConfigurationDelegate.0"),  //$NON-NLS-1$
                    Messages.getString("SystemTapLaunchConfigurationDelegate.1"), Messages.getString("SystemTapLaunchConfigurationDelegate.2") + outputPath +  //$NON-NLS-1$ //$NON-NLS-2$
                    Messages.getString("SystemTapLaunchConfigurationDelegate.3")); //$NON-NLS-1$
            mess.schedule();
            return;
        }

        finishLaunch(launch, config, m);
    }

    /**
     * Returns the current SystemTap command, or returns an error message.
     * @return
     */
    public String getCommand() {
        if (!cmd.isEmpty()) {
            return cmd;
        } else {
            return Messages.getString("SystemTapLaunchConfigurationDelegate.NoCommand"); //$NON-NLS-1$
        }
    }

    private void finishLaunch(ILaunch launch, ILaunchConfiguration config,
            IProgressMonitor monitor) {

        try {
            // Check for cancellation
            if (monitor.isCanceled() || launch == null) {
                return;
            }
            monitor.worked(1);

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

            /*
             * Fetch a parser
             */
            String parserClass = config.getAttribute(LaunchConfigurationConstants.PARSER_CLASS,
                    LaunchConfigurationConstants.DEFAULT_PARSER_CLASS);
            IExtensionRegistry reg = Platform.getExtensionRegistry();
            IConfigurationElement[] extensions = reg
                    .getConfigurationElementsFor(PluginConstants.PARSER_RESOURCE,
                            PluginConstants.PARSER_NAME,
                            parserClass);
            if (extensions == null || extensions.length < 1) {
                SystemTapUIErrorMessages mess = new SystemTapUIErrorMessages(Messages.getString("SystemTapLaunchConfigurationDelegate.InvalidParser1"),  //$NON-NLS-1$
                        Messages.getString("SystemTapLaunchConfigurationDelegate.InvalidParser1"), //$NON-NLS-1$
                        Messages.getString("SystemTapLaunchConfigurationDelegate.InvalidParser2") + //$NON-NLS-1$
                        Messages.getString("SystemTapLaunchConfigurationDelegate.InvalidParser3") + parserClass); //$NON-NLS-1$
                mess.schedule();
                return;
            }

            IConfigurationElement element = extensions[0];
            SystemTapParser parser =
                (SystemTapParser) element.createExecutableExtension(PluginConstants.ATTR_CLASS);

            //Set parser options
            parser.setViewID(config.getAttribute(LaunchConfigurationConstants.VIEW_CLASS,
                    LaunchConfigurationConstants.VIEW_CLASS));
            parser.setSourcePath(outputPath);
            parser.setMonitor(SubMonitor.convert(monitor));
            parser.setDone(false);
            parser.setSecondaryID(config.getAttribute(LaunchConfigurationConstants.SECONDARY_VIEW_ID,
                    LaunchConfigurationConstants.DEFAULT_SECONDARY_VIEW_ID));

            parser.setKillButtonEnabled(true);

            monitor.worked(1);

            /*
             * Launch
             */

            File workDir = getWorkingDirectory(config);
            if (workDir == null) {
                workDir = new File(System.getProperty("user.home", ".")); //$NON-NLS-1$ //$NON-NLS-2$
            }

            //Put command into a shell script
            String cmd = generateCommand();
            File script = File.createTempFile("org.eclipse.linuxtools.profiling.launch" + System.currentTimeMillis(), ".sh"); //$NON-NLS-1$ //$NON-NLS-2$
            String data = "#!/bin/sh\nexec " + cmd; //$NON-NLS-1$
            try (FileOutputStream out = new FileOutputStream(script)){
                out.write(data.getBytes());
            }

            String [] commandArray = new String [] {"sh", script.getAbsolutePath()}; //$NON-NLS-1$

            Process subProcess = CdtSpawnerProcessFactory.getFactory().exec(commandArray, getEnvironment(config),
                    workDir, true);

            IProcess process = DebugPlugin.newProcess(launch, subProcess,
                    renderProcessLabel(commandArray[0]));
            // set the command line used
            process.setAttribute(IProcess.ATTR_CMDLINE,cmd);

            monitor.worked(1);

            StreamListener s = new StreamListener();
            process.getStreamsProxy().getErrorStreamMonitor().addListener(s);

            while (!process.isTerminated()) {
                Thread.sleep(100);
                if ((monitor != null && monitor.isCanceled()) || parser.isDone()) {
                    parser.cancelJob();
                    process.terminate();
                    return;
                }
            }
            Thread.sleep(100);
            s.close();
            parser.setKillButtonEnabled(false);

            if (process.getExitValue() != 0) {
                parser.cancelJob();
                //exit code for command not found
                if (process.getExitValue() == 127){
                    SystemTapUIErrorMessages errorDialog = new SystemTapUIErrorMessages(
                            Messages.getString("SystemTapLaunchConfigurationDelegate.CallGraphGenericError"), //$NON-NLS-1$
                            Messages.getString("SystemTapLaunchConfigurationDelegate.CallGraphGenericError"), //$NON-NLS-1$
                            Messages.getString("SystemTapLaunchConfigurationDelegate.stapNotFound")); //$NON-NLS-1$

                    errorDialog.schedule();
                }else{
                    SystemTapErrorHandler errorHandler = new SystemTapErrorHandler();

                    //Prepare stap information
                    errorHandler.appendToLog(config.getName() + Messages.getString("SystemTapLaunchConfigurationDelegate.stap_command") + cmd+ PluginConstants.NEW_LINE + PluginConstants.NEW_LINE);//$NON-NLS-1$

                    //Handle error from TEMP_ERROR_OUTPUT
                    errorHandler.handle(monitor, new FileReader(TEMP_ERROR_OUTPUT));
                    if ((monitor != null && monitor.isCanceled())) {
                        return;
                    }

                    errorHandler.finishHandling();
                    if (errorHandler.isErrorRecognized()) {
                        SystemTapUIErrorMessages errorDialog = new SystemTapUIErrorMessages(
                                Messages.getString("SystemTapLaunchConfigurationDelegate.CallGraphGenericError"),  //$NON-NLS-1$
                                Messages.getString("SystemTapLaunchConfigurationDelegate.CallGraphGenericError"),  //$NON-NLS-1$
                                errorHandler.getErrorMessage());

                        errorDialog.schedule();
                    }
                }
                return;
            }

            if (element.getAttribute(PluginConstants.ATTR_REALTIME).equals(PluginConstants.VAL_TRUE)) {
                parser.setRealTime(true);
            }

            parser.schedule();
            monitor.worked(1);

            String message = generateErrorMessage(config.getName(), binaryArguments);

            DocWriter dw = new DocWriter(Messages.getString("SystemTapLaunchConfigurationDelegate.DocWriterName"),  //$NON-NLS-1$
                    (Helper.getConsoleByName(config.getName())), message);
            dw.schedule();

        } catch (IOException|InterruptedException|CoreException e) {
            e.printStackTrace();
        } finally {
            monitor.done();

        }
    }

    private String generateErrorMessage(String configName, String binaryCommand) {
        String output = ""; //$NON-NLS-1$

        if (binaryCommand == null || binaryCommand.length() < 0) {
            output = PluginConstants.NEW_LINE +
                        PluginConstants.NEW_LINE + "-------------" + //$NON-NLS-1$
                        PluginConstants.NEW_LINE +
                        Messages.getString("SystemTapLaunchConfigurationDelegate.Relaunch10") //$NON-NLS-1$
                        + configName + PluginConstants.NEW_LINE +
                        Messages.getString("SystemTapLaunchConfigurationDelegate.Relaunch8") + //$NON-NLS-1$
                        Messages.getString("SystemTapLaunchConfigurationDelegate.Relaunch9") + //$NON-NLS-1$
                        "configuration in Profile As --> Profile Configurations." + //$NON-NLS-1$
                        PluginConstants.NEW_LINE + PluginConstants.NEW_LINE;
        } else {
            output = PluginConstants.NEW_LINE
                    + PluginConstants.NEW_LINE +"-------------" //$NON-NLS-1$
                    + PluginConstants.NEW_LINE
                    + Messages.getString("SystemTapLaunchConfigurationDelegate.EndMessage1")  //$NON-NLS-1$
                    + configName + PluginConstants.NEW_LINE +
                    Messages.getString("SystemTapLaunchConfigurationDelegate.EndMessage2") //$NON-NLS-1$
                    + binaryCommand + PluginConstants.NEW_LINE +
                    Messages.getString("SystemTapLaunchConfigurationDelegate.EndMessage3") +  //$NON-NLS-1$
                    Messages.getString("SystemTapLaunchConfigurationDelegate.EndMessage4") +  //$NON-NLS-1$
                    Messages.getString("SystemTapLaunchConfigurationDelegate.EndMessage5") +  //$NON-NLS-1$
                    PluginConstants.NEW_LINE + PluginConstants.NEW_LINE;
        }

        return output;
    }

    private static class StreamListener implements IStreamListener{
        private int counter;
        private BufferedWriter bw;

        public StreamListener() throws IOException {
            File file = new File(TEMP_ERROR_OUTPUT);
            file.delete();
            file.createNewFile();
            bw = Helper.setBufferedWriter(TEMP_ERROR_OUTPUT);
            counter = 0;
        }

        @Override
        public void streamAppended(String text, IStreamMonitor monitor) {
            try {
                if (text.length() < 1) {
                    return;
                }
                counter++;
                if (counter < PluginConstants.MAX_ERRORS) {
                    bw.append(text);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void close() throws IOException {
            bw.close();
        }
    }

    public String generateCommand() {
        // Generate the command
        cmd = SystemTapCommandGenerator.generateCommand(escapeSpecialCharacters(scriptPath), escapeSpecialCharacters(binaryPath),
                partialCommand, needsBinary, needsArguments, escapeSpecialCharacters(arguments), binaryArguments, stap);
        cmd += " >& " + escapeSpecialCharacters(outputPath); //$NON-NLS-1$
        return cmd;
    }

    /**
     * Escapes special characters in the target string
     *
     * @param script the script to be executed by the shell.
     * @return the formatted string that will be executed.
     */
    private String escapeSpecialCharacters(String str) {
        // Modify script to catch escapable characters.
        String res = str;
        for (int i = 0; i < escapableChars.length; i++) {
            res = res.replace(escapableChars[i], "\\" + escapableChars[i]); //$NON-NLS-1$
        }
        return res;
    }
}
