/*
 * 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.
 */

package org.broad.igv.data.expression;


import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.exceptions.LoadResourceFromServerException;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.HttpUtils;

import java.io.*;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.zip.GZIPInputStream;

/**
 * @author: Marc-Danie Nazaire
 * private static String affyMappingURL =
 * SERVER_URL + "/igv/resources/probes/affy_probe_gene_mapping.txt.gz";
 * private static String agilentMappingURL =
 * SERVER_URL + "/igv/resources/probes/agilent_probe_gene_mapping.txt.gz";
 * private static String illuminaMappingURL =
 * SERVER_URL + "/igv/resources/probes/illumina_probe_gene_mapping.txt.gz";
 */
public class ProbeToLocusMap {

    enum Platform {Affymetrix, Agilient, Illumina, Methylation, Mirna, unknown}

    public static final String SERVER_URL = "https://data.broadinstitute.org";

    private static Logger log = Logger.getLogger(ProbeToLocusMap.class);
    private static String affyGenesMappingURL =
            SERVER_URL + "/igvdata/probes/affy/affy_probe_gene_mapping.txt.gz";
    private static String affyHumanMappingURL =
            SERVER_URL + "/igvdata/probes/affy/affy_human_mappings.txt.gz";
    private static String affyMouseMappingURL =
            SERVER_URL + "/igvdata/probes/affy/affy_mouse_mappings.txt.gz";
    private static String affyOtherMappingURL =
            SERVER_URL + "/igvdata/probes/affy/affy_other_mappings.txt.gz";
    private static String agilentGenesMappingURL =
            SERVER_URL + "/igvdata/probes/agilent/agilent_probe_gene_mapping.txt.gz";
    private static String agilentHumanMappingURL =
            SERVER_URL + "/igvdata/probes/agilent/agilent_human_mappings.txt.gz";
    private static String agilentMouseMappingURL =
            SERVER_URL + "/igvdata/probes/agilent/agilent_mouse_mappings.txt.gz";
    private static String agilentOtherMappingURL =
            SERVER_URL + "/igvdata/probes/agilent/agilent_other_mappings.txt.gz";
    private static String illuminaMappingURL =
            SERVER_URL + "/igvdata/probes/illumina/illumina_allMappings.txt.gz";
    private static String illuminaGenesMappingURL =
            SERVER_URL + "/igvdata/probes/illumina/illumina_probe_gene_mapping.txt.gz";
    private static String methylationGeneMappingURL =
            SERVER_URL + "/igvdata/probes/meth/methylation_pobeToGene.tab.gz";
    private static String methylationLociMappingURL =
            SERVER_URL + "/igvdata/probes/meth/methylation_probeToLoci.mappings.txt.gz";

    private static ProbeToLocusMap instance;

    private Map<String, Map<String, String[]>> probeMaps = new HashMap();

    private MappingUrlCache mappingUrlCache = new MappingUrlCache();


    public static synchronized ProbeToLocusMap getInstance() {
        if (instance == null) {
            instance = new ProbeToLocusMap();
        }
        return instance;
    }

    public void clearProbeMappings() {
        if (probeMaps != null) {
            probeMaps.clear();
        }
        if (mappingUrlCache != null) {
            mappingUrlCache.clear();
        }
    }


