/*
 * Copyright (c) 2007-2010 by The Broad Institute, Inc. and the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * This software is licensed under the terms of the GNU Lesser General Public License (LGPL), Version 2.1 which
 * is available at http://www.opensource.org/licenses/lgpl-2.1.php.
 *
 * THE SOFTWARE IS PROVIDED "AS IS." THE BROAD AND MIT MAKE NO REPRESENTATIONS OR WARRANTIES OF
 * ANY KIND CONCERNING THE SOFTWARE, EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT
 * OR OTHER DEFECTS, WHETHER OR NOT DISCOVERABLE.  IN NO EVENT SHALL THE BROAD OR MIT, OR THEIR
 * RESPECTIVE TRUSTEES, DIRECTORS, OFFICERS, EMPLOYEES, AND AFFILIATES BE LIABLE FOR ANY DAMAGES OF
 * ANY KIND, INCLUDING, WITHOUT LIMITATION, INCIDENTAL OR CONSEQUENTIAL DAMAGES, ECONOMIC
 * DAMAGES OR INJURY TO PROPERTY AND LOST PROFITS, REGARDLESS OF WHETHER THE BROAD OR MIT SHALL
 * BE ADVISED, SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.tools;

import jargs.gnu.CmdLineParser;
import org.broad.igv.Globals;
import org.broad.igv.data.seg.SegmentedDataWriter;
import org.broad.igv.feature.GFFParser;
import org.broad.igv.feature.Genome;
import org.broad.igv.feature.GenomeManager;
import org.broad.igv.sam.reader.AlignmentIndexer;
import org.broad.igv.session.ViewContext;
import org.broad.igv.tools.sort.Sorter;
import org.broad.igv.track.WindowFunction;
import org.broad.igv.track.tribble.CodecFactory;
import org.broad.igv.util.FileUtils;
import org.broad.igv.util.ParsingUtils;
import org.broad.tribble.FeatureCodec;
import org.broad.tribble.index.IndexCreator;
import org.broad.tribble.index.interval.IntervalIndexCreator;
import org.broad.tribble.index.linear.LinearIndex;
import org.broad.tribble.index.linear.LinearIndexCreator;

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

/**
 * Command line parser for "igvtools".
 *
 * @author jrobinso
 */
public class IgvTools {

    static int MAX_RECORDS_IN_RAM = 500000;
    static String version = "@VERSION";

    static String[] commandDocs = new String[]{
            "version print the version number",
            "sort    sort an alignment file by start position",
            "index   index an alignment file",
            "tile    convert an cn input file (cn, gct, wig) to tiled data format",
            "count   compute coverage density for an alignment file"
    };
    private static final int MAX_ZOOM = 7;
    private static final int WINDOW_SIZE = 25;
    private static final int EXT_FACTOR = 0;
    private static final Object PROBE_FILE = null;
    private static final int LINEAR_BIN_SIZE = 16000;
    private static final int INTERVAL_SIZE = 1000;
    private static final int LINEAR_INDEX = 1;
    private static final int INTERVAL_INDEX = 2;

    /**
     * The general usage string
     */
    static String usageString() {
        StringBuffer buf = new StringBuffer();

        buf.append("\nProgram: igvtools\n\n");
        buf.append("Usage: igvtools [command] [options] [arguments]\n\n");
        buf.append("Command:");
        for (String c : commandDocs) {
            buf.append(" " + c + "\n\t");

        }
        return buf.toString();
    }


