/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.execute;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.ExecTask;
import org.apache.tools.ant.types.Commandline;
import org.dom4j.Document;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.xml.XMLReceiverHelper;
import org.orbeon.oxf.xml.XPathUtils;
import org.xml.sax.ContentHandler;

import java.io.File;

/**
 * The Execute Processor allows executing an external command.
 *
 * TODO: Currently, we store the outputs using ant properties. This can cause the processor to take up a lot of memory.
 *
 * TODO: Handle stdin if connected.
 */
public class ExecuteProcessor extends ProcessorImpl {

//    private static Logger logger = LoggerFactory.createLogger(ExecuteProcessor.class);

    public static final String EXECUTE_PROCESSOR_CONFIG_NAMESPACE_URI = "http://orbeon.org/oxf/xml/execute-processor-config";

    private static final String OUTPUT_PROPERTY = "org.orbeon.oxf.processor.execute.output";
    private static final String ERROR_PROPERTY = "org.orbeon.oxf.processor.execute.error";
    private static final String RESULT_PROPERTY = "org.orbeon.oxf.processor.execute.result";

//    private static final String INPUT_STDIN = "stdin";
    private static final String OUTPUT_STDOUT = "stdout";
    private static final String OUTPUT_STDERR = "stderr";
    private static final String OUTPUT_RESULT = "result";

    public ExecuteProcessor() {
        addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, EXECUTE_PROCESSOR_CONFIG_NAMESPACE_URI));
