/**
 * A sprite that represents an individual box with whiskers.
 * This sprite is meant to be managed by the {@link Ext.chart.series.sprite.BoxPlot}
 * {@link Ext.chart.MarkerHolder MarkerHolder}, but can also be used independently:
 *
 *     @example
 *     new Ext.draw.Container({
 *         width: 100,
 *         height: 100,
 *         renderTo: Ext.getBody(),
 *         sprites: [{
 *             type: 'boxplot',
 *             translationX: 50,
 *             translationY: 50
 *         }]
 *     });
 *
 * IMPORTANT: the attributes that represent y-coordinates are in screen coordinates,
 * just like with any other sprite. For this particular sprite this means that, if 'low'
 * and 'high' attributes are 10 and 90, then the minimium whisker is rendered at the top
 * of a draw container {@link Ext.draw.Surface surface} at y = 10, and the maximum whisker
 * is rendered at the bottom at y = 90. But because the series surface is flipped vertically
 * in cartesian charts, this means that there minimum is rendered at the bottom and maximum
 * at the top, just as one would expect.
 */
Ext.define('Ext.chart.sprite.BoxPlot', {
    extend: 'Ext.draw.sprite.Sprite',
    alias: 'sprite.boxplot',
    type: 'boxplot',
 
    inheritableStatics: {
        def: {
            processors: {
                /**
                 * @cfg {Number} [x=0] The coordinate of the horizontal center of a boxplot.
                 */
                x: 'number',
 
                /**
                 * @cfg {Number} [low=-20] The y-coordinate of the whisker that represents
                 * the minimum.
                 */
                low: 'number',
 
                /**
                 * @cfg {Number} [q1=-10] The y-coordinate of the box edge that represents
                 * the 1-st quartile.
                 */
                q1: 'number',
 
                /**
                 * @cfg {Number} [median=0] The y-coordinate of the line that represents the median.
                 */
                median: 'number',
 
                /**
                 * @cfg {Number} [q3=10] The y-coordinate of the box edge that represents
                 * the 3-rd quartile.
                 */
                q3: 'number',
 
                /**
                 * @cfg {Number} [high=20] The y-coordinate of the whisker that represents
                 * the maximum.
                 */
                high: 'number',
 
                /**
                 * @cfg {Number} [boxWidth=12] The width of the box in pixels.
                 */
                boxWidth: 'number',
 
                /**
                 * @cfg {Number} [whiskerWidth=0.5] The length of the lines at the ends
                 * of the whiskers, as a ratio of `boxWidth`.
                 */
                whiskerWidth: 'number',
 
                /**
                 * @cfg {Boolean} [crisp=true] Whether to snap the rendered lines to the pixel grid
                 * of not. Generally, it's best to have this set to `true` (which is the default)
                 * for pixel perfect results (especially on non-HiDPI displays), but for boxplots
                 * with small `boxWidth` visible artifacts caused by pixel grid snapping may become
                 * noticeable, and setting this to `false` can be a remedy at the expense
                 * of clarity.
                 */
                crisp: 'bool'
            },
 
            triggers: {
                x: 'bbox',
                low: 'bbox',
                high: 'bbox',
                boxWidth: 'bbox',
                whiskerWidth: 'bbox',
                crisp: 'bbox'
            },
 
            defaults: {
                x: 0,
 
                low: -20,
                q1: -10,
                median: 0,
                q3: 10,
                high: 20,
 
                boxWidth: 12,
                whiskerWidth: 0.5,
 
                crisp: true,
 
                fillStyle: '#ccc',
                strokeStyle: '#000'
            }
        }
    },
 
    updatePlainBBox: function(plain) {
        var me = this,
            attr = me.attr,
            halfLineWidth = attr.lineWidth / 2,
            x = attr.x - attr.boxWidth / 2 - halfLineWidth,
            y = attr.high - halfLineWidth,
            width = attr.boxWidth + attr.lineWidth,
            height = attr.low - attr.high + attr.lineWidth;
 
        plain.x = x;
        plain.y = y;
        plain.width = width;
        plain.height = height;
    },
 
    render: function(surface, ctx) {
        var me = this,
            attr = me.attr;
 
        attr.matrix.toContext(ctx); // enable sprite transformations
 
        if (attr.crisp) {
            me.crispRender(surface, ctx);
        }
        else {
            me.softRender(surface, ctx);
        }
 
        //<debug>
        // eslint-disable-next-line vars-on-top, one-var
        var debug = attr.debug || this.statics().debug || Ext.draw.sprite.Sprite.debug;
 
        if (debug) {
            // This assumes no part of the sprite is rendered after this call.
            // If it is, we need to re-apply transformations.
            // But the bounding box should always be rendered as is, untransformed.
            this.attr.inverseMatrix.toContext(ctx);
 
            if (debug.bbox) {
                this.renderBBox(surface, ctx);
            }
        }
        //</debug>
    },
 
    /**
     * @private
     * Renders a single box with whiskers.
     * Changes to this method have to be reflected in the {@link #crispRender} as well.
     * @param surface
     * @param ctx
     */
    softRender: function(surface, ctx) {
        var me = this,
            attr = me.attr,
 
            x = attr.x,
            low = attr.low,
            q1 = attr.q1,
            median = attr.median,
            q3 = attr.q3,
            high = attr.high,
 
            halfBoxWidth = attr.boxWidth / 2,
            halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2,
 
            dash = ctx.getLineDash();
 
        ctx.setLineDash([]); // Only stem can be dashed.
 
        // Box.
        ctx.beginPath();
        ctx.moveTo(- halfBoxWidth, q3);
        ctx.lineTo(+ halfBoxWidth, q3);
        ctx.lineTo(+ halfBoxWidth, q1);
        ctx.lineTo(- halfBoxWidth, q1);
        ctx.closePath();
        ctx.fillStroke(attr, true);
 
        // Stem.
        ctx.setLineDash(dash);
        ctx.beginPath();
        ctx.moveTo(x, q3);
        ctx.lineTo(x, high);
        ctx.moveTo(x, q1);
        ctx.lineTo(x, low);
        ctx.stroke();
        ctx.setLineDash([]);
 
        // Whiskers.
        ctx.beginPath();
        ctx.moveTo(- halfWhiskerWidth, low);
        ctx.lineTo(+ halfWhiskerWidth, low);
        ctx.moveTo(- halfBoxWidth, median);
        ctx.lineTo(+ halfBoxWidth, median);
        ctx.moveTo(- halfWhiskerWidth, high);
        ctx.lineTo(+ halfWhiskerWidth, high);
        ctx.stroke();
    },
 
    alignLine: function(x, lineWidth) {
        lineWidth = lineWidth || this.attr.lineWidth;
 
        x = Math.round(x);
 
        if (lineWidth % 2 === 1) {
            x -= 0.5;
        }
 
        return x;
    },
 
    /**
     * @private
     * Renders a pixel-perfect single box with whiskers by aligning to the pixel grid.
     * Changes to this method have to be reflected in the {@link #softRender} as well.
     *
     * Note: crisp image is only guaranteed when `attr.lineWidth` is a whole number.
     * @param surface
     * @param ctx
     */
    crispRender: function(surface, ctx) {
        var me = this,
            attr = me.attr,
 
            x = attr.x,
            low = me.alignLine(attr.low),
            q1 = me.alignLine(attr.q1),
            median = me.alignLine(attr.median),
            q3 = me.alignLine(attr.q3),
            high = me.alignLine(attr.high),
 
            halfBoxWidth = attr.boxWidth / 2,
            halfWhiskerWidth = attr.boxWidth * attr.whiskerWidth / 2,
            stemX = me.alignLine(x),
            boxLeft = me.alignLine(- halfBoxWidth),
            boxRight = me.alignLine(+ halfBoxWidth),
            whiskerLeft = stemX + Math.round(-halfWhiskerWidth),
            whiskerRight = stemX + Math.round(halfWhiskerWidth),
            dash = ctx.getLineDash();
 
        ctx.setLineDash([]); // Only stem can be dashed.
 
        // Box.
        ctx.beginPath();
        ctx.moveTo(boxLeft, q3);
        ctx.lineTo(boxRight, q3);
        ctx.lineTo(boxRight, q1);
        ctx.lineTo(boxLeft, q1);
        ctx.closePath();
        ctx.fillStroke(attr, true);
 
        // Stem.
        ctx.setLineDash(dash);
        ctx.beginPath();
        ctx.moveTo(stemX, q3);
        ctx.lineTo(stemX, high);
        ctx.moveTo(stemX, q1);
        ctx.lineTo(stemX, low);
        ctx.stroke();
        ctx.setLineDash([]);
 
        // Whiskers.
        ctx.beginPath();
        ctx.moveTo(whiskerLeft, low);
        ctx.lineTo(whiskerRight, low);
        ctx.moveTo(boxLeft, median);
        ctx.lineTo(boxRight, median);
        ctx.moveTo(whiskerLeft, high);
        ctx.lineTo(whiskerRight, high);
        ctx.stroke();
    }
});