/*
 * The MIT License
 *
 * Copyright (c) 2015 The Broad Institute
 *
 * 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 picard.analysis;

import htsjdk.samtools.util.CollectionUtil;
import htsjdk.samtools.util.Log;
import picard.PicardException;
import picard.analysis.artifacts.CollectSequencingArtifactMetrics;
import picard.cmdline.CommandLineProgram;
import picard.cmdline.CommandLineProgramProperties;
import picard.cmdline.Option;
import picard.cmdline.programgroups.Metrics;
import picard.cmdline.StandardOptionDefinitions;

import java.io.File;
import java.util.*;

/**
 * Class that is designed to instantiate and execute multiple metrics programs that extend
 * SinglePassSamProgram while making only a single pass through the SAM file and supplying
 * each program with the records as it goes.
 *
 * @author Tim Fennell
 */
@CommandLineProgramProperties(

        usage = CollectMultipleMetrics.USAGE_SUMMARY + CollectMultipleMetrics.USAGE_DETAILS,
        usageShort = CollectMultipleMetrics.USAGE_SUMMARY,
        programGroup = Metrics.class
)
public class CollectMultipleMetrics extends CommandLineProgram {

    /**
     * This interface allows developers to create Programs to run in addition to the ones defined in the Program enum.
     * Includes a method for determining whether or not a Program explicitly needs a reference sequence (i.e. cannot be null)
     */

    static final String USAGE_SUMMARY ="Collect multiple classes of metrics.  ";
    static final String USAGE_DETAILS ="This 'meta-metrics' tool runs one or more of the metrics collection modules at the same" +
            " time to cut down on the time spent reading in data from input files. Available modules include " +
            "CollectAlignmentSummaryMetrics, CollectInsertSizeMetrics, QualityScoreDistribution,  MeanQualityByCycle, " +
            "CollectBaseDistributionByCycle, CollectGcBiasMetrics, RnaSeqMetrics, CollectSequencingArtifactMetrics, and CollectQualityYieldMetrics. " +
            "The tool produces outputs of '.pdf' and '.txt' files for each module, except for the " +
            "CollectAlignmentSummaryMetrics module, which outputs only a '.txt' file. Output files are named by specifying a base name " +
            "(without any file extensions).<br /><br />" +
            "" +
            "<p>Currently all programs are run with default options and fixed output extensions, " +
            "but this may become more flexible in future. Specifying a reference sequence file is required.</p>" +