//        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_PROPERTY)); // optional
//        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_STDERR)); // optional
//        addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_RESULT  )); // optional
    }

    private static class Config {

        public String executable;
        public String dir;
        public boolean spawn;
        public String output;
        public String error;
        public boolean logError;
        public boolean append;
        public String input;
        public String inputstring;
        public String resultproperty;
        public Integer timeout;
        public boolean failonerror; // -> exception?
        public boolean failifexecutionfails; // -> exception?
        public boolean newenvironment;
        public boolean vmlauncher;
        public boolean resolveexecutable;
        public boolean searchpath;

        public String argLine;

        public Config(Document document) {
            // Directory and file
            executable = XPathUtils.selectStringValueNormalize(document, "/*/@executable");
            dir = XPathUtils.selectStringValueNormalize(document, "/*/@dir");
            spawn = ProcessorUtils.selectBooleanValue(document, "/*/@spawn", false);
            output = XPathUtils.selectStringValueNormalize(document, "/*/@output");
            error = XPathUtils.selectStringValueNormalize(document, "/*/@error");
            logError = ProcessorUtils.selectBooleanValue(document, "/*/@logError", false);
            append = ProcessorUtils.selectBooleanValue(document, "/*/@append", false);
            input = XPathUtils.selectStringValueNormalize(document, "/*/@input");
            inputstring = XPathUtils.selectStringValueNormalize(document, "/*/@inputstring");
            timeout = XPathUtils.selectIntegerValue(document, "/*/@timeout");

            failonerror = ProcessorUtils.selectBooleanValue(document, "/*/@error", false);
            failifexecutionfails = ProcessorUtils.selectBooleanValue(document, "/*/@failifexecutionfails", true);
            newenvironment = ProcessorUtils.selectBooleanValue(document, "/*/@newenvironment", false);
            vmlauncher = ProcessorUtils.selectBooleanValue(document, "/*/@vmlauncher", true);
            resolveexecutable = ProcessorUtils.selectBooleanValue(document, "/*/@resolveexecutable", false);
            searchpath = ProcessorUtils.selectBooleanValue(document, "/*/@searchpath", false);

            // TODO: other args options
            argLine = XPathUtils.selectStringValue(document, "/*/arg[1]/@line");

            // value
            // file
            // path
            // pathref [no]

            // TODO: set max size for output
        }
    }

    public void doIt(PipelineContext pipelineContext, String outputName, XMLReceiver xmlReceiver) {
        try {
            // Read config
            final Config config = readCacheInputAsObject(pipelineContext, getInputByName(INPUT_CONFIG), new CacheableInputReader<Config>() {
                public Config read(PipelineContext context, ProcessorInput input) {
                    return new Config(readInputAsDOM4J(context, input));
                }
            });

            // Create task
            final ExecTask execTask = new ExecTask();
            final Project project = new Project();
            execTask.setProject(project);

            // Set mandatory executable
            execTask.setExecutable(config.executable);
            if (config.dir != null)
                execTask.setDir(new File(config.dir));

            // Set optional command-line arguments
            if (config.argLine != null) {
                final Commandline.Argument argument = execTask.createArg();
                argument.setLine(config.argLine);
            }

            // Set optional spawn
            execTask.setSpawn(config.spawn);

            // Set output file if available
            final File outputFile;
            if (config.output != null) {
                outputFile = new File(config.output);
            } else {
//                final FileItem fileItem = XMLUtils.prepareFileItem(pipelineContext);
//                if (fileItem instanceof DefaultFileItem) {
//                    final DefaultFileItem defaultFileItem = (DefaultFileItem) fileItem;
//                    outputFile = defaultFileItem.getStoreLocation();
//                } else {
                    outputFile = null;
//                }
            }

            if (outputFile != null)
                execTask.setOutput(outputFile);

            // Set output property
            execTask.setOutputproperty(OUTPUT_PROPERTY);

            // Handle result code
            execTask.setResultProperty(RESULT_PROPERTY);

            // Set error file is available
            final File errorFile;
            if (config.error != null) {
                errorFile = new File(config.error);
            } else {
                errorFile = null;
            }

            if (errorFile != null)
                execTask.setError(errorFile);

            // Set error property
            execTask.setErrorProperty(ERROR_PROPERTY);

            // Set optional append
            execTask.setAppend(config.append);

            // TODO: Handle input
//            execTask.setInput(config.input;
//            execTask.setInputString(config.inputstring);
//
            // Set command timeout
            if (config.timeout != null)
                execTask.setTimeout(config.timeout);

            // Other boolean properties
            execTask.setFailonerror(config.failonerror);
            execTask.setFailIfExecutionFails(config.failifexecutionfails);
            execTask.setNewenvironment(config.newenvironment);
            execTask.setVMLauncher(config.vmlauncher);
            execTask.setResolveExecutable(config.resolveexecutable);
            execTask.setSearchPath(config.searchpath);

//            logger.info("xxx before execute");
            execTask.execute();
//            logger.info("xxx after execute");

            final State state = (State) getState(pipelineContext);
            state.isRead = true;

            if (xmlReceiver != null) {
                // Output or remember stdout
                state.outputString = project.getProperty(OUTPUT_PROPERTY);
                if (outputName.equals(OUTPUT_STDOUT)) {
                    outputStdout(xmlReceiver, state);
                }

                // Output or remember stderr
                state.errorString = project.getProperty(ERROR_PROPERTY);
                if (outputName.equals(OUTPUT_STDERR)) {
                    outputStderr(xmlReceiver, state);
                }

                // Output or remember result
                state.result = project.getProperty(RESULT_PROPERTY);
//                logger.info("xxx: result " + state.result);
                if (outputName.equals(OUTPUT_RESULT)) {
                    outputResult(xmlReceiver, state);
                }
            }

        } catch (Exception e) {
            throw new OXFException(e);
        }
    }

    private void outputStdout(ContentHandler contentHandler, State state) {
        ProcessorUtils.readText(state.outputString, contentHandler, "text/plain", null);
        state.outputString = null;
    }

    private void outputStderr(ContentHandler contentHandler, State state) {
        ProcessorUtils.readText(state.errorString, contentHandler, "text/plain", null);
        state.errorString = null;
    }

    private void outputResult(XMLReceiver xmlReceiver, State state) {
        final XMLReceiverHelper helper = new XMLReceiverHelper(xmlReceiver);
        helper.startDocument();
        helper.startElement("result");
        helper.text(state.result);
        helper.endElement();
        helper.endDocument();
    }

    /**
     * Case where an output must be generated.
     */
    @Override
    public ProcessorOutput createOutput(final String outputName) {
        final ProcessorOutput output = new ProcessorOutputImpl(ExecuteProcessor.this, outputName) {
            public void readImpl(final PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
                final State state = (State) getState(pipelineContext);
                if (!state.isRead) {
                    // We haven't run the command yet
                    doIt(pipelineContext, outputName, xmlReceiver);
                } else {
                    // We have already run the command
                    if (outputName.equals(OUTPUT_STDOUT)) {
                        outputStdout(xmlReceiver, state);
                    } else if (outputName.equals(OUTPUT_STDERR)) {
                        outputStderr(xmlReceiver, state);
                    } else if (outputName.equals(OUTPUT_RESULT)) {
                        outputResult(xmlReceiver, state);
                    }
                }
            }
        };
        addOutput(outputName, output);
        return output;
    }

    /**
     * Case where no output is generated.
     */
    @Override
    public void start(PipelineContext pipelineContext) {
        doIt(pipelineContext, null, null);
    }

    @Override
    public void reset(PipelineContext pipelineContext) {
        setState(pipelineContext, new State());
    }

    private static class State {
        public boolean isRead;
        public String outputString;
        public String errorString;
        public String result;
    }
}