/*
 * RAMPART - Robust Automatic MultiPle AssembleR Toolkit
 * Copyright (C) 2015  Daniel Mapleson - TGAC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.tgac.rampart.stage;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import uk.ac.ebi.fgpt.conan.core.context.DefaultExecutionResult;
import uk.ac.ebi.fgpt.conan.core.param.*;
import uk.ac.ebi.fgpt.conan.core.process.AbstractConanProcess;
import uk.ac.ebi.fgpt.conan.core.process.AbstractProcessArgs;
import uk.ac.ebi.fgpt.conan.model.context.ExecutionContext;
import uk.ac.ebi.fgpt.conan.model.context.ExecutionResult;
import uk.ac.ebi.fgpt.conan.model.context.ResourceUsage;
import uk.ac.ebi.fgpt.conan.model.param.AbstractProcessParams;
import uk.ac.ebi.fgpt.conan.model.param.ConanParameter;
import uk.ac.ebi.fgpt.conan.model.param.ParamMap;
import uk.ac.ebi.fgpt.conan.service.ConanExecutorService;
import uk.ac.ebi.fgpt.conan.service.exception.ConanParameterException;
import uk.ac.ebi.fgpt.conan.service.exception.ProcessExecutionException;
import uk.ac.tgac.conan.core.data.Library;
import uk.ac.tgac.conan.core.data.Organism;
import uk.ac.tgac.conan.core.util.XmlHelper;
import uk.ac.tgac.conan.process.asmIO.AssemblyEnhancer;
import uk.ac.tgac.conan.process.asmIO.AssemblyEnhancerFactory;
import uk.ac.tgac.rampart.stage.util.ReadsInput;

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

/**
 * User: maplesod
 * Date: 16/08/13
 * Time: 15:52
 */
public class AmpStage extends AbstractConanProcess {


    private static Logger log = LoggerFactory.getLogger(AmpStage.class);
    private Mecq.Sample sample;

    public AmpStage() {
        this(null);
    }

    public AmpStage(ConanExecutorService ces) {
        this(ces, new Args(), null);
    }

    public AmpStage(ConanExecutorService ces, Args args, Mecq.Sample sample) {
        super("", args, new Params(), ces);
        this.sample = sample;
    }

    public Args getArgs() {
        return (Args)this.getProcessArgs();
    }

    /**
     * Dispatches amp stage to the specified environments
     *
     * @param executionContext The environment to dispatch jobs to
     * @throws ProcessExecutionException Thrown if there is an issue during execution of an external process
     * @throws InterruptedException Thrown if user has interrupted the process during execution
     */
    @Override
    public ExecutionResult execute(ExecutionContext executionContext) throws ProcessExecutionException, InterruptedException {

        try {

            StopWatch stopWatch = new StopWatch();
            stopWatch.start();

            // Make a shortcut to the args
            Args args = this.getArgs();

            log.info("Starting AMP stage " + args.getIndex());

            // Make sure reads file exists
            if (args.getInputAssembly() == null || !args.getInputAssembly().exists()) {
                throw new IOException("Input file for stage: " + args.getIndex() + " does not exist: " +
                        (args.getInputAssembly() == null ? "null" : args.getInputAssembly().getAbsolutePath()));
            }

            // Make sure the inputs are reasonable
            List<Library> selectedLibs = this.validateInputs(args.getIndex(), args.getInputs(), args.getSample());

            // Create output directory
            if (!args.getOutputDir().exists()) {
                args.getOutputDir().mkdir();
            }

            // Create the configuration for this stage
            AssemblyEnhancer ampProc = this.makeStage(args, selectedLibs);

            // Set a suitable execution context
            ExecutionContext ecCopy = executionContext.copy();
            ecCopy.setContext("AMP-" + args.getIndex(), true, new File(args.getOutputDir(), "amp-" + args.getIndex() + ".log"));
            if (ecCopy.usingScheduler()) {
                ecCopy.getScheduler().getArgs().setThreads(args.getThreads());
                ecCopy.getScheduler().getArgs().setMemoryMB(args.getMemory());
            }

            // Do any setup for this process
            ampProc.setup();

            // Execute the AMP stage
            ExecutionResult result = ampProc.execute(ecCopy);

            if (!ampProc.getOutputFile().exists()) {
                throw new ProcessExecutionException(2, "AMP stage " + args.index + "\" did not produce an output file");
            }

            // Create links for outputs from this assembler to known locations
            this.getConanProcessService().createLocalSymbolicLink(ampProc.getOutputFile(), args.getOutputFile());

            stopWatch.stop();

            log.info("Finished AMP stage " + args.getIndex());

            return new DefaultExecutionResult(
                    Integer.toString(args.index) + "-" + args.tool,
                    0,
                    result.getOutput(),
                    null,
                    -1,
                    new ResourceUsage(result.getResourceUsage() == null ? 0 : result.getResourceUsage().getMaxMem(),
                            stopWatch.getTime() / 1000,
                            result.getResourceUsage() == null ? 0 : result.getResourceUsage().getCpuTime()));
        }
        catch (IOException | ConanParameterException e) {
            throw new ProcessExecutionException(-1, e);
        }
    }

