/*
 * Copyright (c) 2007-2012 The Broad Institute, Inc.
 * SOFTWARE COPYRIGHT NOTICE
 * This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
 *
 * This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
 *
 * 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.
 */

package org.broad.igv.ui;

import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.IExon;
import org.broad.igv.renderer.SashimiJunctionRenderer;
import org.broad.igv.sam.*;
import org.broad.igv.track.*;
import org.broad.igv.ui.color.ColorPalette;
import org.broad.igv.ui.color.ColorUtilities;
import org.broad.igv.ui.event.AlignmentTrackEvent;
import org.broad.igv.ui.panel.*;
import org.broad.igv.ui.util.UIUtilities;

import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.util.*;
import java.util.List;

/**
 * Window for displaying sashimi style junction plot
 * See http://genes.mit.edu/burgelab/miso/docs/sashimi.html
 * <p/>
 * User: jacob
 * Date: 2013-Jan-11
 */
public class SashimiPlot extends JFrame {

    private List<SpliceJunctionFinderTrack> spliceJunctionTracks;
    private ReferenceFrame frame;

    /**
     * The minimum allowed origin of the frame. We set scrolling
     * limits based on initialization
     */
    private final double minOrigin;

    /**
     * The maximum allow end of the frame. We set scrolling
     * limits based on initialization
     */
    private final double maxEnd;



    private static final List<Color> plotColors;

    static {
        ColorPalette palette = ColorUtilities.getDefaultPalette();
        plotColors = Arrays.asList(palette.getColors());
    }


    public SashimiPlot(ReferenceFrame iframe, Collection<? extends AlignmentTrack> alignmentTracks, FeatureTrack geneTrack) {
        getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        int minJunctionCoverage = PreferenceManager.getInstance().getAsInt(PreferenceManager.SAM_JUNCTION_MIN_COVERAGE);

        this.frame = new ReferenceFrame(iframe);

        minOrigin = this.frame.getOrigin();
        maxEnd = this.frame.getEnd();

        initSize(frame.getWidthInPixels());

        BoxLayout boxLayout = new BoxLayout(getContentPane(), BoxLayout.Y_AXIS);
        getContentPane().setLayout(boxLayout);

        //Add control elements to the top
        getContentPane().add(generateControlPanel(this.frame));

        spliceJunctionTracks = new ArrayList<SpliceJunctionFinderTrack>(alignmentTracks.size());
        int colorInd = 0;

        for(AlignmentTrack alignmentTrack: alignmentTracks){


            AlignmentDataManager oldDataManager = alignmentTrack.getDataManager();
            MemoryAlignmentDataManager dataManager = new MemoryAlignmentDataManager(oldDataManager, oldDataManager.getSpliceJunctionLoadOptions());

            SpliceJunctionFinderTrack spliceJunctionTrack = new SpliceJunctionFinderTrack(alignmentTrack.getResourceLocator(), alignmentTrack.getName(), dataManager, true);

            spliceJunctionTrack.setRendererClass(SashimiJunctionRenderer.class);

            Color color = plotColors.get(colorInd);
            colorInd = (colorInd + 1) % plotColors.size();
            spliceJunctionTrack.setColor(color);

            TrackComponent<SpliceJunctionFinderTrack> trackComponent = new TrackComponent<SpliceJunctionFinderTrack>(frame, spliceJunctionTrack);

            initSpliceJunctionComponent(trackComponent, dataManager, oldDataManager.getCoverageTrack(), minJunctionCoverage);

            getContentPane().add(trackComponent);
            spliceJunctionTracks.add(spliceJunctionTrack);
        }

        Axis axis = createAxis(frame);
        getContentPane().add(axis);

        SelectableFeatureTrack geneTrackClone = new SelectableFeatureTrack(geneTrack);
        TrackComponent<SelectableFeatureTrack> geneComponent = new TrackComponent<SelectableFeatureTrack>(frame, geneTrackClone);

        getContentPane().add(geneComponent);

        initGeneComponent(frame.getWidthInPixels(), geneComponent, geneTrackClone);
        validate();
    }

