/*******************************************************************************
 * Copyright (c) 2006, 2018 IBM Corporation 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:
 *     IBM Corporation - Jeff Briggs, Henry Hughes, Ryan Morse
 *     Red Hat - ongoing maintenance
 *******************************************************************************/

package org.eclipse.linuxtools.systemtap.structures.runnable;


import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.linuxtools.internal.systemtap.structures.StructuresPlugin;
import org.eclipse.linuxtools.systemtap.structures.LoggingStreamDaemon;
import org.eclipse.linuxtools.systemtap.structures.listeners.IGobblerListener;
import org.eclipse.linuxtools.tools.launch.core.factory.RuntimeProcessFactory;

/**
 * A class to spawn a separate thread to run a <code>Process</code>.
 * @author Ryan Morse
 * @since 2.0
 */
public class Command implements Runnable {
    /*
     * Bug in the exec command prevents using a single string.  Forced
     * to use a workaround in order to run commands with spaces.
     *
     * http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4365120
     * http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4109888
     */

    /**
     * @since 2.0
     */
    protected boolean stopped = false;
    private boolean started = false;
    /**
     * @since 2.0
     */
    protected StreamGobbler inputGobbler = null;
    /**
     * @since 2.0
     */
    protected StreamGobbler errorGobbler = null;

    private boolean disposed = false;
    private List<IGobblerListener> inputListeners = new ArrayList<>();    //Only used to allow adding listeners before creating the StreamGobbler
    private List<IGobblerListener> errorListeners = new ArrayList<>();    //Only used to allow adding listeners before creating the StreamGobbler
    private int returnVal = Integer.MAX_VALUE;

    /**
     * @since 3.0
     */
    protected final String[] cmd;
    /**
     * @since 3.0
     */
    protected final String[] envVars;

    protected Process process = null;
    /**
     * @since 2.1
     */
    protected final IProject project;
    private final LoggingStreamDaemon logger;

    public static final int ERROR_STREAM = 0;
    public static final int INPUT_STREAM = 1;

    /**
     * Spawns the new thread that this class will run in.  From the Runnable
     * interface spawning the new thread automatically calls the run() method.
     * This must be called by the implementing class in order to start the
     * StreamGobbler.
     * @param cmd The entire command to run
     * @param envVars List of all environment variables to use
     * @since 2.0
     */
    public Command(String[] cmd, String... envVars) {
        this(cmd, envVars, null);
    }

    /**
     * Spawns the new thread that this class will run in.  From the Runnable
     * interface spawning the new thread automatically calls the run() method.
     * This must be called by the implementing class in order to start the
     * StreamGobbler.
     * @param cmd The entire command to run
     * @param envVars List of all environment variables to use, or <code>null</code> if none
     * @param project The project this script belongs to, or <code>null</code> if projectless
     * @since 2.1
     */
    public Command(String[] cmd, String[] envVars, IProject project) {
        this.cmd = cmd != null ? Arrays.copyOf(cmd, cmd.length) : null;
        this.envVars = envVars != null ? Arrays.copyOf(envVars, envVars.length) : null;
        this.project = project;
        logger = new LoggingStreamDaemon();
        addInputStreamListener(logger);
    }

    /**
     * Starts the <code>Thread</code> that the new <code>Process</code> will run in.
     * This must be called in order to get the process to start running.
     * Note that this method only takes effect the first time it is called.
     * @throws CoreException If initializing failed.
     */
    public void start() throws CoreException {
        if (started || stopped) {
            return;
        }
        IStatus status = init();
        if (status.isOK()) {
            Thread t = new Thread(this, cmd[0]);
            t.start();
            started = true;
        } else {
            cleanUpAfterStop();
            returnVal = Integer.MIN_VALUE;
            throw new CoreException(status);
        }
    }

    /**
     * Starts up the process that will execute the provided command and registers
     * the <code>StreamGobblers</code> with their respective streams.
     * @return The status from the initializing process
     * @since 2.0
     */
    protected IStatus init() {
        try {
            process = RuntimeProcessFactory.getFactory().exec(cmd, envVars, project);
        } catch (IOException e) {
            return new Status(IStatus.ERROR, StructuresPlugin.PLUGIN_ID, e.getMessage(), e);
        }
        if (process == null) {
            return new Status(IStatus.ERROR, StructuresPlugin.PLUGIN_ID, Messages.Command_failedToRunSystemtap);
        }
        errorGobbler = new StreamGobbler(process.getErrorStream());
        inputGobbler = new StreamGobbler(process.getInputStream());
        transferListeners();
        return Status.OK_STATUS;
    }

    /**
     * This transfers any listeners which may have been added
     * to the command before the process has been constructed
     * properly to the process itself.
     * @since 2.0
     */
    protected void transferListeners() {
        for (IGobblerListener listener : inputListeners) {
            inputGobbler.addDataListener(listener);
        }
        for (IGobblerListener listener : errorListeners) {
            errorGobbler.addDataListener(listener);
        }
    }

