/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.maf;

import org.broad.igv.logging.*;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.renderer.ContinuousColorScale;
import org.broad.igv.renderer.GraphicUtils;
import org.broad.igv.track.*;
import org.broad.igv.ui.FontManager;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.panel.IGVPopupMenu;
import org.broad.igv.ui.panel.ReferenceFrame;
import org.broad.igv.util.ResourceLocator;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.*;
import java.util.List;

/**
 * @author jrobinso
 */
public class MultipleAlignmentTrack extends AbstractTrack {

    public static final int margin = 5;
    private static Logger log = LogManager.getLogger(MultipleAlignmentTrack.class);
    private static int EXPANDED_HEIGHT = 14;
    private static int GAPS_HEIGHT = 25;

    Genome genome;

    private String refId;
    private List<String> selectedSpecies;

    //private Map<String, String> speciesNames;

    MAFRenderer renderer = new MAFRenderer();
    Rectangle visibleNameRect;


    private int tileSize = 500;

    //MafChunk currentChunk


    MAFReader reader;

    MAFCache loadedAlignments;


    /**
     * Map of chr alias -> "true" chromosome names.
     */
    private HashMap<String, String> chrMappings;

    public MultipleAlignmentTrack() {
    }

    public MultipleAlignmentTrack(final ResourceLocator locator, Genome genome) throws IOException {
        super(locator);
        this.genome = genome;


        if (locator.getPath().endsWith(".maf.dict")) {
            reader = new MAFListReader(locator.getPath());
            //        speciesNames.put(genome.getId(), genome.getDisplayName());

        } else {

            MAFParser parser = new MAFParser(locator.getPath()); //  new MAFLocalReader(locator.getPath());
            String trackName = parser.getTrackName();
            if (trackName != null) {
                setName(trackName);
            }
            reader = parser;
        }

        refId = reader.getRefId();
        Collection<String> mafChrNames = getChrNames();
        if (mafChrNames != null) {
            chrMappings = new HashMap();
            for (String mafChr : mafChrNames) {
                String chr = genome.getCanonicalChrName(mafChr);
                chrMappings.put(chr, mafChr);
            }
        }

        selectedSpecies = new ArrayList<String>();
        selectedSpecies.addAll(reader.getSpecies());
    }


    public List<String> getSelectedSpecies() {
        return selectedSpecies;
    }

    /**
     * @param selectedSpecies the selectedSpecies to set
     */
    public void setSelectedSpecies(List<String> selectedSpecies) {
        this.selectedSpecies = selectedSpecies;
    }

    public String getSpeciesName(String speciesId) {
        String name = reader.getSpeciesName(speciesId);
        return name == null ? speciesId : name;
    }


    @Override
    public int getHeight() {
        return GAPS_HEIGHT + (getSelectedSpecies().size() + 1) * EXPANDED_HEIGHT;
    }

    @Override
    public void renderName(Graphics2D g2D, Rectangle trackRectangle, Rectangle visibleRectangle) {

        this.visibleNameRect = trackRectangle;
        if (isSelected()) {
            g2D.setBackground(Color.LIGHT_GRAY);
        } else {
            g2D.setBackground(Color.WHITE);
        }

        Rectangle rect = new Rectangle(trackRectangle);
        g2D.clearRect(rect.x, rect.y, rect.width, rect.height);

        Font font = FontManager.getFont(fontSize);
        g2D.setFont(font);

        int y = trackRectangle.y;

        rect.height = GAPS_HEIGHT;
        rect.y = y;
        //text, rect.x + rect.width - margin, yPos, g2D

        GraphicUtils.drawVerticallyCenteredText("Gaps", margin, rect, g2D, true);
        rect.y += rect.height;

        rect.height = EXPANDED_HEIGHT;

        for (String sp : getSelectedSpecies()) {

            String name = getSpeciesName(sp);
            if (name == null) {
                name = sp;
            }

            if (visibleRectangle.intersects(rect)) {

                GraphicUtils.drawVerticallyCenteredText(name, margin, rect, g2D, true);
            }
            rect.y += rect.height;
        }

    }

    public void setColorScale(ContinuousColorScale colorScale) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public boolean isReadyToPaint(ReferenceFrame frame) {

        if (frame.getScale() > 1) return true;   // Zoomed out, nothing to pain

        String chr = frame.getChrName();
        int start = Math.max(0, (int) frame.getOrigin());
        int end = (int) frame.getEnd();
        return loadedAlignments != null && loadedAlignments.contains(chr, start, end);
    }

    @Override
    public void load(ReferenceFrame frame) {
        String chr = frame.getChrName();
        double origin = frame.getOrigin();
        double w = frame.getEnd() - origin;

        // Get some buffer (+/- 1/2 screen)
        String mafChr = chrMappings != null && chrMappings.containsKey(chr) ? chrMappings.get(chr) : chr;
        int start = (int) Math.max(0, origin - w);
        int end = (int) (origin + 2 * w);

        List<MultipleAlignmentBlock> alignments = null;
        try {
            alignments = reader.loadAlignments(mafChr, start, end);
        } catch (IOException e) {
            log.error(e);
            throw new RuntimeException(e);
        }

        loadedAlignments = new MAFCache(chr, start, end, alignments);
    }