    private Component generateControlPanel(ReferenceFrame frame) {
        JPanel controlPanel = new JPanel();

        ZoomSliderPanel zoomSliderPanel = new ZoomSliderPanel(frame);
        zoomSliderPanel.setMinZoomLevel(frame.getZoom());

        zoomSliderPanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                SashimiPlot.this.repaint();
            }
        });

        Dimension controlSize = new Dimension(200, 30);

        //JSlider scaleSlider = new JSlider(JSlider.HORIZONTAL);
        //setFixedSize(scaleSlider, controlSize);
        //controlPanel.add(scaleSlider);

        controlPanel.add(zoomSliderPanel);
        setFixedSize(zoomSliderPanel, controlSize);

        Dimension panelSize = controlSize;
        setFixedSize(controlPanel, panelSize);


        BoxLayout layout = new BoxLayout(controlPanel, BoxLayout.X_AXIS);
        controlPanel.setLayout(layout);

        return controlPanel;
    }

    private static void setFixedSize(Component component, Dimension dimension){
        component.setPreferredSize(dimension);
        component.setMinimumSize(dimension);
        component.setMaximumSize(dimension);
    }

    private void initSize(int width) {
        setSize(width, 500);
    }

    private Axis createAxis(ReferenceFrame frame) {
        Axis axis = new Axis(frame);

        Dimension maxDim = new Dimension(Integer.MAX_VALUE, 25);
        axis.setMaximumSize(maxDim);
        Dimension prefDim = new Dimension(maxDim);
        prefDim.setSize(frame.getWidthInPixels(), prefDim.height);
        axis.setPreferredSize(prefDim);

        return axis;
    }

    private void initGeneComponent(int prefWidth, TrackComponent<SelectableFeatureTrack> geneComponent, FeatureTrack geneTrack) {

        geneTrack.setDisplayMode(Track.DisplayMode.SQUISHED);

        geneTrack.clearPackedFeatures();
        RenderContext context = new RenderContextImpl(geneComponent, null, frame, null);
        geneTrack.setForceLoadSync(true);
        geneTrack.load(context);


        Dimension maxGeneDim = new Dimension(Integer.MAX_VALUE, geneTrack.getNumberOfFeatureLevels() * geneTrack.getSquishedRowHeight() + 10);
        geneComponent.setMaximumSize(maxGeneDim);
        Dimension prefGeneDim = new Dimension(maxGeneDim);
        prefGeneDim.setSize(prefWidth, prefGeneDim.height);
        geneComponent.setPreferredSize(prefGeneDim);

        GeneTrackMouseAdapter ad2 = new GeneTrackMouseAdapter(geneComponent);
        geneComponent.addMouseListener(ad2);
        geneComponent.addMouseMotionListener(ad2);
    }

    private void initSpliceJunctionComponent(TrackComponent<SpliceJunctionFinderTrack> trackComponent, IAlignmentDataManager dataManager, CoverageTrack coverageTrack, int minJunctionCoverage) {
        JunctionTrackMouseAdapter ad1 = new JunctionTrackMouseAdapter(trackComponent);
        trackComponent.addMouseListener(ad1);
        trackComponent.addMouseMotionListener(ad1);

        getRenderer(trackComponent.track).setDataManager(dataManager);
        getRenderer(trackComponent.track).setCoverageTrack(coverageTrack);
        getRenderer(trackComponent.track).getCoverageTrack().rescale(trackComponent.frame);

        dataManager.setMinJunctionCoverage(minJunctionCoverage);

        getRenderer(trackComponent.track).setBackground(getBackground());
    }

    private SashimiJunctionRenderer getRenderer(SpliceJunctionFinderTrack spliceJunctionTrack) {
        return (SashimiJunctionRenderer) spliceJunctionTrack.getRenderer();
    }

    /**
     * Should consider using this elsewhere. Single component
     * which contains a single track
     */
    private static class TrackComponent<T extends Track> extends JComponent {

        private T track;
        private ReferenceFrame frame;
        private String toolTipText = null;

        public TrackComponent(ReferenceFrame frame, T track) {
            this.frame = frame;
            this.track = track;
        }

        public void updateToolTipText(TrackClickEvent tce){
            toolTipText = track.getValueStringAt(tce.getFrame().getChrName(), tce.getChromosomePosition(), tce.getMouseEvent().getY(), tce.getFrame());
            toolTipText = "<html>" + toolTipText;
            setToolTipText(toolTipText);
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            Rectangle visibleRect = getVisibleRect();
            RenderContext context = new RenderContextImpl(this, (Graphics2D) g, frame, visibleRect);
            track.render(context, visibleRect);
        }

    }

    /**
     * Set the minimum junction coverage, per trac,k and is not persistent
     *
     * Our "Set Max Junction Coverage Range" just changes the view scaling, it doesn't
     * filter anything, which is different behavior than the minimum. This might be confusing.
     *
     * @param trackComponent
     * @param newMinJunctionCoverage
     **/
    private void setMinJunctionCoverage(TrackComponent<SpliceJunctionFinderTrack> trackComponent, int newMinJunctionCoverage) {
        IAlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager();
        dataManager.setMinJunctionCoverage(newMinJunctionCoverage);
        trackComponent.track.onAlignmentTrackEvent(
                new AlignmentTrackEvent(this, AlignmentTrackEvent.Type.SPLICE_JUNCTION));
        trackComponent.repaint();
    }


    /**
     * Set the max coverage depth, which is a graphical scaling parameter for determining how
     * thick the junction arcs will be
     * @param trackComponent
     * @param newMaxDepth
     */
    private void setMaxCoverageDepth(TrackComponent<SpliceJunctionFinderTrack> trackComponent, int newMaxDepth) {
        getRenderer(trackComponent.track).setMaxDepth(newMaxDepth);
        repaint();
    }

    private class JunctionTrackMouseAdapter extends TrackComponentMouseAdapter<SpliceJunctionFinderTrack> {

        JunctionTrackMouseAdapter(TrackComponent<SpliceJunctionFinderTrack> trackComponent) {
            super(trackComponent);
        }

        @Override
        protected void handleDataClick(MouseEvent e) {
            //Show data of some sort?
        }

        @Override
        protected IGVPopupMenu getPopupMenu(MouseEvent e) {
            IGVPopupMenu menu = new IGVPopupMenu();

            CoverageTrack covTrack = getRenderer(this.trackComponent.track).getCoverageTrack();
            JMenuItem setCoverageDataRange = CoverageTrack.addDataRangeItem(SashimiPlot.this, null, Arrays.asList(covTrack));
            setCoverageDataRange.setText("Set Coverage Data Range");
            menu.add(setCoverageDataRange);

            JMenuItem minJunctionCoverage = new JMenuItem("Set Min Junction Coverage");
            minJunctionCoverage.setToolTipText("Junctions below this threshold will be removed from view");
            minJunctionCoverage.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    IAlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager();
                    SpliceJunctionHelper.LoadOptions loadOptions = dataManager.getSpliceJunctionLoadOptions();

                    String input = JOptionPane.showInputDialog("Set Minimum Junction Coverage", loadOptions.minJunctionCoverage);
                    if (input == null || input.length() == 0) return;
                    try {
                        int newMinJunctionCoverage = Integer.parseInt(input);
                        setMinJunctionCoverage(JunctionTrackMouseAdapter.this.trackComponent, newMinJunctionCoverage);

                    } catch (NumberFormatException ex) {
                        JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer");
                    }
                }
            });

            JMenuItem maxJunctionCoverageRange = new JMenuItem("Set Max Junction Coverage Range");
            maxJunctionCoverageRange.setToolTipText("The thickness of each line will be proportional to the coverage, up until this value");

            maxJunctionCoverageRange.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    String input = JOptionPane.showInputDialog("Set Max Junction Coverage", getRenderer(trackComponent.track).getMaxDepth());
                    if (input == null || input.length() == 0) return;
                    try {
                        int newMaxDepth = Integer.parseInt(input);
                        setMaxCoverageDepth(JunctionTrackMouseAdapter.this.trackComponent, newMaxDepth);
                    } catch (NumberFormatException ex) {
                        JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer");
                    }
                }
            });

            JMenuItem colorItem = new JMenuItem("Set Color");
            colorItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Color color = UIUtilities.showColorChooserDialog(
                            "Select Track Color", trackComponent.track.getColor());
                    SashimiPlot.this.toFront();
                    if (color == null) return;
                    trackComponent.track.setColor(color);
                    trackComponent.repaint();
                }
            });

            JMenuItem saveImageItem = new JMenuItem("Save Image...");
            saveImageItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    File defaultFile = new File("Sashimi.png");
                    IGV.getInstance().createSnapshot(SashimiPlot.this.getContentPane(), defaultFile);
                }
            });

            menu.add(minJunctionCoverage);
            menu.add(maxJunctionCoverageRange);
            menu.add(colorItem);
            menu.add(saveImageItem);

            return menu;
        }
    }


    private class GeneTrackMouseAdapter extends TrackComponentMouseAdapter<SelectableFeatureTrack> {

        GeneTrackMouseAdapter(TrackComponent<SelectableFeatureTrack> trackComponent) {
            super(trackComponent);
        }

        @Override
        protected void handleDataClick(MouseEvent e) {
            trackComponent.track.handleDataClick(createTrackClickEvent(e));
            Set<IExon> selectedExon = trackComponent.track.getSelectedExons();
            for (SpliceJunctionFinderTrack spliceTrack : spliceJunctionTracks) {
                getRenderer(spliceTrack).setSelectedExons(selectedExon);
            }
            repaint();
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            trackComponent.updateToolTipText(createTrackClickEvent(e));
        }

        @Override
        protected IGVPopupMenu getPopupMenu(MouseEvent e) {
            IGVPopupMenu menu = new IGVPopupMenu();
            TrackMenuUtils.addDisplayModeItems(Arrays.<Track>asList(trackComponent.track), menu);
            menu.addPopupMenuListener(new RepaintPopupMenuListener(SashimiPlot.this));
            return menu;
        }
    }

    private abstract class TrackComponentMouseAdapter<T extends Track> extends MouseAdapter {

        protected TrackComponent<T> trackComponent;
        protected PanTool currentTool;

        TrackComponentMouseAdapter(TrackComponent<T> trackComponent) {
            this.trackComponent = trackComponent;
            currentTool = new PanTool(null);
            currentTool.setReferenceFrame(this.trackComponent.frame);
        }


        @Override
        public void mouseDragged(MouseEvent e) {
            if (currentTool.getLastMousePoint() == null) {
                //This shouldn't happen, but does occasionally
                return;
            }
            double diff = e.getX() - currentTool.getLastMousePoint().getX();
            // diff > 0 means moving mouse to the right, which drags the frame towards the negative direction
            boolean hitBounds = SashimiPlot.this.frame.getOrigin() <= minOrigin && diff > 0;
            hitBounds |= SashimiPlot.this.frame.getEnd() >= maxEnd && diff < 0;
            if (!hitBounds) {
                currentTool.mouseDragged(e);
                repaint();
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (e.isPopupTrigger()) {
                doPopupMenu(e);
            } else {
                currentTool.mouseReleased(e);
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (e.isPopupTrigger()) {
                doPopupMenu(e);
            } else {
                currentTool.mousePressed(e);
                super.mousePressed(e);
            }

        }

        protected void doPopupMenu(MouseEvent e) {
            IGVPopupMenu menu = getPopupMenu(e);
            if (menu != null) menu.show(trackComponent, e.getX(), e.getY());
        }

        protected TrackClickEvent createTrackClickEvent(MouseEvent e) {
            return new TrackClickEvent(e, trackComponent.frame);
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.isPopupTrigger()) {
                doPopupMenu(e);
                return;
            }

            currentTool.mouseClicked(e);
            handleDataClick(e);
        }

        /**
         * Essentially left click
         *
         * @param e
         */
        protected abstract void handleDataClick(MouseEvent e);

        /**
         * Essentially right click
         *
         * @param e
         * @return
         */
        protected abstract IGVPopupMenu getPopupMenu(MouseEvent e);
    }

    /**
     * Show SashimiPlot window, or change settings of {@code currentWindow}
     *
     * @param sashimiPlot
     */
    public static void getSashimiPlot(SashimiPlot sashimiPlot) {
        if (sashimiPlot == null) {
            FeatureTrack geneTrack = null;
            if (IGV.getInstance().getFeatureTracks().size() == 1) {
                geneTrack = IGV.getInstance().getFeatureTracks().get(0);
            } else {
                FeatureTrackSelectionDialog dlg = new FeatureTrackSelectionDialog(IGV.getMainFrame());
                dlg.setTitle("Select Gene Track");
                dlg.setVisible(true);
                if (dlg.getIsCancelled()) return;
                geneTrack = dlg.getSelectedTrack();
            }

            Collection<AlignmentTrack> alignmentTracks = new ArrayList<AlignmentTrack>();
            for (Track track : IGV.getInstance().getAllTracks()) {
                if (track instanceof AlignmentTrack) {
                    alignmentTracks.add((AlignmentTrack) track);
                }
            }

            if (alignmentTracks.size() > 1) {
                TrackSelectionDialog<AlignmentTrack> alDlg = new TrackSelectionDialog<AlignmentTrack>(IGV.getMainFrame(), TrackSelectionDialog.SelectionMode.MULTIPLE, alignmentTracks);
                alDlg.setTitle("Select Alignment Tracks");
                alDlg.setVisible(true);
                if (alDlg.getIsCancelled()) return;

                alignmentTracks = alDlg.getSelectedTracks();
            }

            sashimiPlot = new SashimiPlot(FrameManager.getDefaultFrame(), alignmentTracks, geneTrack);
            //sashimiPlot.setShapeType(shapeType);
            sashimiPlot.setVisible(true);
        } else {
            //sashimiPlot.setShapeType(shapeType);
        }


    }

    private static class RepaintPopupMenuListener implements PopupMenuListener {

        Component component;

        RepaintPopupMenuListener(Component component) {
            this.component = component;
        }

        @Override
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            component.repaint();
        }

        @Override
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            component.repaint();
        }

        @Override
        public void popupMenuCanceled(PopupMenuEvent e) {
            component.repaint();
        }
    }

    private static class Axis extends JComponent {

        private ReferenceFrame frame;

        Axis(ReferenceFrame frame) {
            this.frame = frame;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Rectangle visibleRect = getVisibleRect();
            RenderContext context = new RenderContextImpl(this, (Graphics2D) g, frame, visibleRect);
            drawGenomicAxis(context, visibleRect);
        }

        /**
         * Draw axis displaying genomic coordinates
         *
         * @param context
         * @param trackRectangle
         */
        private void drawGenomicAxis(RenderContext context, Rectangle trackRectangle) {
            int numTicks = 4;
            int ticHeight = 5;

            double pixelPadding = trackRectangle.getWidth() / 20;
            int yLoc = ticHeight + 1;

            double origin = context.getOrigin();
            double locScale = context.getScale();

            //Pixel start/end positions of ruler
            double startPix = trackRectangle.getX() + pixelPadding;
            double endPix = trackRectangle.getMaxX() - pixelPadding;

            double ticIntervalPix = (endPix - startPix) / (numTicks - 1);
            double ticIntervalCoord = locScale * ticIntervalPix;

            int startCoord = (int) (origin + (locScale * startPix));

            Graphics2D g2D = context.getGraphic2DForColor(Color.black);

            g2D.drawLine((int) startPix, yLoc, (int) endPix, yLoc);

            for (int tic = 0; tic < numTicks; tic++) {
                int xLoc = (int) (startPix + tic * ticIntervalPix);
                g2D.drawLine(xLoc, yLoc, xLoc, yLoc - ticHeight);

                int ticCoord = (int) (startCoord + tic * ticIntervalCoord);
                String text = "" + ticCoord;
                Rectangle2D textBounds = g2D.getFontMetrics().getStringBounds(text, g2D);
                g2D.drawString(text, (int) (xLoc - textBounds.getWidth() / 2), (int) (yLoc + textBounds.getHeight()));
            }


        }
    }
}
