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

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

import com.jidesoft.swing.JidePopupMenu;
import org.apache.log4j.Logger;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.FeatureUtils;
import org.broad.igv.renderer.GraphicUtils;
import org.broad.igv.renderer.Renderer;
import org.broad.igv.session.ViewContext;
import org.broad.igv.tdf.TDFDataSource;
import org.broad.igv.tdf.TDFReader;
import org.broad.igv.track.*;
import org.broad.igv.ui.IGVMainFrame;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.panel.DragEvent;
import org.broad.igv.ui.panel.DragListener;
import org.broad.igv.ui.util.FileChooserDialog;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.ui.util.UIUtilities;
import org.broad.igv.util.ColorUtilities;
import org.broad.igv.util.ResourceLocator;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.*;
import java.util.List;

/**
 * @author jrobinso
 */
public class AlignmentTrack extends AbstractTrack implements DragListener {

    public static final int MIN_ALIGNMENT_SPACING = 10;
    static final ColorOption DEFAULT_COLOR_OPTION = ColorOption.INSERT_SIZE;
    static final boolean DEFAULT_SHOWALLBASES = false;

    private static ColorOption colorByOption = null;

    SequenceTrack sequenceTrack;


    private CoverageTrack coverageTrack;

    RenderOptions renderOptions;

    private static Logger log = Logger.getLogger(AlignmentTrack.class);
    private int expandedHeight = 14;
    private int collapsedHeight = 2;
    FeatureRenderer renderer;
    double minVisibleScale = 25;
    Rectangle renderedRect;
    HashMap<String, Color> selectedReadNames = new HashMap();
    private int minHeight = 100;
    AlignmentDataManager dataManager;

    public CoverageTrack getCoverageTrack() {
        return coverageTrack;
    }


    public enum SortOption {
        START, STRAND, NUCELOTIDE, QUALITY, SAMPLE, READ_GROUP
    }

    public enum ColorOption {
        INSERT_SIZE, READ_STRAND, FRAGMENT_STRAND, PAIR_ORIENTATION;
    }

    public static class RenderOptions {
        boolean shadeBases;
        boolean shadeCenters;
        boolean flagUnmappedPairs;
        boolean showAllBases;
        int insertSizeThreshold;
        ColorOption colorOption;

        RenderOptions() {
            PreferenceManager.SAMPreferences prefs = PreferenceManager.getInstance().getSAMPreferences();
            shadeBases = prefs.isShadeBaseQuality();
            shadeCenters = prefs.isShadeCenter();
            flagUnmappedPairs = prefs.isFlagUnmappedPair();
            insertSizeThreshold = prefs.getInsertSizeThreshold();
            showAllBases = DEFAULT_SHOWALLBASES;
            colorOption = colorByOption;
        }

        /**
         * Called by session writer.  Return instance variable values as a map of strings.  Used to record current state
         * of object.   Variables with default values are not stored, as it is presumed the user has not changed them.
         *
         * @return
         */
        public Map<String, String> getPersistentState() {
            Map<String, String> attributes = new HashMap();
            PreferenceManager.SAMPreferences prefs = PreferenceManager.getInstance().getSAMPreferences();
            if (shadeBases != prefs.isShadeBaseQuality()) {
                attributes.put("shadeBases", String.valueOf(shadeBases));
            }
            if (shadeCenters != prefs.isShadeCenter()) {
                attributes.put("shadeCenters", String.valueOf(shadeBases));
            }
            if (flagUnmappedPairs != prefs.isFlagUnmappedPair()) {
                attributes.put("flagUnmappedPairs", String.valueOf(flagUnmappedPairs));
            }
            if (insertSizeThreshold != prefs.getInsertSizeThreshold()) {
                attributes.put("insertSizeThreshold", String.valueOf(insertSizeThreshold));
            }
            if (showAllBases != DEFAULT_SHOWALLBASES) {
                attributes.put("showAllBases", String.valueOf(showAllBases));
            }
            if (colorOption != DEFAULT_COLOR_OPTION) {
                attributes.put("colorOption", colorByOption.toString());
            }

            return attributes;
        }