    public void render(RenderContext context, Rectangle rect) {

        double locScale = context.getScale();

        if (locScale > 10) {
            Rectangle r = new Rectangle(rect);
            if (visibleNameRect != null) {
                r.y = visibleNameRect.y;
                r.height = visibleNameRect.height;
            }

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

        }

        ReferenceFrame frame = context.getReferenceFrame();
        String chr = frame.getChrName();
        int start = (int) frame.getOrigin();
        int end = (int) frame.getEnd();

        List<MultipleAlignmentBlock> alignments = null;
        if (loadedAlignments != null && loadedAlignments.overlaps(chr, start, end)) {
            alignments = loadedAlignments.getAlignments();
            if (alignments != null) {
                for (MultipleAlignmentBlock ma : alignments) {
                    renderAlignment(context, rect, ma);
                }
            }
        }
    }

    private void renderAlignment(RenderContext context, Rectangle trackRectangle, MultipleAlignmentBlock ma) {

        int y = trackRectangle.y;

        MultipleAlignmentBlock.Sequence reference = ma.getRefSequence();
        if (reference == null) {
            return;
        }

        Rectangle rect = new Rectangle(trackRectangle);
        rect.height = GAPS_HEIGHT;
        rect.y = y;
        renderer.renderGaps(ma.getGaps(), context, rect);
        rect.y += rect.height;

        rect.height = EXPANDED_HEIGHT;

        for (String sp : getSelectedSpecies()) {

            MultipleAlignmentBlock.Sequence seq = ma.getSequence(sp);
            if (seq != null) {
                renderer.renderSequence(ma, seq, reference, ma.getGaps(), context, rect, this);
            }

            rect.y += rect.height;

        }

    }

    public boolean isLogNormalized() {
        return false;
    }

    public String getValueStringAt(String chr, double position, int mouseX, int mouseY, ReferenceFrame frame) {
        return "Multiple alignments";
    }

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


    /**
     * Override to return a specialized popup menu
     *
     * @return
     */
    @Override
    public IGVPopupMenu getPopupMenu(TrackClickEvent te) {
        IGVPopupMenu menu = new IGVPopupMenu();


        if (getId().endsWith("hg18.maf.dict") || getId().endsWith("hg19.maf.dict")) {
            menu.addSeparator();
            JMenuItem configTrack = new JMenuItem("Configure track...");
            configTrack.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent actionEvent) {
                    configureTrack();
                }
            });
            menu.add(configTrack);
            menu.addSeparator();
        }

        List<Track> selfAsList = Arrays.asList((Track) this);

        return menu;
    }


    @Override
    public boolean handleDataClick(TrackClickEvent te) {
        MouseEvent e = te.getMouseEvent();
        if (e.isPopupTrigger()) {
            configureTrack();
        } else {
            if (IGV.getInstance().isShowDetailsOnClick()) {
                openTooltipWindow(te);
            }
        }
        return true;
    }


    private void configureTrack() {
        AbstractMultipleAlignmentDialog dialog;
        if (getId().endsWith("hg18.maf.dict") || getId().endsWith("hg19.maf.dict")) {
            dialog = new Multiz44ConfigurationDialog(IGV.getInstance().getMainFrame(), true, this);
        } else {
            dialog = null;
        }
        dialog.setLocationRelativeTo(IGV.getInstance().getMainFrame());
        dialog.addWindowListener(new java.awt.event.WindowAdapter() {

            public void windowClosing(java.awt.event.WindowEvent e) {
                System.exit(0);
            }
        });
        dialog.setVisible(true);


        if (dialog.isCancelled()) {

        } else {
            List<String> selectedSpecies = dialog.getSelectedSpecies();
            setSelectedSpecies(selectedSpecies);
            repaint();
        }
    }

    public Collection<String> getChrNames() {
        return reader.getChrNames();
    }


    static String getKey(String chr, int tileNo) {
        return chr + tileNo;
    }

    static class MAFCache {

        String chr;
        int start;
        int end;
        List<MultipleAlignmentBlock> alignments;

        MAFCache(String chr, int start, int end, List<MultipleAlignmentBlock> alignments) {
            this.chr = chr;
            this.start = start;
            this.end = end;
            this.alignments = alignments;
        }

        boolean contains(String chr, int start, int end) {
            return this.start <= start && this.end >= end && this.chr.equals(chr);
        }
        boolean overlaps(String chr, int start, int end) {
            return this.start <= end && this.end >= start && this.chr.equals(chr);
        }

        public List<MultipleAlignmentBlock> getAlignments() {
            return alignments;
        }
    }

}