    public static void main(String[] argv) throws IOException, PreprocessingException {

        Globals.setHeadless(true);

        CmdLineParser parser = new CmdLineParser();
        CmdLineParser.Option windowFunctions = parser.addStringOption('f', "windowFunctions");
        CmdLineParser.Option tmpDirOption = parser.addStringOption('t', "tmpDir");
        CmdLineParser.Option probeFileOption = parser.addStringOption('p', "probeFile");
        CmdLineParser.Option maxZoomOption = parser.addIntegerOption('z', "maxZoom");
        CmdLineParser.Option compressOption = parser.addBooleanOption('c', "compressed");
        CmdLineParser.Option maxRecordsOption = parser.addStringOption('m', "maxRecords");

        // options for coverage counter
        CmdLineParser.Option windowSizeOption = parser.addIntegerOption('w', "windowSize");
        CmdLineParser.Option extFactorOption = parser.addIntegerOption('e', "extFactor");

        // options for indexing
        CmdLineParser.Option indexTypeOption = parser.addIntegerOption('i', "indexType");
        CmdLineParser.Option binSizeOption = parser.addIntegerOption('b', "binSize");

        // options for gentest
        CmdLineParser.Option nRows = parser.addIntegerOption('r', "numRows");
        CmdLineParser.Option strandOption = parser.addIntegerOption('s', "strand");
        CmdLineParser.Option unOrdered = parser.addBooleanOption('u', "unOrdered");

        if (argv.length < 1) {
            System.out.println("Args: " + argv.length);
            System.out.println(usageString());
            System.exit(-1);
        }

        // Parse optional arguments (switches, etc)
        try {
            parser.parse(argv);
        } catch (CmdLineParser.OptionException e) {
            System.err.println(e.getMessage());
            return;
        }
        int maxZoomValue = (Integer) parser.getOptionValue(maxZoomOption, MAX_ZOOM);
        int windowSizeValue = (Integer) parser.getOptionValue(windowSizeOption, WINDOW_SIZE);
        int extFactorValue = (Integer) parser.getOptionValue(extFactorOption, EXT_FACTOR);
        String wfsString = (String) parser.getOptionValue(windowFunctions);
        String probeFile = (String) parser.getOptionValue(probeFileOption, PROBE_FILE);
        String tmpDirName = (String) parser.getOptionValue(tmpDirOption);
        int maxRecords = (Integer) parser.getOptionValue(maxRecordsOption, MAX_RECORDS_IN_RAM);
        int strandOptionValue = (Integer) parser.getOptionValue(strandOption, -1);

        int indexType = (Integer) parser.getOptionValue(indexTypeOption, LINEAR_INDEX);
        int defaultBinSize = indexType == LINEAR_INDEX ? LINEAR_BIN_SIZE : INTERVAL_SIZE;
        int binSize = (Integer) parser.getOptionValue(binSizeOption, defaultBinSize);

        int nRowsValue = (Integer) parser.getOptionValue(nRows, 10);
        boolean sorted = !((Boolean) parser.getOptionValue(unOrdered, false));

        String[] nonOptionArgs = parser.getRemainingArgs();

        try {
            validateArgsLength(nonOptionArgs, 1);

            // The command
            String command = nonOptionArgs[EXT_FACTOR].toLowerCase();

            // Do "version" now, its the only command with no arguments
            if (command.equals("version")) {
                System.out.println("Version " + version);
                return;
            }

            // All remaining commands require an input file, and most need the file extension.  Do that here.
            validateArgsLength(nonOptionArgs, 2);
            String ifile = nonOptionArgs[1];
            if (!(new File(ifile)).exists()) {
                throw new PreprocessingException("File not found: " + ifile);
            }

            if (command.equals("count") || command.equals("pre") || command.equals("tile")) {
                validateArgsLength(nonOptionArgs, 4);
                String ofile = nonOptionArgs[2];
                // Output files must have .tdf extension
                //if (!ofile.endsWith(".tdf")) {
                //    ofile = ofile + ".tdf";
                //}
                String genomeId = nonOptionArgs[3];

                if (command.equals("count")) {
                    doCount(ifile, ofile, genomeId, maxZoomValue, wfsString, windowSizeValue, extFactorValue,
                            strandOptionValue);

                } else {
                    doTile(ifile, ofile, probeFile, genomeId, maxZoomValue, wfsString);
                }
            } else if (command.toLowerCase().equals("sort")) {
                validateArgsLength(nonOptionArgs, 3);
                String ofile = nonOptionArgs[2];
                doSort(ifile, ofile, tmpDirName, maxRecords);
            } else if (command.equals("index")) {
                doIndex(ifile, indexType, binSize);
            } else if (command.equals("seg")) {
                doPreprocessSeg(argv);
            } else if (command.equals("wibtowig")) {
                validateArgsLength(nonOptionArgs, 4);
                File txtFile = new File(nonOptionArgs[1]);
                File wibFile = new File(nonOptionArgs[2]);
                File wigFile = new File(nonOptionArgs[3]);
                String trackLine = nonOptionArgs.length > 4 ? nonOptionArgs[4] : null;
                doWIBtoWIG(txtFile, wibFile, wigFile, trackLine);
            } else if (command.equals("splitgff")) {
                validateArgsLength(nonOptionArgs, 3);
                String outputDirectory = nonOptionArgs[2];
                GFFParser.splitFileByType(ifile, outputDirectory);
            } else if (command.equals("gentest")) {
                validateArgsLength(nonOptionArgs, 2);
                String ofile = ifile;
                TestFileGenerator.generateTestFile(ofile, sorted, nRowsValue, strandOptionValue);
            } else {
                throw new PreprocessingException("Unknown command: " + argv[EXT_FACTOR]);
            }
        } catch (PreprocessingException e) {
            System.err.println(e.getMessage());
        } catch (IOException e) {
            throw new PreprocessingException("Unexpected IO error: ", e);
        }
    }