    protected AssemblyEnhancer makeStage(Args args, List<Library> libs) throws IOException {

        return AssemblyEnhancerFactory.create(
                args.getTool(),
                args.getInputAssembly(),
                args.getBubbleFile() != null && args.getBubbleFile().exists() ? args.getBubbleFile() : null,
                args.getOutputDir(),
                "amp-" + args.getIndex(),
                libs, args.getThreads(),
                args.getMemory(),
                args.getCheckedArgs(),
                args.getUncheckedArgs(),
                this.conanExecutorService);
    }


    @Override
    public String getCommand() {
        return null;
    }

    @Override
    public String getName() {
        // Make a shortcut to the args
        Args args = (Args) this.getProcessArgs();

        return args != null ? "AMP-" + args.getIndex() + " - " + args.getTool() : "Undefined-AMP-stage";
    }

    @Override
    public boolean isOperational(ExecutionContext executionContext) {

        Args args = (Args)this.getProcessArgs();

        AssemblyEnhancer proc = null;

        try {
            proc = this.makeStage(args, null);
        } catch (IOException e) {
            log.warn("Could not create AMP stage " + args.getIndex() + " for tool: " + args.getTool() + "; check tool is installed and configured correctly.");
            return false;
        }

        if (proc == null) {
            log.warn("Could not create AMP stage " + args.getIndex() + " for tool: " + args.getTool() + "; Tool not recognised.  Check tool is supported and you have the correct spelling.");
            return false;
        }

        if (args.getCheckedArgs() != null && !args.getCheckedArgs().trim().isEmpty()) {
            try {
                proc.getAssemblyEnhancerArgs().parse(args.getCheckedArgs());
            } catch (IOException e) {
                throw new IllegalArgumentException("Invalid or unrecognised checked arguments provided to " + args.tool + " in stage " + args.index, e);
            }
        }

        return proc.isOperational(executionContext);
    }

    protected List<Library> validateInputs(int ampIndex, List<ReadsInput> inputs, Mecq.Sample sample) throws IOException {

        List<Library> selectedLibs = new ArrayList<>();

        for(ReadsInput mi : inputs) {
            Library lib = mi.findLibrary(sample.libraries);
            Mecq.EcqArgs ecqArgs = mi.findMecq(sample);

            if (lib == null) {
                throw new IOException("Unrecognised library: " + mi.getLib() + "; not processing AMP stage: " + ampIndex);
            }

            if (ecqArgs == null) {
                if (mi.getEcq().equalsIgnoreCase(Mecq.EcqArgs.RAW)) {
                    selectedLibs.add(lib);
                }
                else {
                    throw new IOException("Unrecognised MECQ dataset requested: " + mi.getEcq() + "; not processing AMP stage: " + ampIndex);
                }
            }
            else {
                selectedLibs.add(ecqArgs.getOutputLibrary(sample, lib));
            }

            log.info("Found library.  Lib name: " + mi.getLib() + "; ECQ name: " + mi.getEcq());
        }

        return selectedLibs;
    }

