/*
 * Copyright (c) 2007-2011 by The Broad Institute of MIT and Harvard.  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
 * WARRANTES 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.
 */
/*
 * GenomeManager.java
 *
 * Created on November 9, 2007, 9:12 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package org.broad.igv.feature.genome;

import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.Chromosome;
import org.broad.igv.feature.CytoBandFileParser;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.UIConstants;

import org.broad.igv.ui.util.ConfirmDialog;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.ui.util.ProgressMonitor;
import org.broad.igv.util.FileUtils;
import org.broad.igv.util.IGVHttpClientUtils;
import org.broad.igv.util.Utilities;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.zip.*;

/**
 * @author jrobinso
 */
public class GenomeManager {

    /**
     * Field description
     */
    public static final String SEQUENCE_FILE_EXTENSION = ".txt";
    /**
     * The refresh frequence in seconds.  Cached genomes will be refreshed
     * from the server if they are older than this value.
     */

    private static Logger log = Logger.getLogger(GenomeManager.class);
    private Map<String, GenomeDescriptor> genomeDescriptorMap;
    //private Map<String, Genome> genomes;

    final public static String USER_DEFINED_GENOME_LIST_FILE = "user-defined-genomes.txt";
    private static GenomeDescriptor DEFAULT_GENOME;
    //private String genomeId;
    public Genome currentGenome;

    private List<GenomeListItem> userDefinedGenomeArchiveList;
    private List<GenomeListItem> cachedGenomeArchiveList;
    private List<GenomeListItem> serverGenomeArchiveList;

    IGV igv;

    /**
     * Creates a new instance of GenomeManager
     */
    public GenomeManager(IGV igv) {
        genomeDescriptorMap = new HashMap();
        this.igv = igv;
    }

    public GenomeManager() {
        genomeDescriptorMap = new HashMap();
        this.igv = null;
    }

    /**
     * Return the genome identified by the id (e.g. mm8, hg17, etc).
     *
     * @param id Genome id.
     * @return
     */
    public Genome getGenome(String id) {

        if (currentGenome == null || !currentGenome.getId().equals(id)) {
            GenomeDescriptor genomeDescriptor = genomeDescriptorMap.get(id);
            if (genomeDescriptor == null) {
                return null;
            } else {
                loadGenome(genomeDescriptor);
            }
        }
        return currentGenome;
    }

