/**
 * Copyright (c) 2010-2011 by Fred Hutchinson Cancer Research Center.  All Rights Reserved.
 * Portions of code, copyright (c) 2013 by F. Hoffmann-La Roche Ltd and Tessella Inc.

 * 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." FRED HUTCHINSON CANCER RESEARCH CENTER MAKES NO
 * REPRESENTATIONS OR WARRANTES 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 FRED HUTCHINSON CANCER RESEARCH
 * CENTER OR ITS 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 FRED HUTCHINSON CANCER RESEARCH CENTER SHALL BE ADVISED,
 * SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 *
 * THE SOFTWARE IS PROVIDED "AS IS." F.HOFFMANN-LA ROCHE AND TESSELLA INC MAKE NO
 * REPRESENTATIONS OR WARRANTES 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 F.HOFFMANN-LA ROCHE AND TESSELLA INC
 * OR ITS 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 F.HOFFMANN-LA ROCHE AND TESSELLA INC SHALL BE ADVISED,
 * SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 */
package org.broad.igv.renderer;

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

import org.apache.commons.math.stat.Frequency;
import org.apache.log4j.Logger;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.IGVFeature;
import org.broad.igv.feature.SpliceJunctionFeature;
import org.broad.igv.feature.Strand;
import org.broad.igv.track.FeatureTrack;
import org.broad.igv.track.RenderContext;
import org.broad.igv.track.Track;
import org.broad.igv.ui.FontManager;

import java.awt.*;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Renderer for splice junctions. Draws a filled-in arc for each junction, with the width of the
 * arc representing the depth of coverage. If coverage information is present for the flanking
 * regions, draws that, too; otherwise indicates flanking regions with rectangles
 *
 * @author dhmay
 */
public class SpliceJunctionRenderer extends IGVFeatureRenderer {

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

    //color for drawing all arcs
    Color ARC_COLOR_NEG = new Color(50, 50, 150, 140); //transparent dull blue
    Color ARC_COLOR_POS = new Color(150, 50, 50, 140); //transparent dull red

    Color ARC_COLOR_HIGHLIGHT_NEG = new Color(90, 90, 255, 255); //opaque, brighter blue
    Color ARC_COLOR_HIGHLIGHT_POS = new Color(255, 90, 90, 255); //opaque, brighter red


    //central horizontal line color
    Color COLOR_CENTERLINE = new Color(0, 0, 0, 100);

    //maximum depth that can be displayed, due to track height limitations. Junctions with
    //this depth and deeper will all look the same
    protected int maxDepth = 50;

    /**
     * Note:  assumption is that featureList is sorted by pStart position.
     *
     * @param featureList
     * @param context
     * @param trackRectangle
     * @param track
     */
    @Override
    public void render(List<IGVFeature> featureList,
                       RenderContext context,
                       Rectangle trackRectangle,
                       Track track) {

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

        // TODO -- use enum instead of string "Color"
        if ((featureList != null) && !featureList.isEmpty()) {

            // Create a graphics object to draw font names.  Graphics are not cached
            // by font, only by color, so its neccessary to create a new one to prevent
            // affecting other tracks.
            Font font = FontManager.getFont(track.getFontSize());
            Graphics2D fontGraphics = (Graphics2D) context.getGraphic2DForColor(Color.BLACK).create();

            if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.ENABLE_ANTIALISING)) {
                fontGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

            }
            fontGraphics.setFont(font);

            //determine whether to show flanking regions
            PreferenceManager prefs = PreferenceManager.getInstance();
            boolean shouldShowFlankingRegions = prefs.getAsBoolean(PreferenceManager.SAM_SHOW_JUNCTION_FLANKINGREGIONS);

            // Track coordinates
            double trackRectangleX = trackRectangle.getX();
            double trackRectangleMaxX = trackRectangle.getMaxX();

 
            SpliceJunctionFeature selectedFeature =
                    (SpliceJunctionFeature) ((FeatureTrack) track).getSelectedFeature();

            // Start of Roche-Tessella modification
            if (track.getAutoScale())    {
                Frequency f = new Frequency();
                List<Integer> scores = new ArrayList<Integer>();

                for (IGVFeature feature : featureList) {
                    SpliceJunctionFeature junctionFeature = (SpliceJunctionFeature) feature;
                    f.addValue(junctionFeature.getScore());
                    scores.add((int) junctionFeature.getScore());
                }

                Collections.sort(scores);
                Collections.reverse(scores);
                for (int s: scores)	{
                    if (f.getCumPct(s) < 0.99)	{
                        maxDepth = s;
                        break;
                    }
                }

            }
            // End of Roche-Tessella modification