    public void loadMapping(String urlString, Map<String, String[]> map) {
        BufferedReader bufReader = null;
        InputStream is = null;
        try {
            if (HttpUtils.isRemoteURL(urlString)) {
                URL url = HttpUtils.createURL(urlString);
                is = HttpUtils.getInstance().openConnectionStream(url);
            } else {
                is = new FileInputStream(urlString);
            }
            if (urlString.endsWith("gz")) {
                is = new GZIPInputStream(is);
            }
            bufReader = new BufferedReader(new InputStreamReader(is));
            loadMapping(bufReader, map);

        } catch (Exception e) {
            log.error("Error loading probe mapping", e);

            throw new LoadResourceFromServerException(e.getMessage(), urlString, e.getClass().getSimpleName());
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    log.error("Error closing probe mapping stream", e);
                }
            }
        }
    }

    public void loadMapping(BufferedReader bufReader, Map<String, String[]> map) throws IOException {
        String line;
        while ((line = bufReader.readLine()) != null) {
            String[] result = Globals.tabPattern.split(line, -1);
            int nTokens = result.length;
            if (nTokens != 2) {
                continue;
            }
            // check if more than one gene symbol found
            String[] genes = result[1].split("///");

            map.put(result[0].trim(), genes);
        }
    }

    /**
     * Return the URL to the mapping file for the give genomeId and platform.
     *
     * @param genomeId
     * @param platform
     * @return
     */
    public String getMappingURL(String genomeId, Platform platform) {

        if (platform == Platform.unknown) {
            return null;
        }

        String mappingUrl = mappingUrlCache.getMappingUrl(genomeId, platform);
        if (mappingUrl == null) {

            boolean mapToGenes = PreferencesManager.getPreferences().getAsBoolean(Constants.PROBE_MAPPING_KEY);
            if (!mapToGenes) {
                boolean hasLociMapping = checkForLociMapping(platform, genomeId);
                if (!hasLociMapping) {
                    MessageUtils.showMessage(
                            "<html>" + platform.toString() + " probe locations are not available for the selected genome " +
                                    " (" + genomeId + "). <br>Expression data will be mapped to gene locations.");
                    mapToGenes = true;
                }
            }

            if (platform == Platform.Affymetrix) {
                if (mapToGenes) {
                    mappingUrl = affyGenesMappingURL;
                } else if (genomeId.startsWith("hg")) {
                    mappingUrl = affyHumanMappingURL;

                } else if (genomeId.startsWith("mm")) {
                    mappingUrl = affyMouseMappingURL;

                } else {
                    mappingUrl = affyOtherMappingURL;
                }
            } else if (platform == Platform.Agilient) {
                if (mapToGenes) {
                    mappingUrl = agilentGenesMappingURL;
                } else if (genomeId.startsWith("hg")) {
                    mappingUrl = agilentHumanMappingURL;

                } else if (genomeId.startsWith("mm")) {
                    mappingUrl = agilentMouseMappingURL;
                } else {
                    mappingUrl = agilentOtherMappingURL;
                }
            } else if (platform == Platform.Illumina) {
                if (mapToGenes) {
                    mappingUrl = illuminaGenesMappingURL;
                } else {
                    mappingUrl = illuminaMappingURL;
                }
            } else if (platform == Platform.Methylation) {
                if (mapToGenes) {
                    mappingUrl = methylationGeneMappingURL;
                } else {
                    mappingUrl = methylationLociMappingURL;
                }
            } else {
                return null;
            }
            mappingUrlCache.put(genomeId, platform, mappingUrl);
        }
        return mappingUrl;

    }

    public static Platform getPlatform(String probeId) {
        if (probeId.endsWith("_at") || probeId.endsWith("_st")) {
            return Platform.Affymetrix;
        } else if (probeId.startsWith("A_")) {
            return Platform.Agilient;
        } else if (probeId.startsWith("ILMN_") || probeId.startsWith("GI_") || probeId.startsWith(
                "NM_") || probeId.startsWith("XM_")) {
            return Platform.Illumina;
        } else if (probeId.startsWith("cg")) {
            return Platform.Methylation;
        }
        return Platform.unknown;
    }

    //mm9 (Affymetrix), mm5 (Agilent) or mm8 (Illumina)

    private static boolean checkForLociMapping(Platform platform, String genomeId) {

        boolean hasLociMapping = true;
        if (genomeId.startsWith("hg") && !genomeId.equals("hg18")) {
            hasLociMapping = false;
        } else if (genomeId.startsWith("mm")) {
            switch (platform) {
                case Affymetrix:
                    if (!genomeId.equals("mm9")) {
                        hasLociMapping = false;
                    }
                    break;

                case Agilient:
                    if (!genomeId.equals("mm5")) {
                        hasLociMapping = false;
                    }
                    break;

                case Illumina:
                    if (!genomeId.equals("mm8")) {
                        hasLociMapping = false;
                    }
                    break;
            }
        }
        return hasLociMapping;
    }

    public String[] getLociForProbe(String probeId, String genomeId) {

        Platform platform = getPlatform(probeId);

        if (platform == Platform.unknown) {
            return null;
        }

        String mappingURL = getMappingURL(genomeId, platform);
        if (mappingURL == null) {
            return null;
        }
        Map<String, String[]> pMap = probeMaps.get(mappingURL);
        if (pMap == null) {
            pMap = new HashMap(500000);
            loadMapping(mappingURL, pMap);
            probeMaps.put(mappingURL, pMap);
        }

        if (log.isDebugEnabled() && pMap == null) {
            log.debug(("Null probeMap for:" + mappingURL));
        }

        return pMap == null ? null : (String[]) pMap.get(probeId);
    }


    // TODO -- this could be generalized to a Generic 2 key cache

    static class MappingUrlCache {

        Map<Platform, Map<String, String>> cache = new Hashtable();

        public void clear() {
            cache.clear();
        }

        public String getMappingUrl(String genomeId, Platform platform) {
            if (cache.containsKey(platform)) {
                Map<String, String> urlMap = cache.get(platform);
                return urlMap.get(genomeId);
            } else {
                return null;
            }
        }

        public void put(String genomeId, Platform platform, String mappingUrl) {
            if (!cache.containsKey(platform)) {
                cache.put(platform, new Hashtable());
            }
            cache.get(platform).put(genomeId, mappingUrl);
        }
    }

}