        /**
         * Called by session reader.  Restores state of object.
         *
         * @param attributes
         */
        public void restorePersistentState(Map<String, String> attributes) {

            String value;
            value = attributes.get("insertSizeThreshold");
            if (value != null) {
                insertSizeThreshold = Integer.parseInt(value);
            }
            value = attributes.get("shadeBases");
            if (value != null) {
                shadeBases = Boolean.parseBoolean(value);
            }
            value = attributes.get("shadeCenters");
            if (value != null) {
                shadeCenters = Boolean.parseBoolean(value);
            }
            value = attributes.get("flagUnmappedPairs");
            if (value != null) {
                flagUnmappedPairs = Boolean.parseBoolean(value);
            }
            value = attributes.get("showAllBases");
            if (value != null) {
                showAllBases = Boolean.parseBoolean(value);
            }
            value = attributes.get("colorOption");
            if (value != null) {
                colorOption = ColorOption.valueOf(value);
                colorByOption = colorOption;
            }
        }

    }


    public AlignmentTrack(ResourceLocator locator, AlignmentDataManager dataManager) {
        super(locator);

        //AlignmentQueryReader reader = SamQueryReaderFactory.getReader(locator);
        this.dataManager = dataManager;

        float maxRange = PreferenceManager.getInstance().getSAMPreferences().getMaxVisibleRange();
        minVisibleScale = (maxRange * 1000) / 700;

        renderer = new AlignmentRenderer();
        this.setExpanded(true);

        PreferenceManager.SAMPreferences prefs = PreferenceManager.getInstance().getSAMPreferences();
        if (prefs.isShowRefSequence()) {
            sequenceTrack = new SequenceTrack("Reference");
            sequenceTrack.setHeight(14);
        }

        renderOptions = new RenderOptions();


        if (colorByOption == null) {
            String colorByString = PreferenceManager.getInstance().getSAMPreferences().getColorBy();
            if (colorByString == null) {
                colorByOption = DEFAULT_COLOR_OPTION;
            } else {
                try {
                    colorByOption = ColorOption.valueOf(colorByString);
                }
                catch (Exception e) {
                    log.error("Error setting color option", e);
                    colorByOption = DEFAULT_COLOR_OPTION;

                }
            }
        }
    }

    public void setCoverageTrack(CoverageTrack coverageTrack) {
        this.coverageTrack = coverageTrack;
    }

    public void setRenderer(FeatureRenderer renderer) {
        this.renderer = renderer;
    }

    @Override
    public void setHeight(int preferredHeight) {
        super.setHeight(preferredHeight);
        minHeight = preferredHeight;
    }

    /**
     * Method description
     *
     * @return
     */
    @Override
    public int getHeight() {
        int h = Math.max(minHeight, getNLevels() * (getRowHeight()) + 20);
        return h;
    }

    private int getRowHeight() {
        return isExpanded() ? expandedHeight : collapsedHeight;
    }

    private int getNLevels() {


        return (dataManager.getAlignmentRows() == null ? 1 : dataManager.getAlignmentRows().size());
    }

    @Override
    public int getPreferredHeight() {
        return Math.max(100, getHeight());
    }


    public void render(RenderContext context, Rectangle rect) {


        // Split rects
        int seqHeight = sequenceTrack == null ? 0 : sequenceTrack.getHeight();
        if (seqHeight > 0) {
            Rectangle seqRect = new Rectangle(rect);
            seqRect.height = seqHeight;
            sequenceTrack.render(context, seqRect);
        }

        int gap = (seqHeight > 0 ? seqHeight : 6);

        rect.y += gap;
        rect.height -= gap;
        renderedRect = new Rectangle(rect);

        if (context.getScale() > minVisibleScale) {

            Graphics2D g = context.getGraphic2DForColor(Color.gray);
            GraphicUtils.drawCenteredText("Zoom in to see alignments.", context.getVisibleRect(), g);
            return;
        }

        renderFeatures(context, rect);
    }