            for (IGVFeature feature : featureList) {
                SpliceJunctionFeature junctionFeature = (SpliceJunctionFeature) feature;
                //if same junction as selected feature, highlight
                boolean shouldHighlight = false;
                if (selectedFeature != null && selectedFeature.isSameJunction(junctionFeature)) {
                    setHighlightFeature(junctionFeature);
                    shouldHighlight = true;
                }

                // Get the pStart and pEnd of the entire feature.  at extreme zoom levels the
                // virtual pixel value can be too large for an int, so the computation is
                // done in double precision and cast to an int only when its confirmed its
                // within the field of view.
                int flankingStart = junctionFeature.getStart();
                int flankingEnd = junctionFeature.getEnd();

                int junctionStart = junctionFeature.getJunctionStart();
                int junctionEnd = junctionFeature.getJunctionEnd();

                double virtualPixelStart = Math.round((flankingStart - origin) / locScale);
                double virtualPixelEnd = Math.round((flankingEnd - origin) / locScale);

                double virtualPixelJunctionStart = Math.round((junctionStart - origin) / locScale);
                double virtualPixelJunctionEnd = Math.round((junctionEnd - origin) / locScale);

                // If the any part of the feature fits in the
                // Track rectangle draw it
                if ((virtualPixelEnd >= trackRectangleX) && (virtualPixelStart <= trackRectangleMaxX)) {

                    //
                    int displayPixelEnd = (int) Math.min(trackRectangleMaxX, virtualPixelEnd);
                    int displayPixelStart = (int) Math.max(trackRectangleX, virtualPixelStart);

                    float depth = junctionFeature.getJunctionDepth();
                    Color color = feature.getColor();

                    drawFeature((int) virtualPixelStart, (int) virtualPixelEnd,
                            (int) virtualPixelJunctionStart, (int) virtualPixelJunctionEnd, depth,
                            trackRectangle, context, feature.getStrand(), junctionFeature, shouldHighlight, color,
                            shouldShowFlankingRegions);
                }
            }

