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

//~--- non-JDK imports --------------------------------------------------------

import net.sf.samtools.util.BlockCompressedInputStream;
import org.apache.log4j.Logger;
import org.broad.igv.PreferenceManager;
import org.broad.igv.exceptions.DataLoadException;
import org.broad.igv.feature.Feature;
import org.broad.igv.feature.GeneManager;
import org.broad.igv.feature.Genome;
import org.broad.igv.feature.GenomeManager;
import org.broad.igv.renderer.BasicFeatureRenderer;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.sam.CoverageTrack;
import org.broad.igv.session.ViewContext;
import org.broad.igv.ui.IGVMainFrame;
import org.broad.igv.ui.MessageCollection;
import org.broad.igv.ui.RegionOfInterest;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.panel.DragEventManager;
import org.broad.igv.ui.panel.DragListener;
import org.broad.igv.ui.panel.TrackPanel;
import org.broad.igv.ui.panel.TrackPanelScrollPane;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.ResourceLocator;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.List;


/**
 * A helper class to manage tracks and groups.
 */

public class TrackManager {

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

    /**
     * Owning object
     */
    IGVMainFrame mainFrame;

    private TrackLoader loader;

    private final Map<String, TrackPanelScrollPane> trackPanelScrollPanes = new Hashtable();

    /**
     * Attribute used to group tracks.  Normally "null".  Set from the "Tracks" menu.
     */
    private String groupByAttribute = null;

    /**
     * The gene track for the current genome, rendered in the FeaturePanel
     */
    private Track geneTrack;

    private Map<String, List<Track>> overlayTracksMap = new HashMap();

    private Set<TrackType> loadedTypes = new HashSet();
    public static final String DATA_PANEL_NAME = "DataPanel";
    public static final String FEATURE_PANEL_NAME = "FeaturePanel";

    public Track getGeneTrack() {
        return geneTrack;
    }

    public Set<TrackType> getLoadedTypes() {
        return loadedTypes;
    }


    public TrackManager(IGVMainFrame mainFrame) {

        this.mainFrame = mainFrame;
        loader = new TrackLoader();
    }

    public void putScrollPane(String name, TrackPanelScrollPane sp) {
        trackPanelScrollPanes.put(name, sp);
    }

    public TrackPanelScrollPane getScrollPane(String name) {
        return trackPanelScrollPanes.get(name);
    }

    public Collection<TrackPanelScrollPane> getTrackPanelScrollPanes() {
        return trackPanelScrollPanes.values();
    }

    public void removeScrollPane(String name) {
        trackPanelScrollPanes.remove(name);
    }

    public void clearScrollPanes() {
        trackPanelScrollPanes.clear();
    }


    /**
     * Method description
     *
     * @param attributeName
     */
    public void setGroupByAttribute(String attributeName) {
        groupByAttribute = attributeName;
        groupTracksByAttribute();
    }

    public void reset() {
        groupByAttribute = "";
        TrackPanelScrollPane tsp = trackPanelScrollPanes.get(DATA_PANEL_NAME);
        if (tsp != null) {
            tsp.getTrackPanel().reset();
        }
    }


    public void groupTracksByAttribute() {
        //for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
        //    TrackPanel trackPanel = tsp.getTrackPanel();
        //    trackPanel.groupTracksByAttribute(groupByAttribute);
        //}
        TrackPanelScrollPane tsp = trackPanelScrollPanes.get(DATA_PANEL_NAME);
        if (tsp != null) {
            tsp.getTrackPanel().groupTracksByAttribute(groupByAttribute);
        }
    }


    /**
     * A (hopefully) temporary solution to force SAM track reloads,  until we have a better
     * dependency scheme
     */
    public void reloadSAMTracks() {
        for (Track t : getAllTracks(false)) {
            if (t instanceof AlignmentTrack) {
                ((AlignmentTrack) t).reloadData();
            }
        }
    }

    public void sortAlignmentTracks(AlignmentTrack.SortOption option) {
        for (Track t : getAllTracks(false)) {
            if (t instanceof AlignmentTrack) {
                ((AlignmentTrack) t).sortRows(option);
            }
        }
    }

    public void packAlignmentTracks() {
        for (Track t : getAllTracks(false)) {
            if (t instanceof AlignmentTrack) {
                ((AlignmentTrack) t).packAlignments();
            }
        }
    }

    public void collapseTracks() {
        for (Track t : getAllTracks(true)) {
            t.setExpanded(false);
        }
    }