    private void renderFeatures(RenderContext context, Rectangle inputRect) {
        try {
            log.debug("Render features");
            final String genomeId = context.getGenomeId();
            final String chr = context.getChr();
            final int start = (int) context.getOrigin();
            final int end = (int) context.getEndLocation() + 1;
            List<AlignmentInterval.Row> tmp = dataManager.getAlignmentRows(genomeId, chr, start, end);

            if (tmp == null) {
                return;
            }

            // Check currently loaded interval.  If out of range null it.  This probably isn't the best place
            // to check this.
            //AlignmentInterval interval = dataManager.getLoadedInterval();
            //if (interval != null &&
            //        (!interval.chr.equals(context.getChr()) ||
            //        interval.getStart() > context.getEndLocation() ||
            //        interval.getEnd() < context.getOrigin())) {
            //    dataManager.setLoadedInterval(null);
            // return;
            //}

            PreferenceManager.SAMPreferences prefs = PreferenceManager.getInstance().getSAMPreferences();
            int maxLevels = prefs.getMaxLevels();

            Rectangle visibleRect = context.getVisibleRect();

            // Divide rectangle into equal height levels
            double y = inputRect.getY();
            double h = expandedHeight;
            if (!isExpanded()) {
                int visHeight = context.getVisibleRect().height;
                collapsedHeight = Math.max(1, Math.min(expandedHeight, visHeight / dataManager.getMaxDepth()));
                h = collapsedHeight;
            }


            int levelNumber = 0;
            // levelList is copied to prevent concurrent modification exception
            //List<AlignmentDataManager.Row> tmp = new ArrayList(dataManager.getAlignmentRows());

            for (AlignmentInterval.Row row : tmp) {

                if ((visibleRect != null && y > visibleRect.getMaxY()) ||
                        levelNumber > maxLevels) {
                    return;
                }

                if (y + h > visibleRect.getY()) {
                    Rectangle rect = new Rectangle(inputRect.x, (int) y, inputRect.width, (int) h);
                    renderOptions.colorOption = colorByOption;
                    renderer.renderAlignments(row.alignments,
                            context,
                            rect,
                            renderOptions,
                            isExpanded(),
                            selectedReadNames);
                }

                y += h;
                levelNumber++;
            }
        } catch (Exception ex) {
            log.error("Error rendering track", ex);
            throw new RuntimeException("Error rendering track ", ex);

        }

    }

    public void reloadData() {
        dataManager.reload();
    }


    /**
     * Sort alignment rows such that alignments that intersect from the
     * center appear left to right by start position
     */
    public void sortRows(SortOption option) {
        dataManager.sortRows(option);
    }

    public void packAlignments() {
        dataManager.packAlignments();
    }

    /**
     * Copy the contents of the popup text to the system clipboard.
     */
    public void copyToClipboard(final MouseEvent e) {
        double location = getViewContext().getChromosomePosition(e.getX());
        double displayLocation = location + 1;
        Alignment alignment = this.getAlignmentAt(displayLocation, e.getY());

        if (alignment != null) {
            StringBuffer buf = new StringBuffer();
            buf.append(alignment.getValueString(location, null).replace("<br>", "\n"));
            buf.append("\n");
            buf.append("Alignment start position = " + alignment.getChr() + ":" + (alignment.getAlignmentStart() + 1));
            buf.append("\n");
            buf.append(alignment.getReadSequence());
            StringSelection stringSelection = new StringSelection(buf.toString());
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            clipboard.setContents(stringSelection, null);
        }

    }

    /**
     * Copy the contents of the popup text to the system clipboard.
     */
    public void gotoMate(final MouseEvent e) {
        double location = getViewContext().getChromosomePosition(e.getX());
        double displayLocation = location + 1;
        Alignment alignment = this.getAlignmentAt(displayLocation, e.getY());

        if (alignment != null) {
            ReadMate mate = alignment.getMate();
            if (mate != null && mate.isMapped()) {
                String chr = mate.mateChr;
                int start = mate.mateStart - 1;
                selectedReadNames.clear();
                selectedReadNames.put(alignment.getReadName(), Color.black);
                ViewContext.getInstance().centerOnLocation(chr, start);
                ViewContext.getInstance().recordHistory();
            }
        }

    }

    public void setStatType(WindowFunction type) {
        // ignored
    }

    public WindowFunction getWindowFunction() {
        return null;
    }

    public void setRendererClass(Class rc) {
        // ignored
    }

    // SamTracks use posA custom renderer, not derived from Renderer

    public Renderer getRenderer() {
        return null;
    }

    public boolean isLogNormalized() {
        return false;
    }

    public float getRegionScore(String chr, int start, int end, int zoom, RegionScoreType type) {
        return 0.0f;
    }

    public String getValueStringAt(
            String chr, double position, int y) {

        Alignment feature = getAlignmentAt(position, y);

        // TODO -- highlight mate

        String tmp = (feature == null) ? null : feature.getValueString(position, getWindowFunction());

        return tmp;
    }

