/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 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.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.broad.igv.batch;

import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.feature.Locus;
import org.broad.igv.feature.RegionOfInterest;
import org.broad.igv.feature.genome.GenomeManager;
import org.broad.igv.google.Ga4ghAPIHelper;
import org.broad.igv.google.OAuthUtils;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.renderer.DataRange;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.track.RegionScoreType;
import org.broad.igv.track.Track;
import org.broad.igv.ui.IGV;
import org.broad.igv.event.DataLoadedEvent;
import org.broad.igv.event.IGVEventBus;
import org.broad.igv.event.IGVEventObserver;
import org.broad.igv.ui.panel.FrameManager;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.ui.util.SnapshotUtilities;
import org.broad.igv.ui.util.UIUtilities;
import org.broad.igv.util.*;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class CommandExecutor {

    private static Logger log = Logger.getLogger(CommandExecutor.class);

    private File snapshotDirectory;
    private IGV igv;
    private int sleepInterval = 2000;


    public CommandExecutor() {
        igv = IGV.getInstance();
    }

    private List<String> getArgs(String[] tokens) {
        List<String> args = new ArrayList(tokens.length);
        for (String s : tokens) {
            if (s.trim().length() > 0) {
                args.add(s.trim());
            }
        }
        return args;
    }


    public String execute(String command) {

        List<String> args = getArgs(StringUtils.breakQuotedString(command, ' ').toArray(new String[]{}));

        String result = "OK";


        System.out.println();
        log.debug("Executing: " + command);
        try {
            if (args.size() == 0) {
                return "Empty command string";
            }


            String cmd = args.get(0).toLowerCase();
            String param1 = args.size() > 1 ? args.get(1) : null;
            String param2 = args.size() > 2 ? args.get(2) : null;
            String param3 = args.size() > 3 ? args.get(3) : null;
            String param4 = args.size() > 4 ? args.get(4) : null;


            if (cmd.equalsIgnoreCase("echo")) {
                result = cmd;
            } else if (cmd.equalsIgnoreCase("gotoimmediate")) {
                return gotoImmediate(args);
            } else if (cmd.equalsIgnoreCase("goto")) {
                result = goto1(args);
            } else if (cmd.equalsIgnoreCase("gototrack")) {
                boolean res = IGV.getInstance().scrollToTrack(param1);
                result = res ? "OK" : String.format("Error: Track %s not found", param1);
            } else if (cmd.equalsIgnoreCase("snapshotdirectory")) {
                result = setSnapshotDirectory(param1);
            } else if (cmd.equalsIgnoreCase("snapshot")) {
                String filename = param1;
                result = createSnapshot(filename, param2);
            } else if ((cmd.equalsIgnoreCase("loadfile") || cmd.equalsIgnoreCase("load")) && param1 != null) {
                result = load(param1, param2, param3, param4);
            } else if (cmd.equalsIgnoreCase("genome") && args.size() > 1) {
                result = genome(param1);
            } else if (cmd.equalsIgnoreCase("new") || cmd.equalsIgnoreCase("reset") || cmd.equalsIgnoreCase("clear")) {
                igv.newSession();
            } else if (cmd.equalsIgnoreCase("region")) {
                defineRegion(param1, param2, param3, param4);
            } else if (cmd.equalsIgnoreCase("sort")) {
                sort(param1, param2, param3, param4);
            } else if (cmd.equalsIgnoreCase("group")) {
                group(param1, param2);
            } else if (cmd.equalsIgnoreCase("collapse")) {
                String trackName = parseTrackName(param1);
                igv.setTrackDisplayMode(Track.DisplayMode.COLLAPSED, trackName);
            } else if (cmd.equalsIgnoreCase("expand")) {
                String trackName = parseTrackName(param1);
                igv.setTrackDisplayMode(Track.DisplayMode.EXPANDED, trackName);
            } else if (cmd.equalsIgnoreCase("squish")) {
                String trackName = parseTrackName(param1);
                igv.setTrackDisplayMode(Track.DisplayMode.SQUISHED, trackName);
            } else if (cmd.equalsIgnoreCase("remove")) {
                String trackName = parseTrackName(param1);
                result = removeTrack(trackName);
            } else if (cmd.equalsIgnoreCase("tweakdivider")) {
                igv.tweakPanelDivider();
            } else if (cmd.equalsIgnoreCase("setDataRange")) {
                result = this.setDataRange(param1, param2);
            } else if (cmd.equalsIgnoreCase("setLogScale")) {
                result = this.setLogScale(param1, param2);
            } else if (cmd.equalsIgnoreCase("maxpanelheight") && param1 != null) {
                return setMaxPanelHeight(param1);
            } else if (cmd.equalsIgnoreCase("tofront")) {
                return UIUtilities.bringToFront();
            } else if (cmd.equalsIgnoreCase("viewaspairs")) {
                return setViewAsPairs(param1, param2);
            } else if (cmd.equalsIgnoreCase("samplingwindowsize")) {
                return this.setSamplingWindowSize(param1);
            } else if (cmd.equalsIgnoreCase("maxdepth") || (cmd.equalsIgnoreCase("samplingreadcount"))) {
                return this.setSamplingReadCount(param1);
            } else if (cmd.equalsIgnoreCase("setSleepInterval")) {
                return this.setSleepInterval(param1);
            } else if (cmd.equalsIgnoreCase("setCredentials")) {
                return this.setCredentials(param1, param2);
            } else if (cmd.equalsIgnoreCase("clearCredentials")) {
                return this.clearCredentials();
            } else if (cmd.equals("preference")) {
                return this.overridePreference(param1, param2);
            } else if (cmd.equalsIgnoreCase("version")) {
                return Globals.VERSION;
            } else if (cmd.equals("exit")) {
                System.exit(0);
            } else if (cmd.equals("zoomin")) {
                FrameManager.incrementZoom(1);
            } else if (cmd.equals("zoomout")) {
                FrameManager.incrementZoom(-1);
            } else if ("oauth".equals(cmd)) {
                try {
                    OAuthUtils.getInstance().setAccessToken(param1);
                } catch (IOException e1) {
                    log.error(e1);
                    return e1.getMessage();
                }
            } else {
                result = "UNKOWN COMMAND: " + command;
                log.error(result);
                return result;
            }

            igv.doRefresh();

            if (RuntimeUtils.getAvailableMemoryFraction() < 0.5) {
                log.debug("Running garbage collection");
                System.gc();
            }
            log.debug("Finished execution: " + command + "  sleeping ....");
            if (sleepInterval > 0) try {
                Thread.sleep(sleepInterval);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("Finished sleeping");

        } catch (
                IOException e
                )

        {
            log.error(e);
            result = "Error: " + e.getMessage();
        }

        log.info(result);

        return result;
    }

    private String removeTrack(String trackName) {
        if (trackName == null) return "Error: NULL TRACK NAME";
        for (Track track : igv.getAllTracks()) {
            if (track.getName().equals(trackName)) {
                igv.removeTracks(Arrays.asList(track));
                return "OK";
            }
        }
        return String.format("Error: Track %s not found", trackName);
    }

    static String parseTrackName(String param1) {
        return param1 == null ? null : StringUtils.stripQuotes(param1);
    }

    private String overridePreference(String prefKey, String prefVal) {
        PreferencesManager.getPreferences().override(prefKey, prefVal);
        return "OK";
    }

    private String setDataRange(String dataRangeString, String trackName) {
        List<Track> tracks = igv.getAllTracks();
        String[] tokens = dataRangeString.split(",");
        //Min,max or min,baseline,max
        DataRange range;
        try {
            if (tokens.length == 2) {
                range = new DataRange(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]));
            } else if (tokens.length == 3) {
                range = new DataRange(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]));
            } else {
                throw new IllegalArgumentException(String.format("ERROR: parsing %s for data range. \n" +
                        "String must be of form <min,max> or <min,baseline,max>", dataRangeString));
            }
        } catch (NumberFormatException e) {
            return "ERROR: Could not parse input string as a Float. " + e.getMessage();
        } catch (IllegalArgumentException e) {
            return e.getMessage();
        }
        for (Track track : tracks) {
            if (trackName == null || trackName.equalsIgnoreCase(track.getName())) {
                track.setDataRange(range);
                track.setAutoScale(false);
            }
        }
        return "OK";
    }

    private String setLogScale(String logScaleString, String trackName) {
        List<Track> tracks = igv.getAllTracks();
        boolean logScale;
        try {
            if(logScaleString.equalsIgnoreCase("true") || logScaleString.equalsIgnoreCase("false")){
                logScale = Boolean.valueOf(logScaleString);
            }else{
                return "ERROR: logscale value (" + logScaleString + ")is not 'true' or 'false'.";
            }
        } catch (IllegalArgumentException e) {
            return e.getMessage();
        }
        DataRange.Type scaleType = logScale==true ?
                DataRange.Type.LOG :
                DataRange.Type.LINEAR;
        for (Track track : tracks) {
            if (trackName == null || trackName.equalsIgnoreCase(track.getName())) {
                    track.getDataRange().setType(scaleType);
            }
        }
        IGV.getInstance().revalidateTrackPanels();
        return "OK";
    }

    private String setViewAsPairs(String vAPString, String trackName) {
        List<Track> tracks = igv.getAllTracks();
        boolean vAP = "false".equalsIgnoreCase(vAPString) ? false : true;
        for (Track track : tracks) {
            if (track instanceof AlignmentTrack) {
                if (trackName == null || trackName.equalsIgnoreCase(track.getName())) {
                    AlignmentTrack atrack = (AlignmentTrack) track;
                    atrack.setViewAsPairs(vAP);
                }
            }
        }
        return "OK";
    }

    private String setSamplingWindowSize(String windowSize) {
        try {
            Integer.parseInt(windowSize);
            PreferencesManager.getPreferences().override(Constants.SAM_SAMPLING_WINDOW, String.valueOf(windowSize));
            return "OK";
        } catch (NumberFormatException e) {
            return "ERROR: SAMPLING WINDOW IS NOT A NUMBER: " + windowSize;
        }

    }

    private String setSamplingReadCount(String samplingReadCount) {
        try {
            Integer.parseInt(samplingReadCount);
            PreferencesManager.getPreferences().override(Constants.SAM_SAMPLING_COUNT, String.valueOf(samplingReadCount));
            return "OK";
        } catch (NumberFormatException e) {
            return "ERROR: SAMPLING READ COUNT IS NOT A NUMBER: " + samplingReadCount;
        }

    }

    private String gotoImmediate(List<String> args) {
        return goto1(args);
    }

    private String setMaxPanelHeight(String param1) {
        try {
            Integer h = Integer.parseInt(param1.trim());
            SnapshotUtilities.setMaxPanelHeight(h);
            return "OK";
        } catch (NumberFormatException e) {
            return "ERROR - max panel height value ('" + param1 + ".) must be an integer number";
        }
    }

    public String setSleepInterval(String param1) {
        try {
            sleepInterval = Integer.parseInt(param1.trim());
            return "OK";
        } catch (NumberFormatException e) {
            return "ERROR - sleep interval value ('" + param1 + ".) must be an integer number";
        }
    }

    private String setCredentials(String param1, String param2) {
        HttpUtils.getInstance().setDefaultUserName(param1);
        HttpUtils.getInstance().setDefaultPassword(param2);
        return "OK";
    }

    private String clearCredentials() {
        HttpUtils.getInstance().clearDefaultCredentials();
        return "OK";
    }


    private String genome(String param1) {
        if (param1 == null) {
            return "ERROR missing genome parameter";
        }
        String result = "OK";
        String genomeID = param1;

        igv.selectGenomeFromList(genomeID);
        if (GenomeManager.getInstance().getCurrentGenome().getId().equals(genomeID)) {
            return result;
        }


        String genomePath = genomeID;
        if (!ParsingUtils.fileExists(genomePath)) {
            String workingDirectory = System.getProperty("user.dir", "");
            genomePath = FileUtils.getAbsolutePath(genomeID, workingDirectory);
        }
        if (ParsingUtils.fileExists(genomePath)) {
            try {
                GenomeManager.getInstance().loadGenome(genomePath, null);
            } catch (IOException e) {
                result = "ERROR: Could not load genome: " + genomeID;
                MessageUtils.showMessage(result);
            }

        } else {
            result = "ERROR: Could not locate genome: " + genomeID;
            MessageUtils.showMessage(result);
        }


        return result;
    }

    /**
     * Load function for port and batch script
     *
     * @param fileString path to file
     * @param param2
     * @param param3
     * @return
     * @throws IOException
     */
    private String load(String fileString, String param2, String param3, String param4) throws IOException {

        // Default for merge is "true" for session files,  "false" otherwise
        String tmpFile = StringUtils.stripQuotes(fileString);
        boolean merge = !(tmpFile.endsWith(".xml") || tmpFile.endsWith(".php") || tmpFile.endsWith(".php3"));

        // remaining parameters might be "merge", "name", or "index"
        String name = null;
        String index = null;
        String coverage = null;
        String format = null;
        for (String param : Arrays.asList(param2, param3)) {
            if (param != null && param.startsWith("name=")) {
                name = param.substring(5);
            } else if (param != null && param.startsWith("merge=")) {
                String mergeString = param.substring(6);
                merge = mergeString.equalsIgnoreCase("true");
            } else if (param != null && param.startsWith("index=")) {
                index = param.substring(6);
            } else if (param != null && param.startsWith("coverage=")) {
                coverage = param.substring(9);
            } else if (param != null && param.startsWith("format=")) {
                format = param.substring(7);
            }
        }
        // Locus is not specified from port commands
        String locus = null;
        Map<String, String> params = null;
        return loadFiles(fileString, index, coverage, name, format, locus, merge, params);
    }

    String loadFiles(final String fileString,
                     final String indexString,
                     final String coverageString,
                     final String nameString,
                     final String formatString,
                     final String locus,
                     final boolean merge,
                     Map<String, String> params) throws IOException {
        return loadFiles(fileString, indexString, coverageString, nameString, formatString, locus, merge, params, null, null);
    }

    /**
     * Load files -- used by port, batch, and http commands
     *
     * @param fileString
     * @param locus
     * @param merge
     * @param nameString
     * @param params
     * @param sort
     * @param sortTag    Used iff sort == SortOption.TAG
     * @return
     * @throws IOException
     */
    String loadFiles(final String fileString,
                     final String indexString,
                     final String coverageString,
                     final String nameString,
                     final String formatString,
                     final String locus,
                     final boolean merge,
                     Map<String, String> params,
                     String sort,
                     String sortTag) throws IOException {


        log.debug("Run load files");


        List<String> files = StringUtils.breakQuotedString(fileString, ',');
        List<String> names = StringUtils.breakQuotedString(nameString, ',');
        List<String> indexFiles = StringUtils.breakQuotedString(indexString, ',');
        List<String> coverageFiles = StringUtils.breakQuotedString(coverageString, ',');
        List<String> formats = StringUtils.breakQuotedString(formatString, ',');

        if (files.size() == 1) {
            // String might be URL encoded
            files = StringUtils.breakQuotedString(fileString.replaceAll("%2C", ","), ',');
            names = nameString != null ? StringUtils.breakQuotedString(nameString.replaceAll("%2C", ","), ',') : null;
            indexFiles = indexString != null ? StringUtils.breakQuotedString(indexString.replaceAll("%2C", ","), ',') : null;
            coverageFiles = coverageString != null ? StringUtils.breakQuotedString(coverageString.replaceAll("%2C", ","), ',') : null;
            formats = formatString != null ? StringUtils.breakQuotedString(formatString.replaceAll("%2C", ","), ',') : null;
        }

        if (names != null && names.size() != files.size()) {
            return "Error: If file is a comma-separated list, names must also be a comma-separated list of the same length";
        }

        if (indexFiles != null && indexFiles.size() != files.size()) {
            return "Error: If file is a comma-separated list, index must also be a comma-separated list of the same length";
        }


        // Must decode URLs (local or remote), but leave local file paths only
        for (int ii = 0; ii < files.size(); ii++) {
            files.set(ii, decodeFileString(files.get(ii).replace("\"", "")));
            if (names != null) {
                names.set(ii, names.get(ii).replace("\"", ""));
            }
            if (indexFiles != null) {
                indexFiles.set(ii, decodeFileString(indexFiles.get(ii).replace("\"", "")));
            }
            if (coverageFiles != null) {
                coverageFiles.set(ii, decodeFileString(coverageFiles.get(ii).replace("\"", "")));
            }
            if (formatString != null) {
                formats.set(ii, decodeFileString(formats.get(ii).replace("\"", "")));
            }
        }

        List<ResourceLocator> fileLocators = new ArrayList<ResourceLocator>();
        List<String> sessionPaths = new ArrayList<String>();

        if (!merge) {
            // If this is a session file start fresh without asking, otherwise ask
            boolean unload = !merge;
            if (fileString.endsWith(".xml") || fileString.endsWith(".php") || fileString.endsWith(".php3")) {
                unload = !merge;
            } else {
                unload = MessageUtils.confirm("Unload current session before loading new tracks?");
            }
            if (unload) {
                igv.newSession();
            }
        }

        // Create set of loaded files
        Set<String> loadedFiles = new HashSet<String>();
        for (ResourceLocator rl : igv.getDataResourceLocators()) {
            loadedFiles.add(rl.getPath());
        }

        // Loop through files

        for (int fi = 0; fi < files.size(); fi++) {

            String f = files.get(fi);

            // Skip already loaded files TODO -- make this optional?  Check for change?
            if (loadedFiles.contains(f)) continue;

            if (f.endsWith(".xml") || f.endsWith(".php") || f.endsWith(".php3") || f.endsWith(".session")) {
                sessionPaths.add(f);
            } else {
                ResourceLocator rl = new ResourceLocator(f);

                if (names != null) {
                    rl.setName(names.get(fi));
                }
                if (indexFiles != null) {
                    rl.setIndexPath(indexFiles.get(fi));
                }
                if (coverageFiles != null) {
                    rl.setCoverage(coverageFiles.get(fi));
                }
                if (formats != null) {
                    String format = formats.get(fi);
                    if (!("ga4gh".equals(format)) && !format.startsWith(".")) format = "." + format;
                    rl.setType(format);
                }

                if ("ga4gh".equals(rl.getType())) {
                    // TODO -- distinguish reads and variants
                    rl.setAttribute("provider", Ga4ghAPIHelper.GA4GH_GOOGLE_PROVIDER);
                } else {
                    if (params != null) {
                        String trackLine = createTrackLine(params);
                        rl.setTrackLine(trackLine);
                    }

                    if (rl.isLocal()) {
                        File file = new File(rl.getPath());
                        if (!file.exists()) {
                            return "Error: " + f + " does not exist.";
                        }
                    }
                }
                fileLocators.add(rl);
            }
        }

        for (String sessionPath : sessionPaths) {
            igv.restoreSessionSynchronous(sessionPath, locus, merge);
        }

        final Future loadTask = igv.loadTracks(fileLocators);

        if (locus != null && !locus.equals("null")) {
            igv.goToLocus(locus);
            //If locus is a single base, we sort by base
            String[] tokens = locus.split(":", 2);
            if (tokens.length == 2) {
                String chr = tokens[0];
                try {
                    int pos = Integer.parseInt(tokens[1].replace(",", ""));
                    if (pos >= 0 && sort == null) sort = "base";

                } catch (Exception e) {
                    //pass
                }
            }

        }

        if (sort != null) {
            submitPerformSort(loadTask, sort, sortTag);
        }

        return CommandListener.OK;
    }

    private void submitPerformSort(final Future loadTask, final String sort, final String sortTag) {
        final AlignmentTrack.SortOption sortOption = getAlignmentSortOption(sort);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    //We need to wait until the track is loaded. If loadTask is null,
                    //it was loaded synchronously
                    if (loadTask != null) {
                        Object res = loadTask.get();
                    }
                    //Thought we were done waiting, huh? Guess again
                    //Alignment tracks load alignment data asynchronously from the track

                    //Since sorting applies to all tracks, we only need to have 1 handler
                    //TODO -- the use of the bus here is, essentially, an attempt to simulate a promise,  do().then()
                    IGVEventBus.getInstance().subscribe(DataLoadedEvent.class, new SortAlignmentsHandler(igv, sortOption, sortTag));

                } catch (InterruptedException e) {
                    log.error(e.getMessage(), e);
                } catch (ExecutionException e) {
                    log.error(e.getMessage(), e);
                }

            }
        };
        LongRunningTask.submit(runnable);
    }

    /**
     * If {@code fileString} is a URL and can be decoded,
     * return the decoded version. Otherwise return the original.
     *
     * @param fileString
     * @return
     */
    static String decodeFileString(String fileString) {
        if (needsDecode(fileString)) {
            return StringUtils.decodeURL(fileString);
        } else {
            return fileString;
        }
    }

    static boolean needsDecode(String fileString) {
        String decodedString = decodeSafe(fileString);
        return (decodedString != null && (HttpUtils.isURL(fileString) || HttpUtils.isURL(decodedString)));
    }

    private static String decodeSafe(String string) {
        String tmp = null;
        try {
            tmp = StringUtils.decodeURL(string);
        } catch (Exception e) {
            log.warn(e.getMessage());
        }
        return tmp;
    }

    /**
     * Convert the parameter map to a UCSC track line.
     *
     * @param params
     * @return
     */
    private String createTrackLine(Map<String, String> params) {
        return params.get("hgt.customText");
//        StringBuffer buf = new StringBuffer();
//        buf.append("track ");
//        for(Map.Entry<String, String> entry : params.entrySet()) {
//            buf.append(entry.getKey());
//            buf.append("=");
//            buf.append(entry.getValue());
//            buf.append(" ");
//        }
//        return buf.toString();
    }


    /**
     * Set a directory to deposit image snapshots
     *
     * @param param1
     * @return
     */
    String setSnapshotDirectory(String param1) {

        if (param1 == null) {
            return "ERROR: missing directory parameter";
        }

        param1 = StringUtils.stripQuotes(param1);

        File parentDir = null;
        try {
            parentDir = getFile(param1);
        } catch (URISyntaxException e) {
            log.error("Error parsing directory path: " + param1, e);
            return "Error parsing directory path: " + param1;
        }

        String result;
        if (parentDir.exists()) {
            snapshotDirectory = parentDir;
            result = "OK";
        } else {
            createParents(parentDir);
            parentDir.mkdir();
            if (parentDir.exists()) {
                snapshotDirectory = parentDir;
                result = "OK";
            } else {
                result = "ERROR: directory: " + param1 + " does not exist";
            }
        }
        return result;
    }

    private File getFile(String param1) throws URISyntaxException {

        // Strip trailing & leading quotes
        if (param1.startsWith("\"")) param1 = param1.substring(1);
        if (param1.endsWith("\"")) param1 = param1.substring(0, param1.lastIndexOf('"'));

        // See if file contains spaces, if not no special treatment is required
        if (param1.indexOf(' ') < 0) {
            return new File(param1);
        } else {
            // If file is absolute use a URI,
            File f = new File(param1);
            if (f.isAbsolute()) {
                URI outputURI = new URI(("file://" + param1.replaceAll(" ", "%20")));
                return new File(outputURI);
            } else {
                return f;
            }
        }
    }

    private String goto1(List<String> args) {
        if (args == null || args.size() < 2) {
            return "ERROR: missing locus parameter";
        }
        String locus = args.get(1);
        for (int i = 2; i < args.size(); i++) {
            locus += (" " + args.get(i));
        }
        igv.goToLocus(locus);
        return "OK";
    }


    private void defineRegion(String param1, String param2, String param3, String param4) {

        RegionOfInterest roi = null;
        if (param1 != null && param2 != null && param3 != null) {
            int start = Math.max(0, Integer.parseInt(param2) - 1);
            int end = Integer.parseInt(param3);
            String desc = param4 != null ? param4 : "";
            roi = new RegionOfInterest(param1, start, end, desc);
        }
        if (param1 != null) {
            Locus locus = Locus.fromString(param1);
            if (locus != null) {
                int start = Math.max(0, locus.getStart() - 1);
                String desc = param2 != null ? param2 : "";
                roi = new RegionOfInterest(locus.getChr(), start, locus.getEnd(), desc);

            }
        }
        if (roi != null) {
            igv.addRegionOfInterest(roi);
        }
    }


    private void sort(String sortArg, String locusString, String param3, String param4) {

        RegionScoreType regionSortOption = getRegionSortOption(sortArg);
        String tag = "";
        if (regionSortOption != null) {
            RegionOfInterest roi = null;
            if (locusString != null) {
                Locus locus = Locus.fromString(locusString);
                if (locus != null) {
                    int start = Math.max(0, locus.getStart() - 1);
                    roi = new RegionOfInterest(locus.getChr(), start, locus.getEnd(), "");
                }
            }
            igv.sortByRegionScore(roi, regionSortOption, FrameManager.getDefaultFrame());

        } else {
            Double location = null;
            if (param3 != null && param3.trim().length() > 0) {
                try {
                    location = new Double(param3.replace(",", ""));
                    tag = param4;
                } catch (NumberFormatException e) {
                    tag = param3;
                }
            } else if (locusString != null && locusString.trim().length() > 0) {
                try {
                    location = new Double(locusString.replace(",", ""));
                    tag = param4;
                } catch (NumberFormatException e) {
                    tag = param3;
                }
            }

            //Convert from 1-based to 0-based
            if (location != null) location--;

            igv.sortAlignmentTracks(getAlignmentSortOption(sortArg), location, tag);

        }
        igv.revalidateTrackPanels();
    }

    private void group(String groupArg, String tagArg) {
        igv.groupAlignmentTracks(getAlignmentGroupOption(groupArg), tagArg, null);
        UIUtilities.invokeAndWaitOnEventThread(() -> igv.revalidateTrackPanels());
    }


    private String createSnapshot(String filename, String region) {

        if (filename == null) {
            String locus = FrameManager.getDefaultFrame().getFormattedLocusString();
            filename = locus.replaceAll(":", "_").replace("-", "_") + ".png";
        } else {
            filename = StringUtils.stripQuotes(filename);
        }

        File file;
        if (snapshotDirectory == null) {
            try {
                file = getFile(filename);
                if (!file.getAbsoluteFile().getParentFile().exists()) {
                    createParents(file);
                }
            } catch (URISyntaxException e) {
                log.error("Error parsing directory path: " + filename, e);
                return "Error parsing directory path: " + filename;
            }
        } else {
            file = new File(snapshotDirectory, filename);
        }
        System.out.println("Snapshot: " + file.getAbsolutePath());


        Component target = null;
        if (region == null || region.trim().length() == 0) {
            target = IGV.getInstance().getContentPane().getMainPanel();
        } else if ("trackpanels".equalsIgnoreCase(region)) {
            target = IGV.getInstance().getMainPanel().getCenterSplitPane();
        }

        if (target == null) {
            String msg = "ERROR. Could not create snapshot. Unknown region: " + region;
            log.error(msg);
            return msg;
        }

        try {
            return IGV.getInstance().createSnapshotNonInteractive(target, file, true);
        } catch (IOException e) {
            log.error(e);
            return e.getMessage();
        }
    }


    private static void createParents(File outputFile) {
        File parent = outputFile.getParentFile();
        if (!parent.exists()) {
            parent.mkdirs();
        }

    }

    private static RegionScoreType getRegionSortOption(String str) {
        if (str == null) return null;
        String option = str.toUpperCase();
        try {
            return RegionScoreType.valueOf(option);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    private static AlignmentTrack.SortOption getAlignmentSortOption(String str) {
        str = str == null ? "base" : str;
        if (str.equalsIgnoreCase("start") || str.equalsIgnoreCase("position")) {
            return AlignmentTrack.SortOption.START;
        } else if (str.equalsIgnoreCase("strand")) {
            return AlignmentTrack.SortOption.STRAND;
        } else if (str.equalsIgnoreCase("base")) {
            return AlignmentTrack.SortOption.NUCLEOTIDE;
        } else if (str.equalsIgnoreCase("quality")) {
            return AlignmentTrack.SortOption.QUALITY;
        } else if (str.equalsIgnoreCase("sample")) {
            return AlignmentTrack.SortOption.SAMPLE;
        } else if (str.equalsIgnoreCase("readGroup") || str.equalsIgnoreCase("read_group")) {
            return AlignmentTrack.SortOption.READ_GROUP;
        } else if (str.equalsIgnoreCase("insertSize") || str.equalsIgnoreCase("insert_size")) {
            return AlignmentTrack.SortOption.INSERT_SIZE;
        } else if (str.equalsIgnoreCase("firstOfPairStrand")) {
            return AlignmentTrack.SortOption.FIRST_OF_PAIR_STRAND;
        } else if (str.equalsIgnoreCase("mateChr")) {
            return AlignmentTrack.SortOption.MATE_CHR;
        } else if (str.equalsIgnoreCase("readOrder")) {
            return AlignmentTrack.SortOption.READ_ORDER;
        }else {
            try {
                return AlignmentTrack.SortOption.valueOf(str.toUpperCase());
            } catch (IllegalArgumentException e) {
                log.error("Unknown sort option: " + str);
                return AlignmentTrack.SortOption.NUCLEOTIDE;
            }
        }

    }

    //      STRAND, SAMPLE, READ_GROUP, FIRST_OF_PAIR_STRAND, TAG, PAIR_ORIENTATION, MATE_CHROMOSOME, NONE, SUPPLEMENTARY

    private static AlignmentTrack.GroupOption getAlignmentGroupOption(String str) {
        if (str == null || str.length() == 0) {
            return AlignmentTrack.GroupOption.NONE;
        } else if (str.equalsIgnoreCase("strand")) {
            return AlignmentTrack.GroupOption.STRAND;
        } else if (str.equalsIgnoreCase("sample")) {
            return AlignmentTrack.GroupOption.SAMPLE;
        } else if (str.equalsIgnoreCase("library")) {
            return AlignmentTrack.GroupOption.LIBRARY;
        } else if (str.equalsIgnoreCase("readGroup") || str.equalsIgnoreCase("read_group")) {
            return AlignmentTrack.GroupOption.READ_GROUP;
        } else {
            try {
                return AlignmentTrack.GroupOption.valueOf(str.toUpperCase());
            } catch (IllegalArgumentException e) {
                log.error("Unknown group by option: " + str);
                return AlignmentTrack.GroupOption.NONE;
            }

        }

    }

    private static class SortAlignmentsHandler implements IGVEventObserver {

        private IGV igv = null;
        private AlignmentTrack.SortOption sortOption;
        private String sortTag;

        SortAlignmentsHandler(IGV igv, AlignmentTrack.SortOption sortOption, String sortTag) {
            this.igv = igv;
            this.sortOption = sortOption;
            this.sortTag = sortTag;

            IGVEventBus.getInstance().subscribe(DataLoadedEvent.class, this);
        }


        @Override
        public void receiveEvent(Object event) {
            boolean sorted = igv.sortAlignmentTracks(sortOption, sortTag);
            if (sorted) {
                IGVEventBus.getInstance().unsubscribe(this);
            }
        }
    }
}