    private Genome loadGenome(GenomeDescriptor genomeDescriptor) {
        InputStream is = null;
        try {

            InputStream inputStream = genomeDescriptor.getCytoBandStream();
            if (inputStream == null) {
                return null;
            }

            BufferedReader reader;
            if (genomeDescriptor.isCytoBandFileGZipFormat()) {
                is = new GZIPInputStream(inputStream);
                reader = new BufferedReader(new InputStreamReader(is));
            } else {
                is = new BufferedInputStream(inputStream);
                reader = new BufferedReader(new InputStreamReader(is));
            }

            currentGenome = new Genome(genomeDescriptor);
            LinkedHashMap<String, Chromosome> chromMap = CytoBandFileParser.loadData(reader);
            currentGenome.setChromosomeMap(chromMap, genomeDescriptor.isChromosomesAreOrdered());

            InputStream aliasStream = genomeDescriptor.getChrAliasStream();
            if (aliasStream != null) {
                try {
                    reader = new BufferedReader(new InputStreamReader(aliasStream));
                    currentGenome.loadChrAliases(reader);
                } catch (Exception e) {
                    // We don't want to bomb if the alias load fails.  Just log it and proceed.
                    log.error("Error loading chromosome alias table");
                }
            }

            // Do this last so that user defined aliases have preference.  TODO This order dependence is rather fragile.
            currentGenome.loadUserDefinedAliases();

            return currentGenome;

        } catch (IOException ex) {
            log.warn("Error loading the genome!", ex);
            throw new RuntimeException("Error loading genome: " + genomeDescriptor.getName());
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException ex) {
                log.warn("Error closing zip stream!", ex);
            }
        }
    }


    public GenomeListItem loadGenomeFromLocalFile(File zipFile) throws IOException {

        GenomeListItem genomeListItem = null;

        boolean isUserDefined = false;
        File parentFolder = zipFile.getParentFile();
        if (parentFolder == null || !parentFolder.equals(Globals.getGenomeCacheDirectory())) {
            isUserDefined = true;
        }

        // user imported genomes are not versioned
        GenomeDescriptor genomeDescriptor = createGenomeDescriptor(zipFile, isUserDefined);
        loadGenome(genomeDescriptor);
        genomeDescriptorMap.put(genomeDescriptor.getId(), genomeDescriptor);
        genomeListItem = new GenomeListItem(genomeDescriptor.getName(),
                genomeDescriptor.getSequenceLocation(), genomeDescriptor.getId(),
                genomeDescriptor.getVersion(),
                isUserDefined);


        return genomeListItem;
    }


    /**
     * Locates a genome in the set of known genome archive locations  then loads it.   Used by unit tests.
     *
     * @param genome The genome to load.
     */
    public void loadGenomeByID(String genome) throws IOException {

        LinkedHashSet<GenomeListItem> genomes = getAllGenomeArchives();
        for (GenomeListItem item : genomes) {
            if (item.getId().equalsIgnoreCase(genome)) {
                String url = item.getLocation();
                if (item.isUserDefined) {
                    loadUserDefinedGenome(url, null);
                } else {
                    loadSystemGenome(url, null);
                }

            }
        }
    }


    /**
     * Copies a system IGV genome archive into IGV's local genome cache and
     * then request it be loaded. If the genome is user-define it is loaded
     * directly from it's own location.
     *
     * @param genomeArchiveFileLocation
     * @param monitor                   ProgressMonitor
     * @return GenomeListItem.
     * @throws FileNotFoundException
     * @see GenomeListItem
     */
    public GenomeListItem loadSystemGenome(
            String genomeArchiveFileLocation,
            ProgressMonitor monitor)
            throws IOException {

        GenomeListItem genomeListItem = null;

        if (genomeArchiveFileLocation == null) {
            loadGenome(getDefaultGenomeDescriptor());
            return getDefaultGenomeListItem();
        }

        if (!genomeArchiveFileLocation.trim().endsWith(Globals.GENOME_FILE_EXTENSION)) {
            throw new RuntimeException(
                    "The extension of archive [" + genomeArchiveFileLocation + "] is not an IGV genome archive extension");
        }

        try {

            File archiveFile;


            if (!Globals.getGenomeCacheDirectory().exists()) {
                Globals.getGenomeCacheDirectory().mkdir();
            }

            if (IGVHttpClientUtils.isURL(genomeArchiveFileLocation.toLowerCase())) {

                URL genomeArchiveURL = new URL(genomeArchiveFileLocation);
                String fileName = Utilities.getFileNameFromURL(
                        URLDecoder.decode(new URL(genomeArchiveFileLocation).getFile(), "UTF-8"));

                archiveFile = new File(Globals.getGenomeCacheDirectory(), fileName);

                refreshCache(archiveFile, genomeArchiveURL);

            } else {

                archiveFile = new File(genomeArchiveFileLocation);
            }


            if (!archiveFile.exists()) {
                throw new FileNotFoundException(genomeArchiveFileLocation);
            }


            // If archive file exists load  it into IGV

            if (monitor != null) {
                monitor.fireProgressChange(25);
            }

            loadGenomeFromLocalFile(archiveFile);

            if (monitor != null) {
                monitor.fireProgressChange(25);
            }

        } catch (SocketException e) {
            throw new GenomeServerException("Server connection error", e);
        }

        if (log.isDebugEnabled()) {
            log.debug("Exit loadGenome");
        }

        return genomeListItem;
    }

    public GenomeListItem loadUserDefinedGenome(
            String genomeArchiveFileLocation,
            ProgressMonitor monitor)
            throws IOException {

       if (genomeArchiveFileLocation == null) {
            loadGenome(getDefaultGenomeDescriptor());
            return getDefaultGenomeListItem();
        }

        try {
            // New genome archive file
            File archiveFile = new File(genomeArchiveFileLocation);

            if (!archiveFile.exists()) {
                throw new FileNotFoundException(genomeArchiveFileLocation);
            }

            File userDefinedListFile = new File(Globals.getGenomeCacheDirectory(), USER_DEFINED_GENOME_LIST_FILE);
            Properties listProperties = retrieveUserDefinedGenomeListFromFile(userDefinedListFile);

            if (listProperties == null) {
                listProperties = new Properties();
            }

            String record = buildClientSideGenomeListRecord(archiveFile, true);

            GenomeListItem genomeListItem = null;

             if (record != null) {
                int version = 0;
                String[] fields = record.split("\t");
                listProperties.setProperty(fields[2], record);
                GenomeImporter.storeUserDefinedGenomeListToFile(userDefinedListFile, listProperties);
                genomeListItem = new GenomeListItem(fields[0], fields[1], fields[2], version, true);
            }

           // If archive file exists load  it into IGV
            if (monitor != null) monitor.fireProgressChange(25);
            loadGenomeFromLocalFile(archiveFile);
            if (monitor != null) monitor.fireProgressChange(25);

            return genomeListItem;

        } catch (SocketException e) {
            throw new GenomeServerException("Server connection error", e);
        }
    }

    /**
     * Refresh a locally cached genome
     *
     * @param archiveFile
     * @param genomeArchiveURL
     * @throws IOException
     */
    private void refreshCache(File archiveFile, URL genomeArchiveURL) {
        // Look in cache first


        try {
            if (archiveFile.exists()) {


                long fileLength = archiveFile.length();
                long contentLength = IGVHttpClientUtils.getContentLength(genomeArchiveURL);

                if (contentLength <= 0) {
                    log.info("Skipping genome update of " + archiveFile.getName() + " due to unknown content length");
                }
                // Force an update of cached genome if file length does not equal remote content length
                boolean forceUpdate = (contentLength != fileLength) &&
                        PreferenceManager.getInstance().getAsBoolean(PreferenceManager.AUTO_UPDATE_GENOMES);
                if (forceUpdate) {
                    log.info("Refreshing genome: " + genomeArchiveURL.toString());
                    File tmpFile = new File(archiveFile.getAbsolutePath() + ".tmp");
                    if (IGVHttpClientUtils.downloadFile(genomeArchiveURL.toExternalForm(), tmpFile)) {
                        FileUtils.copyFile(tmpFile, archiveFile);
                        tmpFile.deleteOnExit();
                    }
                }

            } else {
                // Copy file directly from the server to local cache.
                IGVHttpClientUtils.downloadFile(genomeArchiveURL.toExternalForm(), archiveFile);
            }
        } catch (Exception e) {
            log.error("Error refreshing genome cache. ", e);
            MessageUtils.showMessage(("An error was encountered refreshing the genome cache: " + e.getMessage() +
                    "<br> If this problem persists please contact igv-help@broadinstitute.org"));
        }

    }


    /**
     * Creates a genome descriptor.
     */
    private GenomeDescriptor createGenomeDescriptor(File f, boolean userDefined)
            throws IOException {


        String zipFilePath = f.getAbsolutePath();

        if (!f.exists()) {
            log.error("Genome file: " + f.getAbsolutePath() + " does not exist.");
            return null;
        }


        GenomeDescriptor genomeDescriptor = null;
        Map<String, ZipEntry> zipEntries = new HashMap();
        ZipFile zipFile = new ZipFile(zipFilePath);


        ZipInputStream zipInputStream = null;
        try {
            zipInputStream = new ZipInputStream(new FileInputStream(f));
            ZipEntry zipEntry = zipInputStream.getNextEntry();

            while (zipEntry != null) {
                String zipEntryName = zipEntry.getName();
                zipEntries.put(zipEntryName, zipEntry);

                if (zipEntryName.equalsIgnoreCase(Globals.GENOME_ARCHIVE_PROPERTY_FILE_NAME)) {
                    InputStream inputStream = zipFile.getInputStream(zipEntry);
                    Properties properties = new Properties();
                    properties.load(inputStream);

                    // Cytoband
                    String cytobandZipEntryName = properties.getProperty(Globals.GENOME_ARCHIVE_CYTOBAND_FILE_KEY);

                    // RefFlat
                    String geneFileName = properties.getProperty(Globals.GENOME_ARCHIVE_GENE_FILE_KEY);

                    String chrAliasFileName = properties.getProperty(Globals.GENOME_CHR_ALIAS_FILE_KEY);

                    String sequenceLocation = properties.getProperty(Globals.GENOME_ARCHIVE_SEQUENCE_FILE_LOCATION_KEY);

                    if ((sequenceLocation != null) && !IGVHttpClientUtils.isURL(sequenceLocation)) {
                        File sequenceFolder = null;
                        // Relative or absolute location?
                        if (sequenceLocation.startsWith("/") || sequenceLocation.startsWith("\\")) {
                            sequenceFolder = new File(sequenceLocation);
                        } else {
                            File tempZipFile = new File(zipFilePath);
                            sequenceFolder = new File(tempZipFile.getParent(), sequenceLocation);

                        }
                        sequenceLocation = sequenceFolder.getCanonicalPath();
                        sequenceLocation.replace('\\', '/');
                    }


                    int version = 0;
                    String versionString = properties.getProperty(Globals.GENOME_ARCHIVE_VERSION_KEY);
                    if (versionString != null) {
                        try {
                            version = Integer.parseInt(versionString);
                        } catch (Exception e) {
                            log.error("Error parsing version string: " + versionString);
                        }
                    }

                    boolean chrNamesAltered = false;
                    String chrNamesAlteredString = properties.getProperty("filenamesAltered");
                    if (chrNamesAlteredString != null) {
                        try {
                            chrNamesAltered = Boolean.parseBoolean(chrNamesAlteredString);
                        } catch (Exception e) {
                            log.error("Error parsing version string: " + versionString);
                        }
                    }

                    boolean chromosomesAreOrdered = false;
                    String tmp = properties.getProperty(Globals.GENOME_ORDERED_KEY);
                    if (tmp != null) {
                        try {
                            chromosomesAreOrdered = Boolean.parseBoolean(tmp);
                        } catch (Exception e) {
                            log.error("Error parsing ordered string: " + tmp);
                        }
                    }

                    String url = properties.getProperty(Globals.GENOME_URL_KEY);


                    // The new descriptor
                    genomeDescriptor = new GenomeZipDescriptor(
                            properties.getProperty(Globals.GENOME_ARCHIVE_NAME_KEY),
                            version,
                            chrNamesAltered,
                            properties.getProperty(Globals.GENOME_ARCHIVE_ID_KEY),
                            cytobandZipEntryName,
                            geneFileName,
                            chrAliasFileName,
                            properties.getProperty(Globals.GENOME_GENETRACK_NAME, "Gene"),
                            sequenceLocation,
                            zipFile,
                            zipEntries,
                            userDefined,
                            chromosomesAreOrdered);

                    if (url != null) {
                        genomeDescriptor.setUrl(url);
                    }

                }
                zipEntry = zipInputStream.getNextEntry();
            }
        } finally {
            try {
                if (zipInputStream != null) {
                    zipInputStream.close();
                }
            } catch (IOException ex) {
                log.warn("Error closing imported genome zip stream!", ex);
            }
        }
        return genomeDescriptor;
    }


    /**
     * Gets the descriptor for a specific genome.
     *
     * @param id
     * @return GenomeDescriptor
     */
    public GenomeDescriptor getGenomeDescriptor(String id) {
        if (genomeDescriptorMap.containsKey(id)) {
            return genomeDescriptorMap.get(id);
        } else {
            return getDefaultGenomeDescriptor();
        }
    }


    boolean serverGenomeListUnreachable = false;

    /**
     * Gets a list of all the server genome archive files that
     * IGV knows about.
     *
     * @param excludedArchivesUrls The set of file location to exclude in the return list.
     * @return LinkedHashSet<GenomeListItem>
     * @throws IOException
     * @see GenomeListItem
     */
    public List<GenomeListItem> getServerGenomeArchiveList(Set excludedArchivesUrls)
            throws IOException {

        if (serverGenomeListUnreachable) {
            return null;
        }

        if (serverGenomeArchiveList == null) {
            serverGenomeArchiveList = new LinkedList();
            BufferedReader dataReader = null;
            InputStream inputStream = null;
            String genomeListURLString = "";
            try {
                genomeListURLString = PreferenceManager.getInstance().getGenomeListURL();
                URL serverGenomeURL = new URL(genomeListURLString);

                if (IGVHttpClientUtils.isURL(genomeListURLString)) {
                    inputStream = IGVHttpClientUtils.openConnectionStream(serverGenomeURL);
                } else {
                    File file = new File(genomeListURLString.startsWith("file:") ? serverGenomeURL.getFile() : genomeListURLString);
                    inputStream = new FileInputStream(file);
                }


                dataReader = new BufferedReader(new InputStreamReader(inputStream));

                String genomeRecord;
                while ((genomeRecord = dataReader.readLine()) != null) {

                    if (genomeRecord.startsWith("<") || genomeRecord.startsWith("(#")) {
                        continue;
                    }

                    if (genomeRecord != null) {
                        genomeRecord = genomeRecord.trim();

                        String[] fields = genomeRecord.split("\t");

                        if ((fields != null) && (fields.length >= 3)) {

                            // Throw away records we don't want to see
                            if (excludedArchivesUrls != null) {
                                if (excludedArchivesUrls.contains(fields[1])) {
                                    continue;
                                }
                            }
                            int version = 0;
                            if (fields.length > 3) {
                                try {
                                    version = Integer.parseInt(fields[3]);
                                } catch (Exception e) {
                                    log.error("Error parsing genome version: " + fields[0], e);
                                }
                            }

                            //String displayableName, String url, String id, int version, boolean isUserDefined
                            String name = fields[0];
                            String url = fields[1];
                            String id = fields[2];

                            boolean valid = true;
                            if (url.length() == 0) {
                                log.error("Genome entry : " + name + " has an empty URL string.  Check for extra tabs in the definition file: " +
                                        PreferenceManager.getInstance().getGenomeListURL());
                                valid = false;
                            }
                            // TODO -- more validation


                            if (valid) {


                                try {
                                    GenomeListItem item = new GenomeListItem(fields[0], fields[1], fields[2], version, false);
                                    serverGenomeArchiveList.add(item);
                                } catch (Exception e) {
                                    log.error(
                                            "Error reading a line from server genome list" + " line was: [" + genomeRecord + "]",
                                            e);
                                }
                            }
                        } else {
                            log.error("Found invalid server genome list record: " + genomeRecord);
                        }
                    }
                }
            } catch (Exception e) {
                serverGenomeListUnreachable = true;
                log.error("Error fetching genome list: ", e);
                ConfirmDialog.optionallyShowInfoDialog("Warning: could not connect to the genome server (" +
                        genomeListURLString + ").    Only locally defined genomes will be available.",
                        PreferenceManager.SHOW_GENOME_SERVER_WARNING);
            } finally {
                if (dataReader != null) {
                    dataReader.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            }
        }
        return serverGenomeArchiveList;
    }

    /**
     * Gets a list of all the user-defined genome archive files that
     * IGV knows about.
     *
     * @return LinkedHashSet<GenomeListItem>
     * @throws IOException
     * @see GenomeListItem
     */
    public List<GenomeListItem> getUserDefinedGenomeArchiveList()
            throws IOException {


        if (userDefinedGenomeArchiveList == null) {

            boolean updateClientGenomeListFile = false;

            userDefinedGenomeArchiveList = new LinkedList();

            File listFile = new File(Globals.getGenomeCacheDirectory(), USER_DEFINED_GENOME_LIST_FILE);

            Properties listProperties = retrieveUserDefinedGenomeListFromFile(listFile);

            if (listProperties != null) {

                Collection records = listProperties.values();
                for (Object value : records) {

                    String record = (String) value;
                    if (record.trim().equals("")) {
                        continue;
                    }

                    String[] fields = record.split("\t");
                    File file = new File(fields[1]);
                    if (file.isDirectory() || !file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) {
                        continue;
                    }
                    if (!file.exists()) {
                        updateClientGenomeListFile = true;
                        continue;
                    }

                    GenomeListItem item = new GenomeListItem(fields[0], file.getAbsolutePath(), fields[2], 0, true);
                    userDefinedGenomeArchiveList.add(item);
                }
            }
            if (updateClientGenomeListFile) {
                updateImportedGenomePropertyFile();
            }
        }
        return userDefinedGenomeArchiveList;
    }

    /**
     * Method description
     */
    public void clearGenomeCache() {

        File[] files = Globals.getGenomeCacheDirectory().listFiles();
        for (File file : files) {
            if (file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) {
                file.delete();
            }
        }

    }

    /**
     * Gets a list of all the locally cached genome archive files that
     * IGV knows about.
     *
     * @return LinkedHashSet<GenomeListItem>
     * @throws IOException
     * @see GenomeListItem
     */
    public List<GenomeListItem> getCachedGenomeArchiveList()
            throws IOException {

        if (cachedGenomeArchiveList == null) {
            cachedGenomeArchiveList = new LinkedList();

            if (!Globals.getGenomeCacheDirectory().exists()) {
                return cachedGenomeArchiveList;
            }

            File[] files = Globals.getGenomeCacheDirectory().listFiles();
            for (File file : files) {

                if (file.isDirectory()) {
                    continue;
                }

                if (!file.getName().toLowerCase().endsWith(Globals.GENOME_FILE_EXTENSION)) {
                    continue;
                }

                URL zipUrl = file.toURI().toURL();


                ZipFile zipFile = null;
                FileInputStream fis = null;
                ZipInputStream zipInputStream = null;
                try {

                    zipFile = new ZipFile(file);
                    fis = new FileInputStream(file);
                    zipInputStream = new ZipInputStream(new BufferedInputStream(fis));

                    ZipEntry zipEntry = zipFile.getEntry(Globals.GENOME_ARCHIVE_PROPERTY_FILE_NAME);
                    if (zipEntry == null) {
                        continue;    // Should never happen
                    }

                    InputStream inputStream = zipFile.getInputStream(zipEntry);
                    Properties properties = new Properties();
                    properties.load(inputStream);

                    int version = 0;
                    if (properties.containsKey(Globals.GENOME_ARCHIVE_VERSION_KEY)) {
                        try {
                            version = Integer.parseInt(
                                    properties.getProperty(Globals.GENOME_ARCHIVE_VERSION_KEY));
                        } catch (Exception e) {
                            log.error("Error parsing genome version: " + version, e);
                        }
                    }

                    GenomeListItem item =
                            new GenomeListItem(properties.getProperty(Globals.GENOME_ARCHIVE_NAME_KEY),
                                    file.getAbsolutePath(),
                                    properties.getProperty(Globals.GENOME_ARCHIVE_ID_KEY),
                                    version,
                                    false);
                    cachedGenomeArchiveList.add(item);
                } catch (ZipException ex) {
                    log.error("\nZip error unzipping cached genome.", ex);
                    try {
                        file.delete();
                        zipInputStream.close();
                    } catch (Exception e) {
                        //ignore exception when trying to delete file
                    }
                } catch (IOException ex) {
                    log.warn("\nIO error unzipping cached genome.", ex);
                    try {
                        file.delete();
                    } catch (Exception e) {
                        //ignore exception when trying to delete file
                    }
                } finally {
                    try {
                        if (zipInputStream != null) {
                            zipInputStream.close();
                        }
                        if (zipFile != null) {
                            zipFile.close();
                        }
                        if (fis != null) {
                            fis.close();
                        }
                    } catch (IOException ex) {
                        log.warn("Error closing genome zip stream!", ex);
                    }
                }
            }
        }

        return cachedGenomeArchiveList;
    }

    /**
     * Gets a list of all the server and client-side genome archive files that
     * IGV knows about as a list of GenomeListItems.
     *
     * @return LinkedHashSet<GenomeListItem>
     * @throws IOException
     * @see GenomeListItem
     */
    public LinkedHashSet<GenomeListItem> getAllGenomeArchives() throws IOException {

        LinkedHashSet<GenomeListItem> genomeListItems = new LinkedHashSet();

        final List<GenomeListItem> userDefinedItemList = getUserDefinedGenomeArchiveList();
        if (userDefinedItemList != null) {
            genomeListItems.addAll(userDefinedItemList);
        }

        // Build a single available genome list from both client, server
        // and cached information. This allows us to process
        // everything the same way.
        List<GenomeListItem> serverSideItemList = null;
        try {
            serverSideItemList = getServerGenomeArchiveList(null);
        } catch (UnknownHostException e) {
            log.error(UIConstants.CANNOT_ACCESS_SERVER_GENOME_LIST, e);
        } catch (SocketException e) {
            log.error(UIConstants.CANNOT_ACCESS_SERVER_GENOME_LIST, e);
        }

        // If the server is unreaachable load cached genomes.
        if (serverSideItemList == null || serverSideItemList.isEmpty()) {
            serverSideItemList = getCachedGenomeArchiveList();
        }
        if (serverSideItemList != null) {
            genomeListItems.addAll(serverSideItemList);
        }

        if (genomeListItems.isEmpty()) {
            GenomeDescriptor defaultDes = getDefaultGenomeDescriptor();
            GenomeListItem defaultItem = new GenomeListItem(defaultDes.getName(), null, defaultDes.getId(), 0, false);
            genomeListItems.add(defaultItem);
            genomeDescriptorMap.put(defaultDes.getId(), defaultDes);
        }

        return genomeListItems;
    }

    /**
     * Reconstructs the user-define genome property file.
     *
     * @throws IOException
     */
    public void updateImportedGenomePropertyFile()
            throws IOException {

        if ((userDefinedGenomeArchiveList == null)) {
            return;
        }

        File listFile = new File(Globals.getGenomeCacheDirectory(), USER_DEFINED_GENOME_LIST_FILE);

        if (!listFile.exists()) {
            listFile.createNewFile();
        }

        StringBuffer buffer = new StringBuffer();
        Properties listProperties = new Properties();
        for (GenomeListItem genomeListItem : userDefinedGenomeArchiveList) {

            buffer.append(genomeListItem.getDisplayableName());
            buffer.append("\t");
            buffer.append(genomeListItem.getLocation());
            buffer.append("\t");
            buffer.append(genomeListItem.getId());

            listProperties.setProperty(genomeListItem.getId(), buffer.toString());
            buffer.delete(0, buffer.length());
        }
        GenomeImporter.storeUserDefinedGenomeListToFile(listFile, listProperties);
    }

    /**
     * Create a genonem list record (same format as used by the server) for
     * genome files.
     *
     * @param genomeArchive The genome file from which to extract a record.
     * @param userDefined   true if archive is a user-defined genome archive.
     * @return A tab delimetered genome list record containing
     *         ( name[tab]genome location[tab]genomeId ).
     * @throws IOException
     */
    public String buildClientSideGenomeListRecord(File genomeArchive, boolean userDefined)
            throws IOException {

        GenomeDescriptor genomeDescriptor = createGenomeDescriptor(genomeArchive, userDefined);

        StringBuffer buffer = new StringBuffer();
        buffer.append(genomeDescriptor.getName());
        buffer.append("\t");
        buffer.append(genomeArchive.getAbsoluteFile());
        buffer.append("\t");
        buffer.append(genomeDescriptor.getId());
        return buffer.toString();
    }

    /**
     * Read the user-defined genome property file to find enough information to
     * display the genome in IGV.
     *
     * @param file A java properties file containing tab delimetered data
     *             (display name [tab] genome file location [tab] genome id) about
     *             the user-defined genome.
     * @return A java Properties object contain the file's content.
     */
    public static Properties retrieveUserDefinedGenomeListFromFile(File file) {

        Properties properties = new Properties();

        if ((file != null) && file.exists()) {
            FileInputStream input = null;
            try {
                input = new FileInputStream(file);
                properties.load(input);
            } catch (FileNotFoundException e) {
                log.error("Property file for user-defined genomes was not " + "found!", e);
            } catch (IOException e) {
                log.error("Error readin property file for user-defined " + "genomes!", e);
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    } catch (IOException e) {
                        log.error("Error closing property file for " + "user-defined genomes!",
                                e);
                    }
                }
            }
        }
        return properties;
    }

    /**
     * Create an IGV representation of a user-defined genome.
     *
     * @param genomeZipLocation              A File path to a directory in which the .genome
     *                                       output file will be written.
     * @param cytobandFileName               A File path to a file that contains cytoband data.
     * @param refFlatFileName                A File path to a gene file.
     * @param fastaFileName                  A File path to a FASTA file, a .gz file containing a
     *                                       single FASTA file, or a directory containing ONLY FASTA files.
     * @param relativeSequenceLocation       A relative path to the location
     *                                       (relative to the .genome file to be created) where the sequence data for
     *                                       the new genome will be written.
     * @param genomeDisplayName              The unique user-readable name of the new genome.
     * @param genomeId                       The id to be assigned to the genome.
     * @param genomeFileName                 The file name (not path) of the .genome archive
     *                                       file to be created.
     * @param monitor                        A ProgressMonitor used to track progress - null,
     *                                       if no progress updating is required.
     * @param sequenceOutputLocationOverride
     * @return GenomeListItem
     * @throws FileNotFoundException
     */
    public GenomeListItem defineGenome(String genomeZipLocation,
                                       String cytobandFileName,
                                       String refFlatFileName,
                                       String fastaFileName,
                                       String chrAliasFileName,
                                       String relativeSequenceLocation,
                                       String genomeDisplayName,
                                       String genomeId,
                                       String genomeFileName,
                                       ProgressMonitor monitor,
                                       String sequenceOutputLocationOverride)
            throws IOException {

        File archiveFile;
        File zipFileLocation = null;
        File fastaInputFile = null;
        File refFlatFile = null;
        File cytobandFile = null;
        File chrAliasFile = null;
        File sequenceLocation;

        if ((genomeZipLocation != null) && (genomeZipLocation.trim().length() != 0)) {
            zipFileLocation = new File(genomeZipLocation);
            PreferenceManager.getInstance().setLastGenomeImportDirectory(zipFileLocation);
        }

        if ((cytobandFileName != null) && (cytobandFileName.trim().length() != 0)) {
            cytobandFile = new File(cytobandFileName);
        }

        if ((refFlatFileName != null) && (refFlatFileName.trim().length() != 0)) {
            refFlatFile = new File(refFlatFileName);
        }

        if ((chrAliasFileName != null) && (chrAliasFileName.trim().length() != 0)) {
            chrAliasFile = new File(chrAliasFileName);
        }

        if ((fastaFileName != null) && (fastaFileName.trim().length() != 0)) {
            fastaInputFile = new File(fastaFileName);

            // The sequence info only matters if we have FASTA
            if ((relativeSequenceLocation != null) && (relativeSequenceLocation.trim().length() != 0)) {
                sequenceLocation = new File(genomeZipLocation, relativeSequenceLocation);
                if (!sequenceLocation.exists()) {
                    sequenceLocation.mkdir();
                }
            }
        }

        if (monitor != null) monitor.fireProgressChange(25);

        archiveFile = (new GenomeImporter()).createGenomeArchive(zipFileLocation,
                genomeFileName, genomeId, genomeDisplayName, relativeSequenceLocation,
                fastaInputFile, refFlatFile, cytobandFile, chrAliasFile,
                sequenceOutputLocationOverride, monitor);

        if (monitor != null) monitor.fireProgressChange(75);

        GenomeListItem newItem = loadUserDefinedGenome(archiveFile.getAbsolutePath(), null);
        userDefinedGenomeArchiveList.add(0, newItem);
        updateImportedGenomePropertyFile();
        return newItem;
    }


    public GenomeDescriptor getDefaultGenomeDescriptor() {
        if (DEFAULT_GENOME == null) {
            DEFAULT_GENOME = new GenomeResourceDescriptor("Human hg18", 0, "hg18",
                    "/resources/hg18_cytoBand.txt", null, null, null,
                    "http://www.broadinstitute.org/igv/sequence/hg18", false);
        }
        return DEFAULT_GENOME;
    }

    public GenomeListItem getDefaultGenomeListItem() {
        GenomeDescriptor desc = getDefaultGenomeDescriptor();
        return new GenomeListItem(desc.getName(), null, desc.getId(), 0, false);
    }


    /**
     * A container for specific genome information which can be used to
     * manage loaded genomes.
     */
    public static class GenomeListItem {

        private String displayableName;
        private String location;
        private String id;
        private boolean isUserDefined = false;

        /**
         * Constructor.
         *
         * @param displayableName The name that can be shown to a user.
         * @param url             The url of the genome archive.
         * @param id              The id of the genome.
         * @param isUserDefined
         */
        public GenomeListItem(String displayableName, String url, String id, int version, boolean isUserDefined) {

            this.displayableName = displayableName;
            this.location = url;
            this.id = id;
            this.isUserDefined = isUserDefined;
        }

        public String getDisplayableName() {
            return displayableName;
        }


        public String getId() {
            return id;
        }


        public String getLocation() {
            return location;
        }


        public boolean isUserDefined() {
            return isUserDefined;
        }


        @Override
        public String toString() {
            return getDisplayableName();
        }

        /**
         * Method description
         *
         * @return
         */
        @Override
        public int hashCode() {

            int hash = 1;
            hash = hash * 31 + ((displayableName == null) ? 0 : displayableName.trim().hashCode());
            hash = hash * 13 + ((id == null) ? 0 : id.trim().hashCode());
            return hash;
        }

        /**
         * Equals method.  Two GenomeListItems are equal if their ids are equal
         *
         * @param object
         * @return
         */
        @Override
        public boolean equals(Object object) {

            if (!(object instanceof GenomeListItem)) {
                return false;
            }

            GenomeListItem item = (GenomeListItem) object;

            return getId().equals(item.getId());
        }

    }


    public String getGenomeId() {
        return currentGenome == null ? null : currentGenome.getId();
    }

    public Genome getCurrentGenome() {
        return currentGenome;
    }


}