    public void expandTracks() {
        for (Track t : getAllTracks(true)) {
            t.setExpanded(true);
        }
    }

    public void collapseTrack(String trackName) {
        for (Track t : getAllTracks(true)) {
            if (t.getName().equals(trackName)) {
                t.setExpanded(false);
            }
        }
    }


    public void expandTrack(String trackName) {
        for (Track t : getAllTracks(true)) {
            if (t.getName().equals(trackName)) {
                t.setExpanded(true);
            }
        }
    }


    public boolean loadResources(Collection<ResourceLocator> locators) {

        boolean tracksWereLoaded = false;

        log.info("Loading" + locators.size() + " resources.");
        MessageCollection messages = new MessageCollection();


        for (ResourceLocator locator : locators) {

            if (locator.isLocal()) {
                File trackSetFile = new File(locator.getPath());

                if (!trackSetFile.exists()) {
                    messages.append("File not found: " + locator.getPath() + "\n");
                    continue;
                }
            }
            TrackPanel group = getPanelFor(locator);
            try {
                List<Track> tracks = load(locator);
                group.addTracks(tracks);
                tracksWereLoaded = tracks.size() > 0;
            } catch (Exception e) {
                log.error("Error loading tracks", e);
                messages.append(e.getMessage());
            }
        }

        groupTracksByAttribute();
        resetOverlayTracks();

        //TODO Throw the message window higher up. There should be a throw when you are using the UI,
        // not when you are connecting through Socket

        if (!messages.isEmpty()) {
            for (String message : messages.getMessages()) {
                MessageUtils.showMessage(message);
            }
        }
        return tracksWereLoaded;
    }

    /**
     * Load a resource (track or sample attribute file)
     */

    public List<Track> load(ResourceLocator locator) {

        try {
            List<Track> newTracks = loader.load(locator);
            if (newTracks.size() > 0) {
                for (Track track : newTracks) {
                    track.setSourceFile(locator.getPath());
                    if (locator.getUrl() != null) {
                        track.setUrl(locator.getUrl());
                    }
                    track.setAttributeValue("NAME", track.getName());
                    track.setAttributeValue("DATA FILE", locator.getPath());
                    track.setAttributeValue("DATA TYPE", track.getTrackType().toString());


                }
            } 
            return newTracks;

        } catch (DataLoadException dle) {
            throw dle;
        } catch (Exception e) {
            log.error(e);
            throw new DataLoadException(e.getMessage(), locator.getPath());
        }

    }

    /**
     * @param type
     */
    public void addLoadedType(TrackType type) {
        loadedTypes.add(type);
    }

    /**
     * Load the data file into the specified panel.   Triggered via drag and drop.
     *
     * @param file
     * @param panel
     * @return
     */
    public void load(File file, TrackPanel panel) {
        ResourceLocator locator = new ResourceLocator(file.getAbsolutePath());
        List<Track> tracks = load(locator);
        panel.addTracks(tracks);
    }

    /**
     * Reset the overlay tracks collection.  Currently the only overlayable track
     * type is Mutation.  This method finds all mutation tracks and builds a map
     * of key -> mutatinon track,  where the key is the specified attribute value
     * for linking tracks for overlay.
     * <p/>
     * The method also resets all tracks overlay properties from the user
     * user preference value.  That should probably be done in another method.
     */
    public void resetOverlayTracks() {
        overlayTracksMap.clear();

        if (PreferenceManager.getInstance().getOverlayTracks()) {
            String overlayAttribute = IGVMainFrame.getInstance().getSession().getOverlayAttribute();
            if (overlayAttribute != null) {
                for (Track track : getAllTracks(false)) {
                    if (track != null) {
                        if (track.getTrackType() == UIConstants.overlayTrackType) {
                            String value = track.getAttributeValue(overlayAttribute);

                            if (value != null) {
                                List<Track> trackList = overlayTracksMap.get(value);

                                if (trackList == null) {
                                    trackList = new ArrayList();
                                    overlayTracksMap.put(value, trackList);
                                }

                                trackList.add(track);
                            }
                        }
                    }
                }
            }
        }

        boolean displayOverlays = IGVMainFrame.getInstance().getSession().getDisplayOverlayTracks();

        for (Track track : getAllTracks(false)) {
            if (track != null) {
                if (track.getTrackType() == UIConstants.overlayTrackType) {
                    track.setOverlayVisible(displayOverlays);
                }
            }

        }
    }