    public static class Args extends AbstractProcessArgs {

        private static final String KEY_ATTR_TOOL = "tool";
        private static final String KEY_ATTR_THREADS = "threads";
        private static final String KEY_ATTR_MEMORY = "memory";
        private static final String KEY_ATTR_CHECKED_ARGS = "checked_args";
        private static final String KEY_ATTR_UNCHECKED_ARGS = "unchecked_args";
        private static final String KEY_ELEM_INPUTS = "inputs";
        private static final String KEY_ELEM_SINGLE_INPUT = "input";


        // Defaults
        public static final int DEFAULT_THREADS = 1;
        public static final int DEFAULT_MEMORY = 0;


        // Common stuff
        private File outputDir;
        private File assembliesDir;
        private String jobPrefix;
        private Mecq.Sample sample;
        private Organism organism;

        // Specifics
        private String tool;
        private File inputAssembly;
        private File bubbleFile;
        private List<ReadsInput> inputs;
        private int index;
        private int threads;
        private int memory;
        private String checkedArgs;
        private String uncheckedArgs;


        public Args() {

            super(new Params());

            this.tool = "";
            this.inputAssembly = null;
            this.bubbleFile = null;
            this.index = 0;
            this.threads = DEFAULT_THREADS;
            this.memory = DEFAULT_MEMORY;
            this.checkedArgs = null;
            this.uncheckedArgs = null;

            this.outputDir = new File("");
            this.jobPrefix = "AMP-" + this.index;
            this.inputs = new ArrayList<>();
            this.sample = null;
            this.organism = null;
        }

        public Args(Element ele, File outputDir, File assembliesDir, String jobPrefix, Mecq.Sample sample, Organism organism,
                    File inputAssembly, File bubbleFile, int index) throws IOException {

            // Set defaults
            this();

            // Check there's nothing unexpected in this element
            if (!XmlHelper.validate(ele,
                    new String[] {
                            KEY_ATTR_TOOL
                    },
                    new String[]{
                            KEY_ATTR_THREADS,
                            KEY_ATTR_MEMORY,
                            KEY_ATTR_CHECKED_ARGS,
                            KEY_ATTR_UNCHECKED_ARGS
                    },
                    new String[]{
                            KEY_ELEM_INPUTS
                    },
                    new String[0])) {
                throw new IllegalArgumentException("Found unrecognised element or attribute in AMP stage: " + index);
            }

            // Required
            if (!ele.hasAttribute(KEY_ATTR_TOOL))
                throw new IOException("Could not find " + KEY_ATTR_TOOL + " attribute in AMP stage element");

            this.tool = XmlHelper.getTextValue(ele, KEY_ATTR_TOOL);
            this.inputAssembly = inputAssembly;
            this.bubbleFile = bubbleFile;
            this.sample = sample;

            // Required Elements
            Element inputElements = XmlHelper.getDistinctElementByName(ele, KEY_ELEM_INPUTS);
            NodeList actualInputs = inputElements.getElementsByTagName(KEY_ELEM_SINGLE_INPUT);
            for(int i = 0; i < actualInputs.getLength(); i++) {
                ReadsInput ri = new ReadsInput((Element) actualInputs.item(i));

                if (!foundInLibs(ri.getLib(), sample.libraries)) {
                    throw new IOException("Could not find library \"" + ri.getLib() + "\" in defined libraries");
                }

                if (!foundInMecq(ri.getEcq(), sample.ecqArgList)) {
                    throw new IOException("Could not find ECQ \"" + ri.getEcq() + "\" in defined set of ECQs");
                }

                this.inputs.add(ri);
            }

            // Optional
            this.threads = ele.hasAttribute(KEY_ATTR_THREADS) ? XmlHelper.getIntValue(ele, KEY_ATTR_THREADS) : DEFAULT_THREADS;
            this.memory = ele.hasAttribute(KEY_ATTR_MEMORY) ? XmlHelper.getIntValue(ele, KEY_ATTR_MEMORY) : DEFAULT_MEMORY;
            this.checkedArgs = ele.hasAttribute(KEY_ATTR_CHECKED_ARGS) ? XmlHelper.getTextValue(ele, KEY_ATTR_CHECKED_ARGS) : null;
            this.uncheckedArgs = ele.hasAttribute(KEY_ATTR_UNCHECKED_ARGS) ? XmlHelper.getTextValue(ele, KEY_ATTR_UNCHECKED_ARGS) : null;

            // Other args
            this.outputDir = outputDir;
            this.assembliesDir = assembliesDir;
            this.jobPrefix = jobPrefix;
            this.organism = organism;
            this.index = index;
        }