    private Alignment getAlignmentAt(double position, int y) {
        if (dataManager.getAlignmentRows() == null || dataManager.getAlignmentRows().isEmpty()) {
            return null;
        }

        int h = isExpanded() ? expandedHeight : collapsedHeight;
        int levelNumber = (y - renderedRect.y) / h;
        if (levelNumber < 0 || levelNumber >= dataManager.getAlignmentRows().size()) {
            return null;
        }

        AlignmentInterval.Row row = dataManager.getAlignmentRows().get(levelNumber);
        List<Alignment> features = row.alignments;

        // give posA 2 pixel window, otherwise very narrow features will be missed.
        double bpPerPixel = ViewContext.getInstance().getScale();
        double minWidth = 2 * bpPerPixel;    /* * */
        return (Alignment) FeatureUtils.getFeatureAt(position, minWidth, features);

    }

    public void dragStopped(DragEvent evt) {
        // Disabled.  Not sure why we ever thought this was posA good idea
        //if (PreferenceManager.getInstance().getSAMPreferences().isAutosort() &&
        //        ViewContext.getInstance().getScale() < 1) {
        //    sortRows(SortOption.START);
        //    IGVMainFrame.getInstance().repaintDataPanels();
        //}
    }

    private static Alignment getFeatureContaining(
            List<Alignment> features, int right) {

        int leftBounds = 0;
        int rightBounds = features.size() - 1;
        int idx = features.size() / 2;
        int lastIdx = -1;

        while (idx != lastIdx) {
            lastIdx = idx;
            Alignment f = features.get(idx);
            if (f.contains(right)) {
                return f;
            }

            if (f.getStart() > right) {
                rightBounds = idx;
                idx = (leftBounds + idx) / 2;
            } else {
                leftBounds = idx;
                idx = (rightBounds + idx) / 2;

            }

        }
        // Check the extremes 
        if (features.get(0).contains(right)) {
            return features.get(0);
        }

        if (features.get(rightBounds).contains(right)) {
            return features.get(rightBounds);
        }

        return null;
    }

    @Override
    public boolean handleClick(MouseEvent e) {
        if (e.isPopupTrigger()) {
            getPopupMenu(e).show(e.getComponent(), e.getX(), e.getY());
            return true;
        } else {
            if (e.isShiftDown() || e.isAltDown() || (e.getClickCount() > 1)) {
                return super.handleClick(e);
            } else if (e.getButton() == MouseEvent.BUTTON1 &&
                    (UIConstants.IS_MAC && e.isMetaDown() || (!UIConstants.IS_MAC && e.isControlDown()))) {
                double location = getViewContext().getChromosomePosition(e.getX());
                double displayLocation = location + 1;
                Alignment alignment = this.getAlignmentAt(displayLocation, e.getY());
                if (alignment != null) {
                    if (selectedReadNames.containsKey(alignment.getReadName())) {
                        selectedReadNames.remove(alignment.getReadName());
                    } else {
                        Color c = alignment.isPaired() && alignment.getMate() != null && alignment.getMate().isMapped() ? ColorUtilities.randomColor(selectedReadNames.size() + 1) : Color.black;
                        selectedReadNames.put(alignment.getReadName(), c);
                    }
                }
                return true;
            }
        }
        return false;
    }

    public JPopupMenu getPopupMenu(final MouseEvent evt) {

        JPopupMenu popupMenu = new JidePopupMenu();

        JLabel popupTitle = new JLabel("  " + getName(), JLabel.CENTER);

        Font newFont = popupMenu.getFont().deriveFont(Font.BOLD, 12);
        popupTitle.setFont(newFont);
        if (popupTitle != null) {
            popupMenu.add(popupTitle);
        }

        addSortMenuItem(popupMenu);
        addPackMenuItem(popupMenu);
        popupMenu.addSeparator();
        addColorByMenuItem(popupMenu);

        addShadeBaseMenuItem(popupMenu);
        addShadeCentersMenuItem(popupMenu);

        addGoToMate(popupMenu, evt);
        addShowAllBasesMenuItem(popupMenu);
        addInsertSizeMenuItem(popupMenu);
        popupMenu.addSeparator();

        addShowCoverageItem(popupMenu);
        addLoadCoverageDataItem(popupMenu);

        addCopyToClipboardItem(popupMenu, evt);
        popupMenu.addSeparator();

        addSelecteByNameItem(popupMenu);
        popupMenu.addSeparator();

        JLabel trackSettingsHeading = new JLabel("  Track Settings", JLabel.LEFT);
        trackSettingsHeading.setFont(newFont);

        popupMenu.add(trackSettingsHeading);


        Collection<Track> tmp = new ArrayList();
        tmp.add(this);
        popupMenu.add(TrackMenuUtils.getTrackRenameItem(tmp));

        popupMenu.add(TrackMenuUtils.getExpandCollapseItem(tmp));

        popupMenu.add(TrackMenuUtils.getRemoveMenuItem(tmp));

        popupMenu.addSeparator();
        addClearSelectionsMenuItem(popupMenu);

        return popupMenu;
    }