    /**
     * Method description
     *
     * @return
     */
    public int getVisibleTrackCount() {
        int count = 0;
        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            count += tsv.getVisibleTrackCount();

        }
        return count;
    }

    /**
     * Method description
     *
     * @param includeGeneTrack
     * @return
     */
    public List<Track> getAllTracks(boolean includeGeneTrack) {
        List<Track> allTracks = new ArrayList<Track>();

        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            allTracks.addAll(tsv.getTracks());
        }
        if ((geneTrack != null) && !includeGeneTrack) {
            allTracks.remove(geneTrack);
        }

        return allTracks;
    }

    public void clearSelections() {
        for (Track t : getAllTracks(true)) {
            t.setSelected(false);
        }

    }

    public void setTrackSelections(Set<Track> selectedTracks) {
        for (Track t : getAllTracks(true)) {
            if (selectedTracks.contains(t)) {
                t.setSelected(true);
            }
        }
    }

    public void shiftSelectTracks(Track track) {
        List<Track> allTracks = getAllTracks(true);
        int clickedTrackIndex = allTracks.indexOf(track);
        // Find another track that is already selected.  The semantics of this
        // are not well defined, so any track will do
        int otherIndex = clickedTrackIndex;
        for (int i = 0; i < allTracks.size(); i++) {
            if (allTracks.get(i).isSelected() && i != clickedTrackIndex) {
                otherIndex = i;
                break;
            }
        }

        int left = Math.min(otherIndex, clickedTrackIndex);
        int right = Math.max(otherIndex, clickedTrackIndex);
        for (int i = left; i <= right; i++) {
            allTracks.get(i).setSelected(true);
        }
    }

    public void toggleTrackSelections(Set<Track> selectedTracks) {
        for (Track t : getAllTracks(true)) {
            if (selectedTracks.contains(t)) {
                t.setSelected(!t.isSelected());
            }
        }
    }

    public Collection<Track> getSelectedTracks() {
        HashSet<Track> selectedTracks = new HashSet();
        for (Track t : getAllTracks(true)) {
            if (t.isSelected()) {
                selectedTracks.add(t);
            }
        }
        return selectedTracks;

    }

    /**
     * Return the complete set of unique DataResourcLocators currently loaded
     *
     * @return
     */
    public Set<ResourceLocator> getDataResourceLocators() {
        HashSet<ResourceLocator> locators = new HashSet();

        for (Track track : getAllTracks(false)) {
            ResourceLocator locator = track.getResourceLocator();

            if (locator != null) {
                locators.add(locator);
            }
        }

        return locators;

    }

    /**
     * Method description
     *
     * @param newHeight
     */
    public void setAllTrackHeights(int newHeight) {
        for (Track track : this.getAllTracks(false)) {
            track.setHeight(newHeight);
        }

    }


    public void unloadTracks(Collection<ResourceLocator> locators) {

        Collection<Track> tracksToRemove = new ArrayList();
        HashSet<ResourceLocator> locatorSet = new HashSet(locators);
        for (Track t : getAllTracks(true)) {
            if (locatorSet.contains(t.getResourceLocator())) {
                tracksToRemove.add(t);
            }
        }
        removeTracks(tracksToRemove);
    }

    /**
     * Method description
     *
     * @param tracksToRemove
     */
    public void removeTracks(Collection<Track> tracksToRemove) {

        Set<Track> tmp = new HashSet<Track>(tracksToRemove);
        // TODO -- replace with track.getDependentTracks(), or similar
        for (Track t : tracksToRemove) {
            if (t instanceof AlignmentTrack) {
                CoverageTrack ct = ((AlignmentTrack) t).getCoverageTrack();
                tmp.add(ct);
            }
        }

        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            tsv.removeTracks(tmp);


            if (!tsv.hasTracks()) {
                mainFrame.removeDataPanel(tsp.getName());
            }
        }

        for (Track t : tmp) {
            if (t instanceof DragListener) {
                DragEventManager.getInstance().removeDragListener((DragListener) t);
            }

        }
    }

    /**
     * Sort all groups (data and feature) by attribute value(s).  Tracks are
     * sorted within groups.
     *
     * @param attributeNames
     * @param ascending
     */
    public void sortAllTracksByAttributes(final String attributeNames[], final boolean[] ascending) {
        assert attributeNames.length == ascending.length;

        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            tsv.sortTracksByAttributes(attributeNames, ascending);
        }
    }

    public void sortAllTracksByPosition(List<String> trackIds) {
        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            tsv.sortTracksByPosition(trackIds);
        }
    }

    public TrackPanel getPanelFor(ResourceLocator locator) {
        String path = locator.getPath().toLowerCase();
        if (PreferenceManager.getInstance().isShowSingleTrackPane()) {
            return mainFrame.getDataPanel(DATA_PANEL_NAME);
        } else if (path.endsWith(".sam") || path.endsWith(".bam") ||
                path.endsWith(".sam.list") || path.endsWith(".bam.list") ||
                path.endsWith(".aligned") || path.endsWith(".sorted.txt")) {

            String newPanelName = "Panel" + System.currentTimeMillis();
            return mainFrame.addDataPanel(newPanelName).getTrackPanel();
        } else {
            return getDefaultPanel(locator);
        }
    }


    private TrackPanel getDefaultPanel(ResourceLocator locator) {

        if (locator.getType() != null && locator.getType().equalsIgnoreCase("das")) {
            return mainFrame.getDataPanel(FEATURE_PANEL_NAME);
        }

        String filename = locator.getPath().toLowerCase();

        if (filename.endsWith(".txt") || filename.endsWith(".tab") || filename.endsWith(
                ".xls") || filename.endsWith(".gz")) {
            filename = filename.substring(0, filename.lastIndexOf("."));
        }


        if (filename.contains("refflat") || filename.contains("ucscgene") ||
                filename.contains("genepred") || filename.contains("ensgene") ||
                filename.contains("refgene") ||
                filename.endsWith("gff") || filename.endsWith("gtf") ||
                filename.endsWith("gff3") || filename.endsWith("embl") ||
                filename.endsWith("bed") || filename.endsWith("gistic") ||
                filename.endsWith("bedz") || filename.endsWith("repmask") ||
                filename.contains("dranger")) {
            return mainFrame.getDataPanel(FEATURE_PANEL_NAME);
        } else {
            return mainFrame.getDataPanel(DATA_PANEL_NAME);
        }
    }

    /**
     * Sort all groups (data and feature) by a computed score over a region.  The
     * sort is done twice (1) groups are sorted with the featureGroup, and (2) the
     * groups themselves are sorted.
     *
     * @param region
     * @param type
     */
    public void sortByRegionScore(RegionOfInterest region,
                                  RegionScoreType type) {

        if (region == null) {
            ViewContext vc = ViewContext.getInstance();
            String chr = vc.getChrName();
            int start = (int) vc.getOrigin();
            int end = (int) vc.getEnd() + 1;
            region = new RegionOfInterest(chr, start, end, "");

        }

        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            tsv.sortByRegionsScore(region, type);
        }

    }

    /**
     * Method description
     *
     * @return
     */
    public String getGroupByAttribute() {
        return groupByAttribute;
    }


    /**
     * Method description
     *
     * @param track
     * @return
     */
    public List<Track> getOverlayTracks(Track track) {
        String overlayAttribute = IGVMainFrame.getInstance().getSession().getOverlayAttribute();
        String value = track.getAttributeValue(overlayAttribute);
        return overlayTracksMap.get(value);

    }

    /**
     * Method description
     *
     * @param genomeId
     */
    public void loadGeneTrack(String genomeId) {

        // Load genes
        GeneManager geneManager = GeneManager.getGeneManager(genomeId);


        Map<String, List<Feature>> featureMap = null;
        if (geneManager != null) {
            featureMap = geneManager.getChromsomeGeneMap();
        } else {
            featureMap = new HashMap<String, List<Feature>>();
        }

        FeatureTrack geneFeatureTrack = new FeatureTrack("Genes", new FeatureCollectionSource(featureMap));
        geneFeatureTrack.setMinimumHeight(5);
        geneFeatureTrack.setHeight(30);
        geneFeatureTrack.setRendererClass(BasicFeatureRenderer.class);
        geneFeatureTrack.setColor(Color.BLUE.darker());

        SequenceTrack seqTrack = new SequenceTrack("Reference");
        if (geneManager != null) {

            if (geneManager.getTrackProperties() != null) {
                geneFeatureTrack.setTrackProperties(geneManager.getTrackProperties());
            }

            String geneTrackName = geneManager.getGeneTrackName();

            GeneTrack gt = new GeneTrack(geneFeatureTrack, seqTrack);
            gt.setName(geneTrackName);

            Genome genome = GenomeManager.getInstance().getGenome(genomeId);
            if (genome != null && gt.getUrl() == null) {
                gt.setUrl(genome.getAnnotationURL());
            }

            setGeneTrack(gt);
        } else {
            setGeneTrack(seqTrack);
        }

    }

    /**
     * Replace current gene track with new one.  This is called upon switching genomes
     *
     * @param newGeneTrack
     */
    private void setGeneTrack(Track newGeneTrack) {

        boolean foundGeneTrack = false;
        for (TrackPanelScrollPane tsp : getTrackPanelScrollPanes()) {
            TrackPanel tsv = tsp.getTrackPanel();
            foundGeneTrack = tsv.setGeneTrack(geneTrack, newGeneTrack);
            if (foundGeneTrack) {
                break;
            }
        }

        if (!foundGeneTrack) {
            if (PreferenceManager.getInstance().isShowSingleTrackPane()) {
                mainFrame.getDataPanel(DATA_PANEL_NAME).addTrack(newGeneTrack);
            } else {
                mainFrame.getDataPanel(FEATURE_PANEL_NAME).addTrack(newGeneTrack);
            }
        }

        // Keep a reference to this track so it can be removed
        geneTrack = newGeneTrack;

    }


    /*
  public void updateTrackPositions(String panelName) {
  int regionY = 0;
  Collection<TrackGroup> groups = trackGroups.get(panelName).values();
  for (TrackGroup group : groups) {
  if (group.isVisible()) {
  if (groups.size() > 1) {
  regionY += UIStringConstants.groupGap;
  }
  int previousRegion = -1;
  for (Track track : group.getTracks()) {
  int trackHeight = track.getHeight();
  if (track.isVisible()) {
  if (isDragging) {
  Color currentColor = g.getColor();
  if (dragY >= previousRegion && dragY <= regionY) {
  g.setColor(Color.GRAY);
  g.drawLine(0, regionY, getWidth(), regionY);
  regionY++;
  g.setColor(currentColor);
  dropTarget = track;
  } else if (dragY >= (regionY + trackHeight)) {
  g.setColor(Color.GRAY);
  g.drawLine(0, regionY + trackHeight,
  getWidth(), regionY + trackHeight);
  trackHeight--;
  g.setColor(currentColor);
  dropTarget = null;
  }
  }
  previousRegion = regionY;
  if (regionY + trackHeight >= visibleRect.y) {
  int width = getWidth();
  int height = track.getHeight();
  Rectangle region = new Rectangle(regionX,
  regionY, width, height);
  addMousableRegion(new MouseableRegion(region, track));
  draw(graphics2D, track, regionX, regionY, width,
  height, visibleRect);
  }
  regionY += trackHeight;
  }
  }
  if (group.isDrawBorder()) {
  g.drawLine(0, regionY, getWidth(), regionY);
  }
  }
  }
  }
   * */
    /**
     * Method description
     *
     */
    /*
    public void dumpData() {
    PrintWriter pw = null;
    try {
    String chr = ViewContext.getInstance().getChromosome().getName();
    double x = ViewContext.getInstance().getOrigin();
    pw = new PrintWriter(new FileWriter("DataDump.tab"));
    pw.println("Sample\tCopy Number\tExpression\tMethylation");
    for (String name : groupsMap.keySet()) {
    pw.print(name);
    float cn = 0;
    float exp = 0;
    float meth = 0;
    int nCn = 0;
    int nExp = 0;
    int nMeth = 0;
    for (Track t : groupsMap.get(name).getTracks()) {
    LocusScore score = ((DataTrack) t).getLocusScoreAt(chr, x);
    if ((score != null) && !Float.isNaN(score.getScore())) {
    if (t.getTrackType() == TrackType.COPY_NUMBER) {
    nCn++;
    cn += score.getScore();
    } else if (t.getTrackType() == TrackType.GENE_EXPRESSION) {
    nExp++;
    exp += score.getScore();
    } else if (t.getTrackType() == TrackType.DNA_METHYLATION) {
    nMeth++;
    meth += score.getScore();
    }
    }
    }
    pw.print("\t");
    if (nCn > 0) {
    pw.print(cn / nCn);
    }
    pw.print("\t");
    if (nExp > 0) {
    pw.print(exp / nExp);
    }
    pw.print("\t");
    if (nMeth > 0) {
    pw.print(cn / meth);
    }
    pw.println();
    }
    for (Track track : getTracksForPanel(false)) {
    if (track instanceof DataTrack) {
    LocusScore score = ((DataTrack) track).getLocusScoreAt(chr,
    x);
    if (score != null) {
    pw.println(
    track.getName() + "\t" + track.getTrackType() + "\t" + score.getScore());
    }
    }
    }
    } catch (IOException ex) {
    ex.printStackTrace();
    } finally {
    pw.close();
    }
    }     * */


}
