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

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.renderer;

import org.apache.log4j.Logger;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.AminoAcid;
import org.broad.igv.feature.AminoAcidManager;
import org.broad.igv.feature.AminoAcidSequence;
import org.broad.igv.feature.Strand;
import org.broad.igv.feature.genome.Genome;
import org.broad.igv.feature.genome.GenomeManager;
import org.broad.igv.track.RenderContext;
import org.broad.igv.ui.FontManager;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.util.SOLIDUtils;

import java.awt.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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

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

    //Maximum scale at which the track is displayed
    //public static final int MAX_SCALE_FOR_RENDER = 1000;
    public static final int AMINO_ACID_RESOLUTION = 5;
    static Map<Character, Color> nucleotideColors = new HashMap();


    static {
        //nucleotideColors.put('A', new Color(160, 160, 255));
        //nucleotideColors.put('C', new Color(255, 140, 75));
        //nucleotideColors.put('T', new Color(160, 255, 255));
        //nucleotideColors.put('G', new Color(255, 112, 112));
        //nucleotideColors.put('N', Color.gray);    
        nucleotideColors.put('A', new Color(0, 150, 10));
        nucleotideColors.put('a', new Color(0, 150, 10));
        nucleotideColors.put('C', Color.BLUE);
        nucleotideColors.put('c', Color.BLUE);
        nucleotideColors.put('T', Color.RED);
        nucleotideColors.put('t', Color.RED);
        nucleotideColors.put('G', new Color(209, 113, AMINO_ACID_RESOLUTION));
        nucleotideColors.put('g', new Color(209, 113, AMINO_ACID_RESOLUTION));
        nucleotideColors.put('N', Color.gray);
        nucleotideColors.put('n', Color.gray);
    }

    protected TranslatedSequenceDrawer translatedSequenceDrawer;

    //are we rendering positive or negative strand?
    protected Strand strand = Strand.POSITIVE;

    //Have we successfully downloaded sequence info?
    private boolean hasSequence = true;

    public SequenceRenderer() {
        translatedSequenceDrawer = new TranslatedSequenceDrawer();
    }

    /**
     * @param context
     * @param trackRectangle
     * @param showColorSpace
     * @param showTranslation Should we show the translated amino acids?
     */
    public void draw(RenderContext context, Rectangle trackRectangle,
                     boolean showColorSpace, boolean showTranslation,
                     int resolutionThreshold) {


        if (context.getScale() >= resolutionThreshold) {
            // Zoomed out too far to see sequences.  This can happen when in gene list view and one of the frames
            // is zoomed in but others are not
            context.getGraphic2DForColor(UIConstants.LIGHT_GREY).fill(trackRectangle);

        } else {
            double locScale = context.getScale();
            double origin = context.getOrigin();
            String chr = context.getChr();
            //String genomeId = context.getGenomeId();
            Genome genome = GenomeManager.getInstance().getCurrentGenome();

            //The location of the first base that is loaded, which may include padding around what's visible
            int start = Math.max(0, (int) origin - 1);
            //The location of the last base that is loaded
            int end = (int) (origin + trackRectangle.width * locScale) + 1;

            if (end <= start) return;

            int firstCodonOffset = 0;
            int lastCodonOffset = 0;


            //If we're translating, we need to start with the first bp of the first codon, in frame 3, and
            //end with the last bp of the last codon, in frame 1
            if (showTranslation) {
                if (start > 1) {
                    firstCodonOffset = 2;
                    start -= firstCodonOffset;
                }

                lastCodonOffset = 2;
                end += lastCodonOffset;
            }

            byte[] seq = genome.getSequence(chr, start, end);
            if (seq == null) {
                log.error("Unable to get sequence at " + chr + ":" + start + "-" + end);
                this.hasSequence = false;
                return;
            } else {
                this.hasSequence = true;
            }

            //The combined height of sequence and (optionally) colorspace bands
            int untranslatedSequenceHeight = (int) trackRectangle.getHeight();


            if (showTranslation) {
                untranslatedSequenceHeight = showColorSpace ? (int) trackRectangle.getHeight() / AMINO_ACID_RESOLUTION * 2 :
                        (int) (trackRectangle.getHeight() / 4);
                // Draw translated sequence
                Rectangle translatedSequenceRect = new Rectangle(trackRectangle.x, trackRectangle.y + untranslatedSequenceHeight,
                        (int) trackRectangle.getWidth(), (int) trackRectangle.getHeight() - untranslatedSequenceHeight);
                if (context.getScale() < AMINO_ACID_RESOLUTION) {
                    translatedSequenceDrawer.draw(context, start, translatedSequenceRect, seq, strand);
                }
            }

            //Rectangle containing the sequence and (optionally) colorspace bands
            Rectangle untranslatedSequenceRect = new Rectangle(trackRectangle.x, trackRectangle.y,
                    (int) trackRectangle.getWidth(), untranslatedSequenceHeight);


            byte[] seqCS = null;
            if (showColorSpace) {
                seqCS = SOLIDUtils.convertToColorSpace(seq);
            }

            if (seq != null && seq.length > 0) {
                int hCS = (showColorSpace ? untranslatedSequenceRect.height / 2 : 0);
                int yBase = hCS + untranslatedSequenceRect.y + 2;
                int yCS = untranslatedSequenceRect.y + 2;
                int dY = (showColorSpace ? hCS : untranslatedSequenceRect.height) - 4;
                int dX = (int) (1.0 / locScale);
                // Create a graphics to use
                Graphics2D g = (Graphics2D) context.getGraphics().create();
                if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.ENABLE_ANTIALISING)) {
                    g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                }

                //dhmay adding check for adequate track height
                int fontSize = Math.min(untranslatedSequenceRect.height, Math.min(dX, 12));
                if (fontSize >= 8) {
                    Font f = FontManager.getFont(Font.BOLD, fontSize);
                    g.setFont(f);
                }

                // Loop through base pair coordinates
                int firstVisibleNucleotideStart = start;
                int lastVisibleNucleotideEnd = Math.min(end, seq.length + start);
                int lastPx0 = -1;
                int scale = Math.max(1, (int) context.getScale());
                for (int loc = firstVisibleNucleotideStart; loc < lastVisibleNucleotideEnd; loc += scale) {
                    for (; loc < lastVisibleNucleotideEnd; loc++) {
                        int idx = loc - start;
                        int pX0 = (int) ((loc - origin) / locScale);
                        if (pX0 > lastPx0) {
                            lastPx0 = pX0;
                            char c = (char) seq[idx];
                            if (Strand.NEGATIVE.equals(strand))
                                c = complementChar(c);
                            Color color = nucleotideColors.get(c);
                            if (fontSize >= 8) {
                                if (color == null) {
                                    color = Color.black;
                                }
                                g.setColor(color);
                                drawCenteredText(g, new char[]{c}, pX0, yBase + 2, dX, dY - 2);
                                if (showColorSpace) {
                                    // draw color space #.  Color space is shifted to be between bases as it represents
                                    // two bases.
                                    g.setColor(Color.black);
                                    String cCS = String.valueOf(seqCS[idx]);
                                    drawCenteredText(g, cCS.toCharArray(), pX0 - dX / 2, yCS + 2, dX, dY - 2);
                                }
                            } else {
                                int bw = Math.max(1, dX - 1);
                                if (color != null) {
                                    g.setColor(color);
                                    g.fillRect(pX0, yBase, bw, dY);
                                }
                            }
                        }
                        break;
                    }
                }
            }
        }
    }

    /**
     * Return the complement of a nucleotide or ambiguity code
     *
     * @param inputChar
     * @return
     */
    protected char complementChar(char inputChar) {
        switch (inputChar) {
            case 'A':
                return 'T';
            case 'T':
            case 'U':
                return 'A';
            case 'G':
                return 'C';
            case 'C':
                return 'G';
            case 'M':
                return 'K';
            case 'R':
                return 'Y';
            case 'Y':
                return 'R';
            case 'K':
                return 'M';
            case 'V':
                return 'B';
            case 'H':
                return 'D';
            case 'D':
                return 'H';
            case 'B':
                return 'V';

            case 'a':
                return 't';
            case 't':
            case 'u':
                return 'a';
            case 'g':
                return 'c';
            case 'c':
                return 'g';
            case 'm':
                return 'k';
            case 'r':
                return 'y';
            case 'y':
                return 'r';
            case 'k':
                return 'm';
            case 'v':
                return 'b';
            case 'h':
                return 'd';
            case 'd':
                return 'h';
            case 'b':
                return 'v';

            default:
                return inputChar;
        }
    }

    private void drawCenteredText(Graphics2D g, char[] chars, int x, int y, int w, int h) {
        // Get measures needed to center the message
        FontMetrics fm = g.getFontMetrics();

        // How many pixels wide is the string
        int msg_width = fm.charsWidth(chars, 0, 1);

        // How far above the baseline can the font go?
        int ascent = fm.getMaxAscent();

        // How far below the baseline?
        int descent = fm.getMaxDescent();

        // Use the string width to find the starting point
        int msgX = x + w / 2 - msg_width / 2;

        // Use the vertical height of this font to find
        // the vertical starting coordinate
        int msgY = y + h / 2 - descent / 2 + ascent / 2;

        g.drawChars(chars, 0, 1, msgX, msgY);

    }

    public Strand getStrand() {
        return strand;
    }

    public void setStrand(Strand strand) {
        this.strand = strand;
    }

    public boolean hasSequence() {
        return this.hasSequence;
    }


    /**
     * @author Damon May
     *         This class draws three amino acid bands representing the 3-frame translation of one strand
     *         of the associated SequenceTrack
     */
    public static class TranslatedSequenceDrawer {

        public static final int HEIGHT_PER_BAND = 14;
        public static final int TOTAL_HEIGHT = 3 * HEIGHT_PER_BAND;

        //alternating colors for aminoacids
        public static final Color AA_COLOR_1 = new Color(128, 128, 128);
        public static final Color AA_COLOR_2 = new Color(170, 170, 170);

        public static final Color AA_FONT_COLOR = Color.WHITE;

        //minimum font size for drawing AA characters
        public static final int MIN_FONT_SIZE = 6;
        //minimum vertical buffer around AA characters in band
        public static final int MIN_FONT_VBUFFER = 1;
        //ideal vertical buffer around AA characters in band
        public static final int IDEAL_FONT_VBUFFER = 2;

        protected static final Color STOP_CODON_COLOR = Color.RED;
        protected static final Color METHIONINE_COLOR = Color.GREEN;

        protected static final Color NUCLEOTIDE_SEPARATOR_COLOR = new Color(150, 150, 150, 120);

        /**
         * @param context
         * @param start          Must be the first base involved in any codon that's even partially visible
         * @param trackRectangle
         * @param seq
         */
        public void draw(RenderContext context, int start, Rectangle trackRectangle, byte[] seq, Strand strand) {
            //each band gets 1/3 of the height, rounded
            int idealHeightPerBand = trackRectangle.height / 3;
            //In this situation, band height is more equal if we tweak things a bit
            if (trackRectangle.height % 3 == 2)
                idealHeightPerBand++;
            int minHeightPerBand = Math.min(idealHeightPerBand, trackRectangle.height - (2 * idealHeightPerBand));

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

            //Figure out the dimensions of a single box containing an aminoacid, at this zoom
            int oneAcidBoxWidth = getPixelFromChromosomeLocation(context.getChr(), 3, origin, locScale) -
                    getPixelFromChromosomeLocation(context.getChr(), 0, origin, locScale) + 1;
            int oneAcidBoxMinDimension = Math.min(oneAcidBoxWidth, minHeightPerBand);

            //Calculate the font size.  If that's less than MIN_FONT_SIZE, we won't draw amino acids
            int fontSize = 0;
            boolean shouldDrawLetters = false;
            if (oneAcidBoxMinDimension >= 2 * MIN_FONT_VBUFFER + MIN_FONT_SIZE) {
                int idealFontSize = oneAcidBoxMinDimension - 2 * IDEAL_FONT_VBUFFER;

                fontSize = Math.max(idealFontSize, MIN_FONT_SIZE);
                shouldDrawLetters = true;
            }

            boolean shouldDrawNucleotideLines = shouldDrawLetters && oneAcidBoxWidth >= 2.5 * fontSize;

            Rectangle bandRectangle = new Rectangle(trackRectangle.x, 0, trackRectangle.width, 0);
            int heightAlreadyUsed = 0;

            //rf 0
            bandRectangle.y = trackRectangle.y;
            bandRectangle.height = idealHeightPerBand;
            heightAlreadyUsed += bandRectangle.height;

            //This set collects the X positions for nucleotide lines, if we choose to draw them.
            //Technically we could calculate these, but I haven't managed to do that without some wiggle
            Set<Integer> nucleotideLineXPositions = new HashSet<Integer>();

            //only draw nucleotide lines the last time this is called
            drawOneTranslation(context, start, bandRectangle, 0, shouldDrawLetters, fontSize,
                    nucleotideLineXPositions, seq, strand);

            //rf 1
            bandRectangle.y = trackRectangle.y + heightAlreadyUsed;
            bandRectangle.height = idealHeightPerBand;
            heightAlreadyUsed += bandRectangle.height;
            drawOneTranslation(context, start, bandRectangle, 1, shouldDrawLetters, fontSize,
                    nucleotideLineXPositions, seq, strand);

            //rf 2
            bandRectangle.y = trackRectangle.y + heightAlreadyUsed;
            bandRectangle.height = trackRectangle.height - heightAlreadyUsed;
            drawOneTranslation(context, start, bandRectangle, 2, shouldDrawLetters, fontSize,
                    nucleotideLineXPositions, seq, strand);

            if (shouldDrawNucleotideLines) {
                Graphics2D graphicsForNucleotideLines = context.getGraphic2DForColor(NUCLEOTIDE_SEPARATOR_COLOR);
                //use a dashed stroke
                graphicsForNucleotideLines.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
                        0, new float[]{1, 2}, 0));

                int topYCoord = trackRectangle.y - 1;
                for (int xVal : nucleotideLineXPositions) {
                    if (xVal >= trackRectangle.x && xVal <= trackRectangle.x + trackRectangle.width)
                        graphicsForNucleotideLines.drawLine(xVal,
                                topYCoord,
                                xVal, topYCoord + trackRectangle.height);
                }
            }
        }

        /**
         * Draw the band representing a translation of the sequence in one reading frame
         *
         * @param context
         * @param start                    the index of the first base in seq.  Should be the first nucleotide that's in a codon
         *                                 that's even partially visible, in any frame
         * @param bandRectangle
         * @param readingFrame
         * @param shouldDrawLetters
         * @param fontSize
         * @param nucleotideLineXPositions a Set that will accrue all of the x positions that we define here
         * @param seq                      nucleotide sequence starting at start
         *                                 for the beginning and end of aminoacid boxes
         */
        protected void drawOneTranslation(RenderContext context, int start,
                                          Rectangle bandRectangle, int readingFrame,
                                          boolean shouldDrawLetters, int fontSize,
                                          Set<Integer> nucleotideLineXPositions, byte[] seq,
                                          Strand strand) {

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

            Graphics2D fontGraphics = (Graphics2D) context.getGraphic2DForColor(AA_FONT_COLOR).create();
            if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.ENABLE_ANTIALISING)) {
                fontGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            }


            //The start location of the first codon that overlaps this region
            int readingFrameOfFullSeq = start % 3;
            int indexOfFirstCodonStart = readingFrame - readingFrameOfFullSeq;
            if (indexOfFirstCodonStart < 0)
                indexOfFirstCodonStart += 3;

            if (seq != null && seq.length > 0) {
                Graphics2D g = (Graphics2D) context.getGraphics().create();
                if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.ENABLE_ANTIALISING)) {
                    g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
                }

                String nucSequence = new String(seq, indexOfFirstCodonStart, seq.length - indexOfFirstCodonStart);
                AminoAcidSequence aaSequence = AminoAcidManager.getInstance().
                        getAminoAcidSequence(strand, start + indexOfFirstCodonStart, nucSequence);

                if ((aaSequence != null) && aaSequence.hasNonNullSequence()) {
                    //This rectangle holds a single AA glyph. x and width will be updated in the for loop
                    Rectangle aaRect = new Rectangle(0, bandRectangle.y, 1, bandRectangle.height);

                    //start position for this amino acid. Will increment in for loop below
                    int aaSeqStartPosition = aaSequence.getStartPosition();

                    //calculated oddness or evenness of first amino acid
                    int firstFullAcidIndex = (int) Math.floor((aaSeqStartPosition - readingFrame) / 3);
                    boolean odd = (firstFullAcidIndex % 2) == 1;

                    if (shouldDrawLetters) {
                        Font f = FontManager.getFont(Font.BOLD, fontSize);
                        g.setFont(f);
                    }

                    for (AminoAcid acid : aaSequence.getSequence()) {
                        if (acid != null) {
                            //calculate x pixel boundaries of this AA rectangle
                            int px = getPixelFromChromosomeLocation(context.getChr(), aaSeqStartPosition, origin, locScale);
                            int px2 = getPixelFromChromosomeLocation(context.getChr(), aaSeqStartPosition + 3,
                                    origin, locScale);

                            //if x boundaries of this AA overlap the band rectangle
                            if ((px <= bandRectangle.getMaxX()) && (px2 >= bandRectangle.getX())) {
                                aaRect.x = px;
                                aaRect.width = px2 - px;

                                nucleotideLineXPositions.add(aaRect.x);
                                nucleotideLineXPositions.add(aaRect.x + aaRect.width);

                                Graphics2D bgGraphics =
                                        context.getGraphic2DForColor(getColorForAminoAcid(acid.getSymbol(), odd));

                                bgGraphics.fill(aaRect);

                                if (shouldDrawLetters) {
                                    String acidString = new String(new char[]{acid.getSymbol()});
                                    GraphicUtils.drawCenteredText(acidString, aaRect, fontGraphics);
                                }
                            }
                            //need to switch oddness whether we displayed the AA or not,
                            //because oddness is calculated from first AA
                            odd = !odd;

                            aaSeqStartPosition += 3;
                        }
                    }
                }
            }
        }

        protected Color getColorForAminoAcid(char acidSymbol, boolean odd) {
            switch (acidSymbol) {
                case 'M':
                    return METHIONINE_COLOR;
                case '*':
                    return STOP_CODON_COLOR;
                default:
                    return odd ? AA_COLOR_1 : AA_COLOR_2;
            }
        }

        protected int getPixelFromChromosomeLocation(String chr, int chromosomeLocation, double origin,
                                                     double locationScale) {
            return (int) Math.round((chromosomeLocation - origin) / locationScale);
        }

        public Color getDefaultColor() {
            return Color.BLACK;
        }


    }
}