        private boolean foundInLibs(String libName, List<Library> ll) {

            for(Library s : ll) {
                if (libName.equalsIgnoreCase(s.getName())) {
                    return true;
                }
            }

            return false;
        }

        private boolean foundInMecq(String ecqName, List<Mecq.EcqArgs> ll) {

            if (ecqName.equalsIgnoreCase("raw")) {
                return true;
            }

            for(Mecq.EcqArgs s : ll) {
                if (ecqName.equalsIgnoreCase(s.getName())) {
                    return true;
                }
            }

            return false;
        }

        public Params getParams() {
            return (Params)this.params;
        }

        public File getOutputDir() {
            return outputDir;
        }

        public void setOutputDir(File outputDir) {
            this.outputDir = outputDir;
        }

        public String getJobPrefix() {
            return jobPrefix;
        }

        public void setJobPrefix(String jobPrefix) {
            this.jobPrefix = jobPrefix;
        }

        public Mecq.Sample getSample() {
            return sample;
        }

        public void setSample(Mecq.Sample sample) {
            this.sample = sample;
        }

        public Organism getOrganism() {
            return organism;
        }

        public void setOrganism(Organism organism) {
            this.organism = organism;
        }

        public String getTool() {
            return tool;
        }

        public void setTool(String tool) {
            this.tool = tool;
        }

        public File getInputAssembly() {
            return inputAssembly;
        }

        public void setInputAssembly(File inputAssembly) {
            this.inputAssembly = inputAssembly;
        }

        public File getBubbleFile() {
            return bubbleFile;
        }

        public void setBubbleFile(File bubbleFile) {
            this.bubbleFile = bubbleFile;
        }

        public File getOutputFile() {
            return new File(this.assembliesDir, "amp-stage-" + this.index + ".fa");
        }


        public int getIndex() {
            return index;
        }

        public void setIndex(int index) {
            this.index = index;
        }

        public File getAssembliesDir() {
            return assembliesDir;
        }

        public void setAssembliesDir(File assembliesDir) {
            this.assembliesDir = assembliesDir;
        }

        public int getThreads() {
            return threads;
        }

        public void setThreads(int threads) {
            this.threads = threads;
        }

        public int getMemory() {
            return memory;
        }

        public void setMemory(int memory) {
            this.memory = memory;
        }

        public String getCheckedArgs() {
            return checkedArgs;
        }

        public void setCheckedArgs(String checkedArgs) {
            this.checkedArgs = checkedArgs;
        }

        public String getUncheckedArgs() {
            return uncheckedArgs;
        }

        public void setUncheckedArgs(String uncheckedArgs) {
            this.uncheckedArgs = uncheckedArgs;
        }

        public List<ReadsInput> getInputs() {
            return inputs;
        }

        @Override
        public void parseCommandLine(CommandLine cmdLine) {

            Params params = this.getParams();
        }

        @Override
        public ParamMap getArgMap() {

            Params params = this.getParams();

            ParamMap pvp = new DefaultParamMap();

            if (this.inputAssembly != null)
                pvp.put(params.getInput(), this.inputAssembly.getAbsolutePath());

            if (this.bubbleFile != null)
                pvp.put(params.getBubbleFile(), this.bubbleFile.getAbsolutePath());

            if (this.outputDir != null)
                pvp.put(params.getOutputDir(), this.outputDir.getAbsolutePath());

            if (this.jobPrefix != null)
                pvp.put(params.getJobPrefix(), this.jobPrefix);

            pvp.put(params.getThreads(), Integer.toString(this.threads));
            pvp.put(params.getMemory(), Integer.toString(this.memory));

            if (this.checkedArgs != null)
                pvp.put(params.getCheckedArgs(), this.checkedArgs);

            if (this.uncheckedArgs != null)
                pvp.put(params.getUncheckedArgs(), this.uncheckedArgs);

            return pvp;
        }