            "<p>Note: Metrics labeled as percentages are actually expressed as fractions!</p>" +
            "" +
            "<h4>Usage example (all modules on by default):</h4>" +
            "<pre>" +
            "java -jar picard.jar CollectMultipleMetrics \\<br />" +
            "      I=input.bam \\<br />" +
            "      O=multiple_metrics \\<br />" +
            "      R=reference_sequence.fasta <br />" +
            "</pre>" +
            "<h4>Usage example (two modules only):</h4>" +
            "java -jar picard.jar CollectMultipleMetrics \\<br />" +
            "      I=input.bam \\<br />" +
            "      O=multiple_metrics \\<br />" +
            "      R=reference_sequence.fasta \\<br />" +
            "      PROGRAM=null \\<br />" +
            "      PROGRAM=QualityScoreDistribution \\<br />" +
            "      PROGRAM=MeanQualityByCycle "+
            "</pre>" +
            "<hr />";
    public static interface ProgramInterface {
        /** By default, this method calls the {@link #makeInstance(String, String, File, File, Set, File, File)} method without 'includeUnpaired' parameter. */
        default  SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference,
                                                   final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals, final boolean includeUnpaired) {
            return makeInstance(outbase, outext, input, reference, metricAccumulationLevel, dbSnp, intervals);
        }
        SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference,
                                          final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals);
        public boolean needsReferenceSequence();
        public boolean supportsMetricAccumulationLevel();
    }

    public static enum Program implements ProgramInterface {
        CollectAlignmentSummaryMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return true;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectAlignmentSummaryMetrics program = new CollectAlignmentSummaryMetrics();
                program.OUTPUT = new File(outbase + ".alignment_summary_metrics" + outext);

                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.METRIC_ACCUMULATION_LEVEL = metricAccumulationLevel;
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        CollectInsertSizeMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return true;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectInsertSizeMetrics program = new CollectInsertSizeMetrics();
                program.OUTPUT = new File(outbase + ".insert_size_metrics" + outext);
                program.Histogram_FILE = new File(outbase + ".insert_size_histogram.pdf");
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.METRIC_ACCUMULATION_LEVEL = metricAccumulationLevel;
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        QualityScoreDistribution {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return false;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final QualityScoreDistribution program = new QualityScoreDistribution();
                program.OUTPUT = new File(outbase + ".quality_distribution_metrics" + outext);
                program.CHART_OUTPUT = new File(outbase + ".quality_distribution.pdf");
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        MeanQualityByCycle {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return false;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final MeanQualityByCycle program = new MeanQualityByCycle();
                program.OUTPUT = new File(outbase + ".quality_by_cycle_metrics" + outext);
                program.CHART_OUTPUT = new File(outbase + ".quality_by_cycle.pdf");
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        CollectBaseDistributionByCycle {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return false;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectBaseDistributionByCycle program = new CollectBaseDistributionByCycle();
                program.OUTPUT = new File(outbase + ".base_distribution_by_cycle_metrics" + outext);
                program.CHART_OUTPUT = new File(outbase + ".base_distribution_by_cycle.pdf");
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        CollectGcBiasMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return true;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return true;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectGcBiasMetrics program = new CollectGcBiasMetrics();
                program.OUTPUT = new File(outbase + ".gc_bias.detail_metrics" + outext);
                program.SUMMARY_OUTPUT = new File(outbase + ".gc_bias.summary_metrics" + outext);
                program.CHART_OUTPUT = new File(outbase + ".gc_bias.pdf");

                program.INPUT = input;
                // previously MetricAccumulationLevel.ALL_READS, MetricAccumulationLevel.LIBRARY
                program.METRIC_ACCUMULATION_LEVEL = metricAccumulationLevel;
                program.SCAN_WINDOW_SIZE = 100;
                program.MINIMUM_GENOME_FRACTION = 1.0E-5;
                program.IS_BISULFITE_SEQUENCED = false;
                program.ASSUME_SORTED = false;
                program.ALSO_IGNORE_DUPLICATES = false;

                //GC_Bias actually uses the class-level REFERENCE_SEQUENCE variable.
                program.REFERENCE_SEQUENCE = reference;

                return program;
            }
        },
        RnaSeqMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return true;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return true;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectRnaSeqMetrics program = new CollectRnaSeqMetrics();
                program.OUTPUT       = new File(outbase + ".rna_metrics" + outext);
                program.CHART_OUTPUT = new File(outbase + ".rna_coverage.pdf");
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.METRIC_ACCUMULATION_LEVEL = metricAccumulationLevel;
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;
                
                return program;
            }
        },
        CollectSequencingArtifactMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return true;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() { return false; }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                return makeInstance(outbase, outext, input, reference, metricAccumulationLevel, dbSnp, intervals, false);
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals, final boolean includeUnpaired) {
                final CollectSequencingArtifactMetrics program = new CollectSequencingArtifactMetrics();
                program.OUTPUT = new File(outbase);
                program.FILE_EXTENSION = outext;
                program.DB_SNP = dbSnp;
                program.INTERVALS = intervals;
                program.INCLUDE_UNPAIRED = includeUnpaired;
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;
                return program;
            }
        },
        CollectQualityYieldMetrics {
            @Override
            public boolean needsReferenceSequence() {
                return false;
            }
            @Override
            public boolean supportsMetricAccumulationLevel() {
                return false;
            }
            @Override
            public SinglePassSamProgram makeInstance(final String outbase, final String outext, final File input, final File reference, final Set<MetricAccumulationLevel> metricAccumulationLevel, final File dbSnp, final File intervals) {
                final CollectQualityYieldMetrics program = new CollectQualityYieldMetrics();
                program.OUTPUT = new File(outbase + ".quality_yield_metrics" + outext);
                // Generally programs should not be accessing these directly but it might make things smoother
                // to just set them anyway. These are set here to make sure that in case of a the derived class
                // overrides
                program.INPUT = input;
                program.REFERENCE_SEQUENCE = reference;
                return program;
            }
        }
    }

    @Option(shortName = StandardOptionDefinitions.INPUT_SHORT_NAME, doc = "Input SAM or BAM file.")
    public File INPUT;

    @Option(doc = "If true (default), then the sort order in the header file will be ignored.",
            shortName = StandardOptionDefinitions.ASSUME_SORTED_SHORT_NAME)
    public boolean ASSUME_SORTED = true;

    @Option(doc = "Stop after processing N reads, mainly for debugging.")
    public int STOP_AFTER = 0;

    @Option(shortName = StandardOptionDefinitions.OUTPUT_SHORT_NAME, doc = "Base name of output files.")
    public String OUTPUT;

    // create the default accumulation level as a variable. We'll use this to init the command-line arg and for validation later.
    private final Set<MetricAccumulationLevel> accumLevelDefault = CollectionUtil.makeSet(MetricAccumulationLevel.ALL_READS);

    @Option(shortName="LEVEL", doc="The level(s) at which to accumulate metrics.")
    public Set<MetricAccumulationLevel> METRIC_ACCUMULATION_LEVEL = new HashSet<>(accumLevelDefault);

    @Option(shortName = "EXT", doc="Append the given file extension to all metric file names (ex. OUTPUT.insert_size_metrics.EXT). None if null", optional=true)
    public String FILE_EXTENSION = null;

    @Option(doc = "Set of metrics programs to apply during the pass through the SAM file.")
    public Set<Program> PROGRAM = new LinkedHashSet<>(Arrays.asList(Program.CollectAlignmentSummaryMetrics, Program.CollectBaseDistributionByCycle,
            Program.CollectInsertSizeMetrics, Program.MeanQualityByCycle, Program.QualityScoreDistribution));

    @Option(doc = "An optional list of intervals to restrict analysis to. Only pertains to some of the PROGRAMs. Programs whose stand-alone CLP does not " +
            "have an INTERVALS argument will silently ignore this argument.", optional = true)
    public File INTERVALS;

    @Option(doc = "VCF format dbSNP file, used to exclude regions around known polymorphisms from analysis " +
            "by some PROGRAMs; PROGRAMs whose CLP doesn't allow for this argument will quietly ignore it.", optional = true)
    public File DB_SNP;

    @Option(shortName = "UNPAIRED", doc = "Include unpaired reads in CollectSequencingArtifactMetrics. If set to true then all paired reads will be included as well - " +
            "MINIMUM_INSERT_SIZE and MAXIMUM_INSERT_SIZE will be ignored in CollectSequencingArtifactMetrics.")
    public boolean INCLUDE_UNPAIRED = false;
    /**
     * Contents of PROGRAM set is transferred to this set during command-line validation, so that an outside
     * developer can invoke this class programmatically and provide alternative Programs to run by calling
     * setProgramsToRun().
     */
    private Set<ProgramInterface> programsToRun;

    private static final Log log = Log.getInstance(CollectMultipleMetrics.class);

    // Stock main method
    public static void main(final String[] args) {
        new CollectMultipleMetrics().instanceMainWithExit(args);
    }

    @Override
    protected String[] customCommandLineValidation() {
        if (PROGRAM.isEmpty()) {
            return new String[]{"No programs specified with PROGRAM"};
        }
        programsToRun = new LinkedHashSet<>(PROGRAM);

        return super.customCommandLineValidation();
    }

    /**
     * Use this method when invoking CollectMultipleMetrics programmatically to run programs other than the ones
     * available via enum.  This must be called before doWork().
     */
    public void setProgramsToRun(final Collection<ProgramInterface> programsToRun) {
        this.programsToRun.clear();
        this.programsToRun.addAll(programsToRun);
    }

    @Override
    public int doWork() {
        if (OUTPUT.endsWith(".")) {
            OUTPUT = OUTPUT.substring(0, OUTPUT.length() - 1);
        }

        final List<SinglePassSamProgram> programs = new ArrayList<>();
        for (final ProgramInterface program : programsToRun) {
            if (program.needsReferenceSequence() && REFERENCE_SEQUENCE == null) {
                throw new PicardException("The " + program.toString() + " program needs a Reference Sequence, please set REFERENCE_SEQUENCE in the command line");
            }
            if (!accumLevelDefault.equals(METRIC_ACCUMULATION_LEVEL) && !program.supportsMetricAccumulationLevel()) {
                log.warn("The " + program.toString() + " program does not support a metric accumulation level, but METRIC_ACCUMULATION_LEVEL" +
                        " was overridden in the command line. " + program.toString() + " will be run against the entire input.");
            }

            final String outext = (null != FILE_EXTENSION) ? FILE_EXTENSION : ""; // Add a file extension if desired
            final SinglePassSamProgram instance = program.makeInstance(OUTPUT, outext, INPUT, REFERENCE_SEQUENCE, METRIC_ACCUMULATION_LEVEL, DB_SNP, INTERVALS, INCLUDE_UNPAIRED);

            // Generally programs should not be accessing these directly but it might make things smoother
            // to just set them anyway
            instance.INPUT = INPUT;
            instance.REFERENCE_SEQUENCE = REFERENCE_SEQUENCE;

            instance.setDefaultHeaders(getDefaultHeaders());

            programs.add(instance);
        }
        SinglePassSamProgram.makeItSo(INPUT, REFERENCE_SEQUENCE, ASSUME_SORTED, STOP_AFTER, programs);

        return 0;
    }
}