            //draw a central horizontal line
            Graphics2D g2D = context.getGraphic2DForColor(COLOR_CENTERLINE);
            g2D.drawLine((int) trackRectangleX, (int) trackRectangle.getCenterY(),
                    (int) trackRectangleMaxX, (int) trackRectangle.getCenterY());

        }
    }


    /**
     * Draw depth of coverage for the starting or ending flanking region
     *
     * @param g2D
     * @param pixelStart
     * @param pixelLength
     * @param regionDepthArray
     * @param maxPossibleArcHeight
     * @param trackRectangle
     * @param isPositiveStrand
     */
    protected void drawFlankingRegion(Graphics g2D, int pixelStart, int pixelLength, int[] regionDepthArray,
                                      int maxPossibleArcHeight, Rectangle trackRectangle, boolean isPositiveStrand) {
        for (int i = 0; i < pixelLength; i++) {
            float arrayIndicesPerPixel = (float) regionDepthArray.length /
                    (float) pixelLength;
            int flankingRegionArrayPixelMinIndex = (int) (i * arrayIndicesPerPixel);
            int flankingRegionArrayPixelMaxIndex = (int) ((i + 1) * arrayIndicesPerPixel);
            flankingRegionArrayPixelMinIndex =
                    Math.max(0, Math.min(flankingRegionArrayPixelMinIndex, regionDepthArray.length - 1));
            flankingRegionArrayPixelMaxIndex =
                    Math.max(0, Math.min(flankingRegionArrayPixelMaxIndex, regionDepthArray.length - 1));

            int meanDepthThisPixel = 0;
            for (int j = flankingRegionArrayPixelMinIndex; j <= flankingRegionArrayPixelMaxIndex; j++)
                meanDepthThisPixel += regionDepthArray[j];
            meanDepthThisPixel /= (flankingRegionArrayPixelMaxIndex - flankingRegionArrayPixelMinIndex + 1);
            meanDepthThisPixel = Math.min(maxDepth, meanDepthThisPixel);
            int pixelHeight = Math.max(maxPossibleArcHeight * meanDepthThisPixel / maxDepth, 2);
            g2D.fillRect(pixelStart + i,
                    (int) trackRectangle.getCenterY() + (isPositiveStrand ? -pixelHeight : 0),
                    1, pixelHeight);
        }
    }

    /**
     * Draw a filled arc representing a single feature. The thickness and height of the arc are proportional to the
     * depth of coverage.  Some of this gets a bit arcane -- the result of lots of visual tweaking.
     *
     * @param pixelFeatureStart  the starting position of the feature, whether on-screen or not
     * @param pixelFeatureEnd    the ending position of the feature, whether on-screen or not
     * @param pixelJunctionStart the starting position of the junction, whether on-screen or not
     * @param pixelJunctionEnd   the ending position of the junction, whether on-screen or not
     * @param depth              coverage depth
     * @param trackRectangle
     * @param context
     * @param strand
     * @param junctionFeature
     * @param shouldHighlight
     * @param featureColor       the color specified for this feature.  May be null.
     */
    protected void drawFeature(int pixelFeatureStart, int pixelFeatureEnd,
                               int pixelJunctionStart, int pixelJunctionEnd, float depth,
                               Rectangle trackRectangle, RenderContext context, Strand strand,
                               SpliceJunctionFeature junctionFeature, boolean shouldHighlight, Color featureColor,
                               boolean shouldShowFlankingRegions) {


        boolean isPositiveStrand = true;
        // Get the feature's direction, color appropriately
        if (strand != null && strand.equals(Strand.NEGATIVE))
            isPositiveStrand = false;

        //If the feature color is specified, use it, except that we set our own alpha depending on whether
        //the feature is highlighted.  Otherwise default based on strand and highlight.
        Color color;
        if (featureColor != null) {
            int r = featureColor.getRed();
            int g = featureColor.getGreen();
            int b = featureColor.getBlue();
            int alpha = shouldHighlight ? 255 : 140;
            color = new Color(r, g, b, alpha);
        } else {
            if (isPositiveStrand)
                color = shouldHighlight ? ARC_COLOR_HIGHLIGHT_POS : ARC_COLOR_POS;
            else
                color = shouldHighlight ? ARC_COLOR_HIGHLIGHT_NEG : ARC_COLOR_NEG;
        }

        Graphics2D g2D = context.getGraphic2DForColor(color);
        if (PreferenceManager.getInstance().getAsBoolean(PreferenceManager.ENABLE_ANTIALISING)) {
            g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        }
        //Height of top of an arc of maximum depth
        int maxPossibleArcHeight = (trackRectangle.height - 1) / 2;

        if (shouldShowFlankingRegions) {
            if (junctionFeature.hasFlankingRegionDepthArrays()) {
                //draw a wigglegram of the splice junction flanking region depth of coverage

                int startFlankingRegionPixelLength = pixelJunctionStart - pixelFeatureStart;
                int endFlankingRegionPixelLength = pixelFeatureEnd - pixelJunctionEnd;

                drawFlankingRegion(g2D, pixelFeatureStart, startFlankingRegionPixelLength,
                        junctionFeature.getStartFlankingRegionDepthArray(), maxPossibleArcHeight,
                        trackRectangle, isPositiveStrand);
                drawFlankingRegion(g2D, pixelJunctionEnd + 1, endFlankingRegionPixelLength,
                        junctionFeature.getEndFlankingRegionDepthArray(), maxPossibleArcHeight,
                        trackRectangle, isPositiveStrand);
            } else {
                //Draw rectangles indicating the overlap on each side of the junction
                int overlapRectHeight = 3;
                int overlapRectTopX = (int) trackRectangle.getCenterY() + (isPositiveStrand ? -2 : 0);
                if (pixelFeatureStart < pixelJunctionStart) {
                    g2D.fillRect(pixelFeatureStart, overlapRectTopX,
                            pixelJunctionStart - pixelFeatureStart, overlapRectHeight);
                }
                if (pixelJunctionEnd < pixelFeatureEnd) {
                    g2D.fillRect(pixelJunctionEnd, overlapRectTopX,
                            pixelFeatureEnd - pixelJunctionEnd, overlapRectHeight);
                }
            }
        }

        //Create a path describing the arc, using Bezier curves. The Bezier control points for the top and
        //bottom arcs are based on the boundary points of the rectangles containing the arcs

        //proportion of the maximum arc height used by a minimum-height arc
        double minArcHeightProportion = 0.33;

        int innerArcHeight = (int) (maxPossibleArcHeight * minArcHeightProportion);
        float depthProportionOfMax = Math.min(1, depth / maxDepth);
        int arcWidth = Math.max(1, (int) ((1 - minArcHeightProportion) * maxPossibleArcHeight * depthProportionOfMax));
        int outerArcHeight = innerArcHeight + arcWidth;


        //Height of bottom of the arc
        int arcBeginY = (int) trackRectangle.getCenterY() +
                (isPositiveStrand ? -1 : 1);
        int outerArcPeakY = isPositiveStrand ?
                arcBeginY - outerArcHeight :
                arcBeginY + outerArcHeight;
        int innerArcPeakY = isPositiveStrand ?
                arcBeginY - innerArcHeight :
                arcBeginY + innerArcHeight;
        //dhmay: I don't really understand Bezier curves.  For some reason I have to put the Bezier control
        //points farther up or down than I want the arcs to extend.  This multiplier seems about right
        int outerBezierY = arcBeginY + (int) (1.3 * (outerArcPeakY - arcBeginY));
        int innerBezierY = arcBeginY + (int) (1.3 * (innerArcPeakY - arcBeginY));

        //Putting the Bezier control points slightly off to the sides of the arc 
        int bezierXPad = Math.max(1, (pixelJunctionEnd - pixelJunctionStart) / 30);

        GeneralPath arcPath = new GeneralPath();
        arcPath.moveTo(pixelJunctionStart, arcBeginY);
        arcPath.curveTo(pixelJunctionStart - bezierXPad, outerBezierY, //Bezier 1
                pixelJunctionEnd + bezierXPad, outerBezierY,         //Bezier 2
                pixelJunctionEnd, arcBeginY);        //Arc end
        arcPath.curveTo(pixelJunctionEnd + bezierXPad, innerBezierY, //Bezier 1
                pixelJunctionStart - bezierXPad, innerBezierY,         //Bezier 2
                pixelJunctionStart, arcBeginY);        //Arc end

        //Draw the arc, to ensure outline is drawn completely (fill won't do it, necessarily). This will also
        //give the arc a darker outline
        g2D.draw(arcPath);
        //Fill the arc
        g2D.fill(arcPath);

        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT);
        g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT);

    }


}