        @Override
        protected void setOptionFromMapEntry(ConanParameter param, String value) {

            Params params = this.getParams();

            if (param.equals(params.getInput())) {
                this.inputAssembly = new File(value);
            } else if (param.equals(params.getBubbleFile())) {
                this.bubbleFile = new File(value);
            } else if (param.equals(params.getOutputDir())) {
                this.outputDir = new File(value);
            } else if (param.equals(params.getJobPrefix())) {
                this.jobPrefix = value;
            } else if (param.equals(params.getThreads())) {
                this.threads = Integer.parseInt(value);
            } else if (param.equals(params.getMemory())) {
                this.memory = Integer.parseInt(value);
            } else if (param.equals(params.getCheckedArgs())) {
                this.checkedArgs = value;
            } else if (param.equals(params.getUncheckedArgs())) {
                this.uncheckedArgs = value;
            }
        }

        @Override
        protected void setArgFromMapEntry(ConanParameter param, String value) {
            //To change body of implemented methods use File | Settings | File Templates.
        }
    }

    public static class Params extends AbstractProcessParams {

        private ConanParameter input;
        private ConanParameter bubbleFile;
        private ConanParameter outputDir;
        private ConanParameter jobPrefix;
        private ConanParameter threads;
        private ConanParameter memory;
        private ConanParameter checkedArgs;
        private ConanParameter uncheckedArgs;

        public Params() {

            this.input = new PathParameter(
                    "input",
                    "The input assembly containing the assembly to enhance",
                    true
            );

            this.bubbleFile = new PathParameter(
                    "bubble",
                    "The input assembly containing the assembly to enhance",
                    true
            );

            this.outputDir = new PathParameter(
                    "output",
                    "The output directory which should contain the enhancement steps",
                    true
            );

            this.jobPrefix = new ParameterBuilder()
                    .longName("job_prefix")
                    .description("Describes the jobs that will be executed as part of this pipeline")
                    .create();

            this.threads = new NumericParameter(
                    "threads",
                    "The number of threads to use for this AMP stage",
                    true
            );

            this.memory = new NumericParameter(
                    "memory",
                    "The amount of memory to request for this AMP stage",
                    true
            );

            this.checkedArgs = new ParameterBuilder()
                    .longName("checked_args")
                    .description("Any additional arguments to provide to this specific process.  MUST be in posix format.  Will be checked by wrappers")
                    .argValidator(ArgValidator.OFF)
                    .create();

            this.uncheckedArgs = new ParameterBuilder()
                    .longName("checked_args")
                    .description("Any additional arguments to provide to this specific process.  Will NOT be checked by wrappers.  Will be passed as is to the underlying process.")
                    .argValidator(ArgValidator.OFF)
                    .create();
        }

        public ConanParameter getInput() {
            return input;
        }

        public ConanParameter getBubbleFile() {
            return bubbleFile;
        }

        public ConanParameter getOutputDir() {
            return outputDir;
        }

        public ConanParameter getJobPrefix() {
            return jobPrefix;
        }

        public ConanParameter getThreads() {
            return threads;
        }

        public ConanParameter getMemory() {
            return memory;
        }

        public ConanParameter getCheckedArgs() {
            return checkedArgs;
        }

        public ConanParameter getUncheckedArgs() {
            return uncheckedArgs;
        }

        @Override
        public ConanParameter[] getConanParametersAsArray() {
            return new ConanParameter[]{
                    this.input,
                    this.bubbleFile,
                    this.outputDir,
                    this.jobPrefix,
                    this.threads,
                    this.memory,
                    this.checkedArgs,
                    this.uncheckedArgs
            };
        }

    }

}