    /**
     * This method handles checking the status of the running <code>Process</code>. It
     * is called when the new Thread is created, and thus should never be called by
     * any implementing program. To run call the {@link #start} method.
     */
    @Override
    public void run() {
        errorGobbler.start();
        inputGobbler.start();
        try {
            process.waitFor();
            cleanUpAfterStop();
        } catch (InterruptedException e) {
            // This thread was interrupted while waiting for
            // the process to exit. Destroy the process just
            // to make sure it exits.
            stop();
        }
    }

    /**
     * Performs cleanup operations for when the process ends:
     * Stops the <code>StreamGobblers</code> from monitering
     * the dead process and unregisters the StreamListener.
     * Also wakes up any threads waiting on this command.
     * @since 3.1
     */
    protected synchronized void cleanUpAfterStop() {
        if (!stopped) {
            if (errorGobbler != null) {
                errorGobbler.stop();
            }
            if (inputGobbler != null) {
                inputGobbler.stop();
            }
            removeInputStreamListener(logger);
            stopped = true;
            notifyAll(); // Wake up threads waiting for this command to stop.
        }
    }

    /**
     * Stops the process from running and performs post-stop cleanup if necessary.
     */
    public synchronized void stop() {
        if (!stopped) {
            if (process != null) {
                process.destroy();
            }
            cleanUpAfterStop();
        }
    }

    /**
     * Method to check whether or not the process is running.
     * @return The execution status.
     */
    public boolean isRunning() {
        return !stopped;
    }

    /**
     * Method to check whether or not the process has began to run.
     * @return <code>false</code> before the process begins to run or
     * if initialization of the process has failed; <code>true</code> otherwise.
     * @since 3.0
     */
    public boolean hasStarted() {
        return started;
    }

    /**
     * Method to check if this class has already been disposed.
     * @return Status of the class.
     */
    public boolean isDisposed() {
        return disposed;
    }

    /**
     * The return value of the process.
     * 2^231-1 if the process is still running.
     * -2^231 if there was an error creating the process
     * @return The return value generated from running the provided command.
     */
    public int getReturnValue() {
        return returnVal;
    }

    /**
     * Registers the provided <code>IGobblerListener</code> with the InputStream
     * @param listener A listener to monitor the InputStream from the Process
     */
    public void addInputStreamListener(IGobblerListener listener) {
        if (inputGobbler != null) {
            inputGobbler.addDataListener(listener);
        } else {
            inputListeners.add(listener);
        }
    }

    /**
     * Registers the provided <code>IGobblerListener</code> with the ErrorStream
     * @param listener A listener to monitor the ErrorStream from the Process
     */
    public void addErrorStreamListener(IGobblerListener listener) {
        if (errorGobbler != null) {
            errorGobbler.addDataListener(listener);
        } else {
            errorListeners.add(listener);
        }
    }

    /**
     * Removes the provided listener from those monitoring the InputStream.
     * @param listener An <code>IGobblerListener</code> that is monitoring the stream.
     */
    public void removeInputStreamListener(IGobblerListener listener) {
        if (inputGobbler != null) {
            inputGobbler.removeDataListener(listener);
        } else {
            inputListeners.remove(listener);
        }
    }

    /**
     * Removes the provided listener from those monitoring the ErrorStream.
     * @param listener An <code>IGobblerListener</code> that is monitoring the stream.
     */
    public void removeErrorStreamListener(IGobblerListener listener) {
        if (errorGobbler != null) {
            errorGobbler.removeDataListener(listener);
        } else {
            errorListeners.remove(listener);
        }
    }

    /**
     * Saves the input stream data to a permanent file. Any new data on the
     * stream will automatically be saved to the file.
     * @param file The file to save the InputStream to.
     * @return True if the save was successful, false otherwise.
     */
    public boolean saveLog(File file) {
        return logger.saveLog(file);
    }

    /**
     * Disposes of all internal components of this class. Nothing in the class should be
     * referenced after this is called.
     */
    public synchronized void dispose() {
        if (!disposed) {
            stop();

            if (inputListeners != null) {
                inputListeners.clear();
            }
            inputListeners = null;

            if (errorListeners != null) {
                errorListeners.clear();
            }
            errorListeners = null;

            if (inputGobbler != null) {
                inputGobbler.dispose();
            }
            inputGobbler = null;

            if (errorGobbler != null) {
                errorGobbler.dispose();
            }
            errorGobbler = null;

            logger.dispose();
            process = null;
            disposed = true;
        }
    }

    /**
     * @return The process of this command.
     * @since 3.0
     */
    public Process getProcess() {
        return process;
    }

}