    public void addSelecteByNameItem(JPopupMenu menu) {
        // Change track height by attribute
        JMenuItem item = new JMenuItem("Select by name...");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                String val = MessageUtils.showInputDialog("Enter read name: ");
                if (val != null && val.trim().length() > 0) {
                    selectedReadNames.put(val, ColorUtilities.randomColor(selectedReadNames.size() + 1));
                    refresh();
                }
            }
        });

        menu.add(item);
    }

    public void addSortMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        JMenu item = new JMenu("Sort alignments");

        JMenuItem m1 = new JMenuItem("by start location");
        m1.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.START);
                refresh();

            }
        });

        JMenuItem m2 = new JMenuItem("by strand");
        m2.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.STRAND);
                refresh();

            }
        });

        JMenuItem m3 = new JMenuItem("by base");
        m3.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {

                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.NUCELOTIDE);
                refresh();

            }
        });

        JMenuItem m4 = new JMenuItem("by mapping quality");
        m4.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {

                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.QUALITY);
                refresh();

            }
        });

        JMenuItem m5 = new JMenuItem("by sample");
        m5.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {

                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.SAMPLE);
                refresh();

            }
        });

        JMenuItem m6 = new JMenuItem("by read group");
        m6.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {

                IGVMainFrame.getInstance().getTrackManager().sortAlignmentTracks(SortOption.READ_GROUP);
                refresh();

            }
        });

        if (ViewContext.getInstance().getScale() >= MIN_ALIGNMENT_SPACING) {
            item.setEnabled(false);
        }


        item.add(m1);
        item.add(m2);
        item.add(m3);
        item.add(m4);
        item.add(m5);
        item.add(m6);
        menu.add(item);
    }


    private void setColorOption(ColorOption option) {
        colorByOption = option;
        PreferenceManager.getInstance().put(PreferenceManager.SAMPreferences.COLOR_BY, option.toString());
        PreferenceManager.getInstance().updateSAMPreferences();
    }

    public void addColorByMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        JMenu item = new JMenu("Color alignments");

        ButtonGroup group = new ButtonGroup();

        JRadioButtonMenuItem m1 = new JRadioButtonMenuItem("by insert size");
        m1.setSelected(colorByOption == ColorOption.INSERT_SIZE);
        m1.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                setColorOption(ColorOption.INSERT_SIZE);
                refresh();
            }
        });

        JRadioButtonMenuItem m1a = new JRadioButtonMenuItem("by pair orientation");
        m1a.setSelected(colorByOption == ColorOption.PAIR_ORIENTATION);
        m1a.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                setColorOption(ColorOption.PAIR_ORIENTATION);
                refresh();
            }
        });

        JRadioButtonMenuItem m2 = new JRadioButtonMenuItem("by read strand");
        m2.setSelected(colorByOption == ColorOption.READ_STRAND);
        m2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                setColorOption(ColorOption.READ_STRAND);
                refresh();
            }
        });

        JRadioButtonMenuItem m3 = new JRadioButtonMenuItem("by first-in-pair read strand");
        m3.setSelected(colorByOption == ColorOption.FRAGMENT_STRAND);
        m3.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                setColorOption(ColorOption.FRAGMENT_STRAND);
                refresh();
            }
        });

        item.add(m1);
        item.add(m1a);
        item.add(m2);
        item.add(m3);
        group.add(m1);
        group.add(m1a);
        group.add(m2);
        group.add(m3);
        menu.add(item);
    }


    public void addPackMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        JMenuItem item = new JMenuItem("Re-pack alignments");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        IGVMainFrame.getInstance().getTrackManager().packAlignmentTracks();
                        refresh();
                    }
                });
            }
        });

        menu.add(item);
    }

    public void addCopyToClipboardItem(JPopupMenu menu, final MouseEvent me) {
        // Change track height by attribute
        JMenuItem item = new JMenuItem("Copy read details to clipboard");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        copyToClipboard(me);
                    }
                });
            }
        });
        if (ViewContext.getInstance().getScale() >= MIN_ALIGNMENT_SPACING) {
            item.setEnabled(false);
        }

        menu.add(item);
    }

    public void addGoToMate(JPopupMenu menu, final MouseEvent me) {
        // Change track height by attribute
        JMenuItem item = new JMenuItem("Go to mate");
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                gotoMate(me);
            }
        });


        menu.add(item);
    }

    public void addClearSelectionsMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        JMenuItem item = new JMenuItem("Clear selections");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        AlignmentTrack.this.selectedReadNames.clear();
                        refresh();
                    }
                });
            }
        });

        menu.add(item);
    }

    public void addShowAllBasesMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Show all bases");
        item.setSelected(renderOptions.showAllBases);
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        renderOptions.showAllBases = item.isSelected();
                        refresh();
                    }
                });
            }
        });

        menu.add(item);
    }

    public void addInsertSizeMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Set insert size threshold ...");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                int threshold = renderOptions.insertSizeThreshold;
                String val = MessageUtils.showInputDialog("insert size threshold", String.valueOf(threshold));
                try {
                    int newThreshold = Integer.parseInt(val);
                    if (newThreshold != threshold) {
                        renderOptions.insertSizeThreshold = newThreshold;
                        refresh();
                    }
                }
                catch (NumberFormatException ex) {
                    MessageUtils.showMessage("Insert size must be an integer value: " + val);
                }

            }
        });


        menu.add(item);
    }

    private void refresh() {
        IGVMainFrame.getInstance().repaintDataPanels();
    }

    public void addShadeBaseMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Shade base by quality");
        item.setSelected(renderOptions.shadeBases);
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        renderOptions.shadeBases = item.isSelected();
                        refresh();
                    }
                });
            }
        });

        menu.add(item);
    }

    public void addShadeCentersMenuItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Shade alignments intersecting center");
        item.setSelected(renderOptions.shadeCenters);
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {

                        renderOptions.shadeCenters = item.isSelected();
                        refresh();
                    }
                });
            }
        });

        menu.add(item);
    }


    public void addShowCoverageItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Show coverage track");
        item.setSelected(getCoverageTrack() != null && getCoverageTrack().isVisible());
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {
                        if (getCoverageTrack() != null) {
                            getCoverageTrack().setVisible(item.isSelected());
                            refresh();
                            IGVMainFrame.getInstance().repaintNamePanels();
                        }
                    }
                });
            }
        });

        menu.add(item);
    }

    public void addLoadCoverageDataItem(JPopupMenu menu) {
        // Change track height by attribute
        final JMenuItem item = new JCheckBoxMenuItem("Load coverage data...");
        item.addActionListener(new ActionListener() {

            public void actionPerformed(ActionEvent e) {
                UIUtilities.invokeOnEventThread(new Runnable() {

                    public void run() {

                        FileChooserDialog trackFileDialog = IGVMainFrame.getInstance().getTrackFileChooser();
                        trackFileDialog.setMultiSelectionEnabled(false);
                        trackFileDialog.setVisible(true);
                        if (!trackFileDialog.isCanceled()) {
                            File file = trackFileDialog.getSelectedFile();
                            String path = file.getAbsolutePath();
                            if (path.endsWith(".tdf") || path.endsWith(".tdf")) {

                                TDFReader reader = TDFReader.getReader(file.getAbsolutePath());
                                TDFDataSource ds = new TDFDataSource(reader, 0, getName() + " coverage");
                                getCoverageTrack().setDataSource(ds);
                                refresh();
                            } else {
                                MessageUtils.showMessage("Coverage data must be in .tdf format");
                            }
                        }
                    }
                });
            }
        });

        menu.add(item);
    }

    @Override
    public Map<String, String> getPersistentState() {
        Map<String, String> attrs = super.getPersistentState();
        attrs.putAll(renderOptions.getPersistentState());
        return attrs;
    }

    @Override
    public void restorePersistentState(Map<String, String> attributes) {
        super.restorePersistentState(attributes);    //To change body of overridden methods use File | Settings | File Templates.
        renderOptions.restorePersistentState(attributes);
    }
}