    public static void doTile(String ifile, String ofile, String probeFile, String genomeId, int maxZoomValue,
                              String wfsString) throws IOException, PreprocessingException {
        validateIsTilable(ifile);
        Genome genome = loadGenome(genomeId);
        if (genome == null) {
            throw new PreprocessingException("Genome could not be loaded: " + genomeId);
        }
        File tmp = new File(ifile);

        // TODO -- do a better size estimate for directories
        int nLines = tmp.isDirectory() ? 310000000 : ParsingUtils.estimateLineCount(ifile);

        boolean isGCT = Preprocessor.getExtension(ifile).endsWith("gct");

        Collection<WindowFunction> windowFunctions = parseWFS(wfsString, isGCT);

        Preprocessor p = new Preprocessor(new File(ofile), genome, windowFunctions, nLines, null);
        if (tmp.isDirectory()) {
            for (File f : tmp.listFiles()) {
                p.preprocess(f, probeFile, maxZoomValue);
            }
        } else {
            p.preprocess(tmp, probeFile, maxZoomValue);
        }
        p.finish();

    }

    public static void doCount(String ifile, String ofile, String genomeId, int maxZoomValue, String wfsString,
                               int windowSizeValue, int extFactorValue, int strandOption) throws IOException {
        Genome genome = loadGenome(genomeId);
        if (genome == null) {
            throw new PreprocessingException("Genome could not be loaded: " + genomeId);
        }
        int nLines = ParsingUtils.estimateLineCount(ifile);

        // Multiple files allowed for count command (a tdf and a wig)
        File tdfFile = null;
        File wigFile = null;
        String[] files = ofile.split(",");
        if (files[EXT_FACTOR].endsWith("wig")) {
            wigFile = new File(files[EXT_FACTOR]);
        } else {
            tdfFile = new File(files[EXT_FACTOR]);
        }
        if (files.length > 1) {
            if (files[1].endsWith("wig")) {
                wigFile = new File(files[1]);
            } else if (files[1].endsWith("tdf")) {
                tdfFile = new File(files[1]);
            }
        }

        boolean isGCT = Preprocessor.getExtension(ifile).endsWith("gct");
        Collection<WindowFunction> windowFunctions = parseWFS(wfsString, isGCT);

        Preprocessor p = new Preprocessor(tdfFile, genome, windowFunctions, nLines, null);
        p.count(ifile, windowSizeValue, extFactorValue, maxZoomValue, wigFile, strandOption);
        p.finish();
    }


    public static void doWIBtoWIG(File txtFile, File wibFile, File wigFile, String trackLine) {
        UCSCUtils.convertWIBFile(txtFile, wibFile, wigFile, trackLine);
    }


    public static void doPreprocessSeg(String[] argv) {
        int nArgs = argv.length - 1;
        String[] args = new String[nArgs];
        System.arraycopy(argv, 1, args, EXT_FACTOR, nArgs);
        SegmentedDataWriter.main(args);
    }


    /**
     * Create an index for an alignment or feature file
     *
     * @param ifile
     * @throws IOException
     */
    public static void doIndex(String ifile, int indexType, int binSize) throws IOException {
        if (ifile.endsWith(".gz")) {
            System.out.println("Cannot index a gzipped file");
            System.exit(-1);
        }

        File inputFile = new File(ifile);
        if (ifile.endsWith(".bed") || ifile.endsWith(".vcf") || ifile.endsWith(".gff") ||
                ifile.endsWith(".gff3")) {
            FeatureCodec codec = CodecFactory.getCodec(ifile);

            IndexCreator indexer = null;
            if (indexType == LINEAR_INDEX) {
                LinearIndexCreator c = new LinearIndexCreator(inputFile, codec);
                c.setBinWidth(binSize);
                indexer = c;
            } else {
                IntervalIndexCreator c  = new IntervalIndexCreator(inputFile, codec);
                c.setFeaturesPerInterval(binSize);
                indexer = c;
            }

            indexer.createIndex();

        } else {
            AlignmentIndexer indexer = AlignmentIndexer.getInstance(inputFile, null, null);
            indexer.createSamIndex();
        }

    }


