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

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

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.Genome;
import org.broad.igv.feature.GenomeDescriptor;
import org.broad.igv.feature.GenomeManager;
import org.broad.igv.sam.AlignmentTrack;
import org.broad.igv.track.Track;
import org.broad.igv.track.TrackGroup;
import org.broad.igv.ui.IGVMainFrame;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.panel.DataPanel;
import org.broad.igv.ui.panel.DragEventManager;
import org.broad.igv.ui.panel.TrackPanelScrollPane;
import org.broad.igv.ui.util.MessageUtils;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

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

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


    private static ViewContext instance = new ViewContext();

    private String genomeId;

    Genome genome;

    /**
     * The nominal viewport width in pixels.
     */
    public int binsPerTile = 700;

    int dataPanelWidth = 700;

    /**
     * The chromosome currently in view
     */
    private String chrName = "chrAll";

    /**
     * The minimum zoom level for the current screen size + chromosome combination.
     */
    private int minZoom = 0;

    /**
     * The current zoom level.  Zoom level -1 corresponds to the whole
     * genome view (chromosome "all")
     */
    private int zoom = minZoom;
    /**
     * The maximum zoom level.  Set to prevent integer overflow.  This is a function
     * of chromosom length.
     */
    public static int maxZoom = 23;
    /**
     * The number of tiles for this zoom level, = 2^zoom
     */
    private int nTiles = 1;
    /**
     * The maximum virtual pixel value.
     */
    private double maxPixel;
    /**
     * The origin in bp
     */
    private double origin = 0;

    /**
     * The location (x axis) locationScale in base pairs / virtual pixel
     */
    private double locationScale;

    private boolean locationScaleValid = false;
    /**
     * Indicates if the data is stretched to fill the window.  This happens at low
     * resolution views when drawing at the resolution of the zoom level would
     * only partially fill the screen.
     */
    private boolean stretched = false;


    public static ViewContext getInstance() {
        return instance;
    }

    /**
     * Constructs ...
     */
    private ViewContext() {
        String lastChromosomeViewed = PreferenceManager.getInstance().getLastChromosomeViewed();

        if ((lastChromosomeViewed == null) || lastChromosomeViewed.trim().equals("")) {
            this.chrName = getHomeChr();
        } else {
            this.chrName = lastChromosomeViewed;

        }
    }

    /**
     * Return the "home chromosome" for the current genome.  This should probably be a method
     * on the Genome class.
     *
     * @return
     */
    public String getHomeChr() {
        return genome == null ? Globals.CHR_ALL : genome.getHomeChromosome();
    }

    /**
     * Compute the maximum zoom level, which is a function of chromosome length.
     */
    private void computeMaxZoom() {

        // Compute max zoom.  Assume window size @ max zoom of ~ 50 bp
        if (genome != null && chrName != null && genome.getChromosome(chrName) != null) {
            if (chrName.equals(Globals.CHR_ALL)) {
                maxZoom = 0;
            } else {
                int chrLength = genome.getChromosome(chrName).getLength();

                maxZoom = (int) (Math.log(chrLength / 50.0) / log2) + 1;
            }

            if (zoom > maxZoom) {
                setZoom(maxZoom);
            }
        }


        // TODO -- fire chromosome changed event rather than this
    }

    private void setZoom(int newZoom) {
        zoom = Math.min(maxZoom, newZoom);
        nTiles = (int) Math.pow(2, Math.max(minZoom, zoom));
        maxPixel = getTilesTimesBinsPerTile();
        invalidateLocationScale();

        // TODO -- do this with events,
        if (IGVMainFrame.hasInstance()) {
            IGVMainFrame.getInstance().repaintStatusAndZoomSlider();
        }

    }

    /**
     * Method description
     *
     * @param increment
     */
    public void incrementZoom(int increment) {
        zoomAndCenter((int) zoom + increment);
    }

    /**
     * Method description
     *
     * @param newZoom
     */
    public void zoomAndCenterAdjusted(int newZoom) {
        zoomAndCenter(minZoom + newZoom);
    }

    /**
     * Method description
     *
     * @param newZoom
     */
    public void zoomAndCenter(int newZoom) {

        // Zoom but remain centered about current center
        double currentCenter = origin + ((dataPanelWidth / 2) * getScale());

        zoomTo(newZoom, currentCenter);
    }

    /**
     * Method description
     *
     * @param newZoom
     * @param newCenter
     */
    public void zoomTo(final int newZoom, final double newCenter) {

        if (chrName.equals(Globals.CHR_ALL)) {
            chrName = getHomeChr();
        }

        if (chrName.equals(Globals.CHR_ALL)) {

            // DISABLE ZOOMING FOR GENOME VIEW
            // Translate the location to chromosome number
            jumpToChromosomeForGenomeLocation(newCenter);
            IGVMainFrame.getInstance().chromosomeChangeEvent();
        } else {
            if (zoom != newZoom) {

                setZoom(newZoom);
                computeLocationScaleImmediately();

                double newLocationScale = getScale();

                // Adjust origin so newCenter is centered
                double newOrigin = Math.round(
                        newCenter - ((dataPanelWidth / 2) * newLocationScale));

                setOrigin(newOrigin);

            }
        }
        // This is a hack,  this is not a drag event but is a "jump"
        DragEventManager.getInstance().dragStopped();

    }

    private void jumpToChromosomeForGenomeLocation(double locationMB) {
        double startMB = 0;

        for (String chr : getGenome().getChromosomeNames()) {
            double endMB = startMB + getGenome().getChromosome(chr).getLength() / 1000.0;

            if ((locationMB > startMB) && (locationMB <= endMB)) {

                // this.jumpTo(chr, -1, -1);
                this.setChromosomeName(chr);
                break;
            }

            startMB = endMB;
        }
    }

    public void pageLeft() {
        shiftOriginPixels(-dataPanelWidth);
    }

    public void pageRight() {
        shiftOriginPixels(dataPanelWidth);
    }

    /**
     * Method description
     *
     * @param delta
     */
    public void shiftOriginPixels(double delta) {
        double shiftBP = delta * getScale();
        setOrigin(origin + shiftBP);
    }

    public void snapToGrid() {
        setOrigin(Math.round(origin));
    }

    /**
     * Method description
     *
     * @param chrLocation
     */
    public void centerOnLocation(String chr, double chrLocation) {
        if (!chrName.equals(chr)) {
            chrName = chr;
            computeMaxZoom();
            if (zoom > maxZoom) {
                setZoom(maxZoom);
            }
        }
        centerOnLocation(chrLocation);
    }

    /**
     * Method description
     *
     * @param chrLocation
     */
    public void centerOnLocation(double chrLocation) {
        double windowWidth = (dataPanelWidth * getScale()) / 2;

        setOrigin(Math.round(chrLocation - windowWidth));
    }

    public boolean windowAtEnd() {
        double windowLengthBP = dataPanelWidth * getScale();
        return origin + windowLengthBP + 1 > getChromosomeLength();

    }


    /* Keep origin within data range */

    /**
     * Method description
     *
     * @param newOrigin
     */
    public void setOrigin(double newOrigin) {
        int windowLengthBP = (int) (dataPanelWidth * getScale());

        origin = Math.max(0, Math.min(newOrigin, getChromosomeLength() - windowLengthBP));

        for (TrackPanelScrollPane sp : IGVMainFrame.getInstance().getTrackManager().getTrackPanelScrollPanes()) {
            preloadTrackData(sp.getDataPanel());
        }

        // If zoomed in sufficiently track the center position
        if (locationScale < 10) {
            IGVMainFrame.getInstance().setStatusBarMessage(chrName + ":" + ((int) getCenter() + 1));
        }

        // Repaint
        IGVMainFrame.getInstance().repaintDataAndHeaderPanels();
        IGVMainFrame.getInstance().repaintStatusAndZoomSlider();
    }

    static double log2 = Math.log(2);

    public void jumpTo(String chr, int start, int end) {
        jumpTo(chr, start, end, true);

    }

    public void jumpTo(String chr, int start, int end, boolean update) {

        //if (log.isDebugEnabled()) {
        //    log.debug("Jump to: " + chr + ":" + start + "-" + end);
        //}

        // Switch chromosomes if not null
        if (chr != null) {
            if (genome.getChromosome(chr) == null && !chr.contains(Globals.CHR_ALL)) {
                JOptionPane.showMessageDialog(IGVMainFrame.getInstance(),
                        chr + " is not a valid chromosome.");
                return;
            }
            setChromosomeName(chr);
        }

        if (start >= 0) {

            // Estmate zoom level
            int z = (int) (Math.log(getChromosomeLength() / (end - start)) / log2) + 1;

            if (z != this.zoom) {
                zoom = Math.min(maxZoom, Math.max(minZoom, z));
                nTiles = (int) Math.pow(2, zoom);
                maxPixel = getTilesTimesBinsPerTile();
            }

            // overide computed scale
            int w = dataPanelWidth;
            //if (log.isDebugEnabled()) {
            //    log.debug("W = " + w);
            //}

            setLocationScale(((double) (end - start)) / w);

            origin = start;
        }

        if (update) {

            for (TrackPanelScrollPane sp : IGVMainFrame.getInstance().getTrackManager().getTrackPanelScrollPanes()) {
                preloadTrackData(sp.getDataPanel());
            }

            if (log.isDebugEnabled()) {
                log.debug("Data panel width = " + dataPanelWidth);
                log.debug("New start = " + (int) getOrigin());
                log.debug("New end = " + (int) getEnd());
                log.debug("New center = " + (int) getCenter());
            }

            // Repaint
            IGVMainFrame.getInstance().repaintDataAndHeaderPanels();
            IGVMainFrame.getInstance().repaintStatusAndZoomSlider();
        }
    }

    /**
     * Method description
     *
     * @return
     */
    public double getOrigin() {
        return origin;
    }

    public double getCenter() {
        return origin + getScale() * dataPanelWidth / 2;
    }

    public double getEnd() {
        return origin + getScale() * dataPanelWidth;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getZoom() {
        return zoom;
    }

    /**
     * Return the maximum zoom level
     *
     * @return
     */
    public int getMaxZoom() {
        return maxZoom;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getAdjustedZoom() {
        return zoom - minZoom;
    }

    /**
     * Method description
     *
     * @return
     */
    public int getNTiles() {
        return nTiles;
    }

    /**
     * Method description
     *
     * @return
     */
    public double getMaxPixel() {
        return maxPixel;
    }

    /**
     * Method description
     *
     * @param panel
     */
    public void setDataPanel(DataPanel panel) {

        if (panel != null) {

            panel.addComponentListener(new ComponentAdapter() {

                @Override
                public void componentResized(ComponentEvent e) {
                    int w = e.getComponent().getWidth();
                    if (w != dataPanelWidth) {
                        dataPanelWidth = w;
                        invalidateLocationScale();
                    }
                }
            });
        }
    }

    /**
     * Method description
     *
     * @param name
     */
    public void setChrName(String name) {
        this.setChromosomeName(name);
    }

    /**
     * @param name
     * @param force
     * @ deprecated, replace with calls to setChrName();
     */
    public void setChromosomeName(String name, boolean force) {

        if ((chrName == null) || !name.equals(chrName) || force) {
            chrName = name;
            origin = 0;
            setZoom(0);
            computeMaxZoom();
        }
    }

    /**
     * @param name
     * @ deprecated, replace with calls to setChrName();
     */
    public void setChromosomeName(String name) {
        setChromosomeName(name, false);
    }

    /**
     * Method description
     *
     * @return
     */
    public String getChrName() {
        return chrName;
    }

    public String getNextChrName(String chr) {
        List<String> chrList = genome.getChromosomeNames();
        for (int i = 0; i < chrList.size() - 1; i++) {
            if (chrList.get(i).equals(chr)) {
                return chrList.get(i + 1);
            }
        }
        return null;
    }

    public String getPrevChrName(String chr) {
        List<String> chrList = genome.getChromosomeNames();
        for (int i = chrList.size() - 1; i > 0; i--) {
            if (chrList.get(i).equals(chr)) {
                return chrList.get(i - 1);
            }
        }
        return null;
    }

    // TODO -- this parameter shouldn't be stored here.  Maybe in a specialized
    // layout manager?

    /**
     * Method description
     *
     * @return
     */
    public int getDataPanelWidth() {
        return dataPanelWidth;
    }

    /**
     * Return the current locationScale in base pairs / pixel
     *
     * @return
     */
    public double getScale() {
        if ((locationScale == 0) || !locationScaleValid) {
            computeLocationScaleImmediately();
        }

        if (locationScale < 0) {
            System.err.println("Negative scale");
        }

        return locationScale;
    }

    /**
     * Method description
     */
    public void invalidateLocationScale() {
        //Thread.dumpStack();
        locationScaleValid = false;
    }

    public void computeLocationScaleImmediately() {

        if (genome != null) {

            computeMinZoom();

            double virtualPixelSize = getTilesTimesBinsPerTile();

            stretched = virtualPixelSize < dataPanelWidth;

            double nPixel = Math.max(virtualPixelSize, dataPanelWidth);

            setLocationScale(((double) getChromosomeLength()) / nPixel);
        }
    }

    /**
     * Compute the minimum zoom level for the data panel width.  This is defined as the maximum
     * zoom level for which all data bins will fit in the window without loss of
     * data,  i.e. the maximum zoom level for which nBins < nPixels.  The number
     * of bins is defined as
     * nBins =  2^z
     * so minZoom is the value z such that nBins < dataPanelWidth
     */
    private void computeMinZoom() {
        if (this.chrName.equals(Globals.CHR_ALL)) {
            minZoom = 0;
        } else {
            minZoom = Math.max(0, (int) (Math.log((dataPanelWidth / binsPerTile)) / log2));

            if (zoom < minZoom) {
                zoom = minZoom;
                nTiles = (int) Math.pow(2, zoom);
                maxPixel = getTilesTimesBinsPerTile();
            }
        }

    }

    /**
     * Return the chromosome position corresponding to the pixel index.  The
     * pixel index is the pixel "position" translated by -origin.
     *
     * @param pixelIndex
     * @return
     */
    public double getChromosomePosition(int pixelIndex) {
        return origin + (getScale() * pixelIndex);
    }

    /**
     * Return the pixel position corresponding to the chromosomal position.
     *
     * @param chromosomePosition
     * @return
     */
    public int getPixelPosition(double chromosomePosition) {
        return (int) ((chromosomePosition - origin) / getScale());
    }

    /**
     * Method description
     *
     * @return
     */
    public Chromosome getChromosome() {
        if (genome == null) {
            log.error("Genome not loaded!");
            return null;
        }

        return genome.getChromosome(chrName);
    }

    /**
     * Method description
     *
     * @return
     */
    public int getChromosomeLength() {

        if (genome == null) {
            return 1;
        }

        if (chrName.equals("All")) {

            // TODO -- remove the hardcoded unit divider ("1000")
            return (int) (genome.getLength() / 1000);

            // return genome.getLength();
        } else {
            if (getChromosome() == null) {
                System.out.println("Null chromosome: " + chrName);
                if (genome == null) {
                    return 1;
                } else {
                    return genome.getChromosomes().iterator().next().getLength();
                }
            }

            return getChromosome().getLength();
        }
    }

    // /////////////////////////////////////////////////////////////////////////
    // Genome methods /////////////////////////////////////////////////////////

    /**
     * Attempt to switch genomes to newGenome.  Return the actual genome, which
     * might differ if the load of newGenome fails.
     *
     * @param newGenome
     */
    public String setGenomeId(String newGenome) {

        if (log.isDebugEnabled()) {
            log.debug("Setting genome id: " + newGenome);
        }

        boolean startUp = (genomeId == null);
        boolean loadFailed = false;

        genomeId = newGenome;
        if (!GenomeManager.getInstance().isGenomeLoaded(genomeId)) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("findGenomeAndLoad: " + genomeId);
                }
                GenomeManager.getInstance().findGenomeAndLoad(genomeId);
            } catch (IOException e) {
                log.error("Error loading genome: " + genomeId, e);
                loadFailed = true;
                MessageUtils.showMessage("Load of genome: " + genomeId + " failed.");
            }
        }
        genome = GenomeManager.getInstance().getGenome(genomeId);

        if (genome == null || loadFailed) {
            GenomeDescriptor defaultDesc = GenomeManager.getInstance().getDefaultGenomeDescriptor();
            String msg = "Could not locate genome: " + genomeId + ".  Loading " + defaultDesc.getName();
            MessageUtils.showMessage(msg);
            log.error("Could not locate genome: " + genomeId + ".  Loading " + defaultDesc.getName());

            // The previously used genome is unavailable.   Load hg18, we are assuming that
            // this is always available.  // TODO -- permit IGV starting with no selected genome
            genomeId = defaultDesc.getId();
            try {
                if (log.isDebugEnabled()) {
                    log.debug("findGenomeAndLoad: " + genomeId);
                }
                GenomeManager.getInstance().findGenomeAndLoad(genomeId);
            } catch (IOException e) {
                log.error("Error loading genome: " + genomeId, e);
                MessageUtils.showMessage("<html>Load of genome: " + genomeId + " failed." +
                        "<br>IGV is in an ustable state and will be closed." +
                        "<br>Please report this error to igv-help@broadinstitute.org");
                System.exit(-1);
            }

            if (log.isDebugEnabled()) {
                log.debug("Get genome id");
            }
            genome = GenomeManager.getInstance().getGenome(genomeId);


            PreferenceManager.getInstance().setDefaultGenome(genomeId);
            this.setChrName(getHomeChr());
        } else if (!startUp) {
            // We don't know what chromosomes the new genome has, set to "all" (whole genome)
            this.setChrName(getHomeChr());
        }
        computeMaxZoom();
        invalidateLocationScale();

        return genomeId;
    }

    /**
     * Version for command line tools.
     *
     * @param newGenome
     */
    public void setGenomeIdHeadless(String newGenome) {


        genomeId = newGenome;
        genome = GenomeManager.getInstance().getGenome(genomeId);


    }

    /**
     * Method description
     *
     * @return
     */
    public String getGenomeId() {
        return genomeId;
    }

    /**
     * Method description
     *
     * @return
     */
    public Genome getGenome() {
        return genome;
    }

    /**
     * Return the collection of chromosome names for the currently loaded
     * genome
     *
     * @return
     */
    public Collection<String> getChromosomeNames() {
        return genome.getChromosomeNames();
    }

    /**
     * Method description
     *
     * @return
     */
    public boolean isStretched() {
        return stretched;
    }

    /**
     * Method description
     *
     * @return
     */
    public double getTilesTimesBinsPerTile() {
        return (double) nTiles * (double) binsPerTile;
    }

    /**
     * Get the UCSC style locus string corresponding to the current view.  THe UCSC
     * conventions are followed for coordinates,  specifically the internal representation
     * is "zero" based (first base is numbered 0) but the display representation is
     * "one" based (first base is numbered 1).   Consequently 1 is added to the
     * computed positions.
     *
     * @return
     */
    public String getCurrentLocusString() {

        if (zoom == 0) {
            return getChrName();
        } else {

            int start = 0;
            int end = getDataPanelWidth();
            int startLoc = (int) getChromosomePosition(start) + 1;
            int endLoc = (int) getChromosomePosition(end);
            String startStr = NumberFormat.getInstance().format(startLoc);
            String endStr = NumberFormat.getInstance().format(endLoc);
            String position = getChrName() + ":" + startStr + "-" + endStr;

            return position;
        }

    }

    private void setLocationScale(double locationScale) {
        this.locationScale = locationScale;
        locationScaleValid = true;

        //if (log.isDebugEnabled()) {
        //    log.debug("locationScale = " + locationScale);
        //}
    }

    /**
     *
     */
    private void preloadTrackData(DataPanel dp) {

        Rectangle visibleRect = dp.getVisibleRect();
        int start = (int) origin;
        int end = (int) (start + dp.getWidth() * locationScale);
        int trackY = 0;
        Collection<TrackGroup> groups = dp.getTrackGroups();

        for (Iterator<TrackGroup> groupIter = groups.iterator(); groupIter.hasNext();) {
            TrackGroup group = groupIter.next();
            if (group.isVisible()) {
                if (groups.size() > 1) {
                    trackY += UIConstants.groupGap;
                }
                for (Track track : group.getTracks()) {
                    int trackHeight = track.getHeight();

                    if (!(track instanceof AlignmentTrack) && visibleRect != null) {
                        if (trackY > visibleRect.y + visibleRect.height) {
                            break;
                        } else if (trackY + trackHeight < visibleRect.y) {
                            trackY += trackHeight;
                            continue;
                        }
                    }


                    if (track.isVisible()) {
                        track.preloadData(chrName, start, end, zoom);

                        trackY += track.getHeight();
                    }
                }
            }

        }
    }
}
