/*
* Copyright 2012-2015 Broad Institute, Inc.
* 
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
* 
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
* THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package org.broadinstitute.gatk.utils.R;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.broadinstitute.gatk.utils.Utils;
import org.broadinstitute.gatk.utils.exceptions.GATKException;
import org.broadinstitute.gatk.utils.exceptions.UserException;
import org.broadinstitute.gatk.utils.io.IOUtils;
import org.broadinstitute.gatk.utils.io.Resource;
import org.broadinstitute.gatk.utils.runtime.ProcessController;
import org.broadinstitute.gatk.utils.runtime.ProcessSettings;
import org.broadinstitute.gatk.utils.runtime.RuntimeUtils;

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

/**
 * Generic service for executing RScripts
 */
public class RScriptExecutor {
    private static final String RSCRIPT_BINARY = "Rscript";
    private static final File RSCRIPT_PATH = RuntimeUtils.which(RSCRIPT_BINARY);
    public static final boolean RSCRIPT_EXISTS = (RSCRIPT_PATH != null);
    private static final String RSCRIPT_MISSING_MESSAGE = "Please add the Rscript directory to your environment ${PATH}";

    /**
     * our log
     */
    private static Logger logger = Logger.getLogger(RScriptExecutor.class);

    private boolean exceptOnError = false;
    private final List<RScriptLibrary> libraries = new ArrayList<RScriptLibrary>();
    private final List<Resource> scriptResources = new ArrayList<Resource>();
    private final List<File> scriptFiles = new ArrayList<File>();
    private final List<String> args = new ArrayList<String>();

    public void setExceptOnError(boolean exceptOnError) {
        this.exceptOnError = exceptOnError;
    }

    public void addLibrary(RScriptLibrary library) {
        this.libraries.add(library);
    }

    public void addScript(Resource script) {
        this.scriptResources.add(script);
    }

    public void addScript(File script) {
        this.scriptFiles.add(script);
    }

    /**
     * Adds args to the end of the Rscript command line.
     * @param args the args.
     * @throws NullPointerException if any of the args are null.
     */
    public void addArgs(Object... args) {
        for (Object arg: args)
            this.args.add(arg.toString());
    }

    public String getApproximateCommandLine() {
        StringBuilder command = new StringBuilder("Rscript");
        for (Resource script: this.scriptResources)
            command.append(" (resource)").append(script.getFullPath());
        for (File script: this.scriptFiles)
            command.append(" ").append(script.getAbsolutePath());
        for (String arg: this.args)
            command.append(" ").append(arg);
        return command.toString();
    }

    public boolean exec() {
        if (!RSCRIPT_EXISTS) {
            if (exceptOnError) {
                throw new UserException.CannotExecuteRScript(RSCRIPT_MISSING_MESSAGE);
            } else {
                logger.warn("Skipping: " + getApproximateCommandLine());
                return false;
            }
        }

        List<File> tempFiles = new ArrayList<File>();
        try {
            File tempLibSourceDir  = IOUtils.tempDir("RlibSources.", "");
            File tempLibInstallationDir = IOUtils.tempDir("Rlib.", "");
            tempFiles.add(tempLibSourceDir);
            tempFiles.add(tempLibInstallationDir);

            StringBuilder expression = new StringBuilder("tempLibDir = '").append(tempLibInstallationDir).append("';");

            if (this.libraries.size() > 0) {
                List<String> tempLibraryPaths = new ArrayList<String>();
                for (RScriptLibrary library: this.libraries) {
                    File tempLibrary = library.writeLibrary(tempLibSourceDir);
                    tempFiles.add(tempLibrary);
                    tempLibraryPaths.add(tempLibrary.getAbsolutePath());
                }

                expression.append("install.packages(");
                expression.append("pkgs=c('").append(StringUtils.join(tempLibraryPaths, "', '")).append("'), lib=tempLibDir, repos=NULL, type='source', ");
                // Install faster by eliminating cruft.
                expression.append("INSTALL_opts=c('--no-libs', '--no-data', '--no-help', '--no-demo', '--no-exec')");
                expression.append(");");

                for (RScriptLibrary library: this.libraries) {
                    expression.append("library('").append(library.getLibraryName()).append("', lib.loc=tempLibDir);");
                }
            }

            for (Resource script: this.scriptResources) {
                File tempScript = IOUtils.writeTempResource(script);
                tempFiles.add(tempScript);
                expression.append("source('").append(tempScript.getAbsolutePath()).append("');");
            }

            for (File script: this.scriptFiles) {
                expression.append("source('").append(script.getAbsolutePath()).append("');");
            }

            String[] cmd = new String[this.args.size() + 3];
            int i = 0;
            cmd[i++] = RSCRIPT_BINARY;
            cmd[i++] = "-e";
            cmd[i++] = expression.toString();
            for (String arg: this.args)
                cmd[i++] = arg;

            ProcessSettings processSettings = new ProcessSettings(cmd);
            if (logger.isDebugEnabled()) {
                processSettings.getStdoutSettings().printStandard(true);
                processSettings.getStderrSettings().printStandard(true);
            }

            ProcessController controller = ProcessController.getThreadLocal();

            if (logger.isDebugEnabled()) {
                logger.debug("Executing:");
                for (String arg: cmd)
                    logger.debug("  " + arg);
            }
            int exitValue = controller.exec(processSettings).getExitValue();
            logger.debug("Result: " + exitValue);

            if (exitValue != 0)
                throw new RScriptExecutorException(
                        "RScript exited with " + exitValue +
                                (logger.isDebugEnabled() ? "" : ". Run with -l DEBUG for more info."));

            return true;
        } catch (GATKException e) {
            if (exceptOnError) {
                throw e;
            } else {
                logger.warn(e.getMessage());
                return false;
            }
        } finally {
            for (File temp: tempFiles)
                FileUtils.deleteQuietly(temp);
        }
    }
}