    public static void doSort(String ifile, String ofile, String tmpDirName, int maxRecords) {

        File inputFile = new File(ifile);
        File outputFile = new File(ofile);
        Sorter sorter = Sorter.getSorter(inputFile, outputFile);

        if (tmpDirName != null) {
            File tmpDir = new File(tmpDirName);
            if (!tmpDir.exists()) {
                System.err.println("Error: tmp directory: " + tmpDir.getAbsolutePath() + " does not exist.");
                System.exit(-1);
            }
            sorter.setTmpDir(tmpDir);
        }

        sorter.setMaxRecords(maxRecords);
        sorter.run();
    }


    private static void validateArgsLength(String[] nonOptionArgs, int len) throws PreprocessingException {
        if (nonOptionArgs.length < len) {
            throw new PreprocessingException(usageString());
        }
    }


    private static Genome loadGenome(String genomeFileOrID) {

        String rootDir = FileUtils.getInstallDirectory();

        File genomeFile = new File(rootDir, "genomes" + File.separator + genomeFileOrID + ".genome");
        if (!genomeFile.exists()) {
            genomeFile = new File(rootDir, "genomes" + File.separator + genomeFileOrID);
        }
        if (!genomeFile.exists()) {
            genomeFile = new File(genomeFileOrID);
        }
        if (!genomeFile.exists()) {
            throw new PreprocessingException("Genome definition file not found for: " + genomeFileOrID);
        }

        GenomeManager.GenomeListItem item = GenomeManager.getInstance().loadGenomeFromLocalFile(genomeFile);
        String genomeId = item.getId();
        if (genomeId == null) {
            throw new PreprocessingException("Error loading: " + genomeFileOrID);
        }
        ViewContext.getInstance().setGenomeId(genomeId);
        Genome genome = GenomeManager.getInstance().getGenome(genomeId);
        return genome;
    }


    /**
     * Test if the file type can be "tiled".
     */
    private static void validateIsTilable(String ifile) {
        File tmp = new File(ifile);
        if (tmp.isDirectory()) {
            File[] files = tmp.listFiles();
            if (files.length == EXT_FACTOR) {
                throw new PreprocessingException("Tile command not supported for empty directory: " + ifile);
            }
            ifile = files[EXT_FACTOR].getName();
        }
        String ext = Preprocessor.getExtension(ifile);
        if (!(ext.equals(".cn") ||
                ext.equals(".igv") ||
                ext.equals(".wig") ||
                ext.equals(".ewig") ||
                ext.equals(".cn") ||
                ext.equals(".snp") ||
                ext.equals(".xcn") ||
                ext.equals(".gct") ||
                ext.equals(".bedgraph") ||
                Preprocessor.isAlignmentFile(ext))) {
            throw new PreprocessingException("Tile command not supported for files of type: " + ext);
        }
    }


    /**
     * Parse the window functions line.
     *
     * @param string comma delimited string of window functions, e.g. min, p10, max
     * @return colleciton of WindowFunctions objects
     */
    private static Collection<WindowFunction> parseWFS(String string, boolean isGCT) {
        if (string == null || string.length() == EXT_FACTOR) {
            return isGCT ? Arrays.asList(WindowFunction.min, WindowFunction.mean, WindowFunction.max) :
                    Arrays.asList(WindowFunction.mean);
        } else {
            String[] tokens = string.split(",");
            List<WindowFunction> funcs = new ArrayList(tokens.length);
            for (int i = EXT_FACTOR; i < tokens.length; i++) {
                String wf = tokens[i];
                if (wf.startsWith("p")) {
                    wf = wf.replaceFirst("p", "percentile");
                }
                try {
                    funcs.add(WindowFunction.valueOf(wf));
                } catch (Exception e) {
                    System.err.println("Unrecognized window function: " + tokens[i]);
                }
            }
            return funcs;
        }
    }


}

