/**
 * @class Ext.chart.series.sprite.Line
 * @extends Ext.chart.series.sprite.Aggregative
 *
 * Line series sprite.
 */
Ext.define('Ext.chart.series.sprite.Line', {
    alias: 'sprite.lineSeries',
    extend: 'Ext.chart.series.sprite.Aggregative',
 
    inheritableStatics: {
        def: {
            processors: {
                smooth: 'bool',
                fillArea: 'bool',
                step: 'bool',
                preciseStroke: 'bool'
            },
 
            defaults: {
                /**
                 * @cfg {Boolean} smooth 'true' if the sprite uses line smoothing.
                 */
                smooth: false,
 
                /**
                 * @cfg {Boolean} fillArea 'true' if the sprite paints the area underneath the line.
                 */
                fillArea: false,
 
                /**
                 * @cfg {Boolean} step 'true' if the line uses steps instead of straight lines to connect the dots.
                 * It is ignored if `smooth` is true.
                 */
                step: false,
 
                /**
                 * @cfg {Boolean} preciseStroke 'true' if the line uses precise stroke.
                 */
                preciseStroke: true
            },
 
            dirtyTriggers: {
                dataX: 'dataX,bbox,smooth',
                dataY: 'dataY,bbox,smooth',
                smooth: 'smooth'
            },
 
            updaters: {
                smooth: function (attr) {
                    if (attr.smooth && attr.dataX && attr.dataY && attr.dataX.length > 2 && attr.dataY.length > 2) {
                        this.smoothX = Ext.draw.Draw.spline(attr.dataX);
                        this.smoothY = Ext.draw.Draw.spline(attr.dataY);
                    } else {
                        delete this.smoothX;
                        delete this.smoothY;
                    }
                }
            }
        }
    },
 
    list: null,
 
    updatePlainBBox: function (plain) {
        var attr = this.attr,
            ymin = Math.min(0, attr.dataMinY),
            ymax = Math.max(0, attr.dataMaxY);
        plain.x = attr.dataMinX;
        plain.y = ymin;
        plain.width = attr.dataMaxX - attr.dataMinX;
        plain.height = ymax - ymin;
    },
 
    drawStroke: function (surface, ctx, start, end, list, xAxis) {
        var attr = this.attr,
            matrix = attr.matrix,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            smooth = attr.smooth,
            step = attr.step,
            scale = Math.pow(2, power(attr.dataX.length, end)),
            smoothX = this.smoothX,
            smoothY = this.smoothY,
            i, j, lineConfig, changes,
            cx1, cy1, cx2, cy2, x, y, x0, y0, saveOpacity;
 
        function power(count, end) {
            var power = 0,
                n = count;
            while (< end && count > 0) {
                power++;
                n += count >> power;
            }
            return power > 0 ? power - 1 : power;
        }
 
        ctx.beginPath();
        if (smooth && smoothX && smoothY) {
            ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);
            for (= 0, j = start * 3 + 1; i < list.length - 3; i += 3, j += 3 * scale) {
                cx1 = smoothX[j] * xx + dx;
                cy1 = smoothY[j] * yy + dy;
                cx2 = smoothX[+ 1] * xx + dx;
                cy2 = smoothY[+ 1] * yy + dy;
                x = list[+ 3];
                y = list[+ 4];
                x0 = list[i];
                y0 = list[+ 1];
                if (attr.renderer) {
                    lineConfig = {
                        type: 'line',
                        smooth: true,
                        step: step,
                        cx1: cx1,
                        cy1: cy1,
                        cx2: cx2,
                        cy2: cy2,
                        x: x,
                        y: y,
                        x0: x0,
                        y0: y0
                    };
                    changes = attr.renderer.call(this, this, lineConfig, {store:this.getStore()}, (start + i/3 + 1));
                    ctx.save();
                        Ext.apply(ctx, changes);
                        // Fill the area if we need to, using the fill color and transparent strokes. 
                        if (attr.fillArea) {
                            saveOpacity = ctx.strokeOpacity;
                            ctx.save();
                                ctx.strokeOpacity = 0;
                                ctx.moveTo(x0, y0);
                                ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
                                ctx.lineTo(x, xAxis);
                                ctx.lineTo(x0, xAxis);
                                ctx.lineTo(x0, y0);
                                ctx.closePath();
                                ctx.fillStroke(attr, true);
                            ctx.restore();
                            ctx.strokeOpacity = saveOpacity;
                            ctx.beginPath();
                        }
                        // Draw the line on top of the filled area. 
                        ctx.moveTo(x0, y0);
                        ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
                        ctx.stroke();
                        ctx.moveTo(x0, y0);
                        ctx.closePath();
                    ctx.restore();
                    ctx.beginPath();
                    ctx.moveTo(x, y);
                } else {
                    ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
                }
            }
        } else {
            ctx.moveTo(list[0], list[1]);
            for (= 3; i < list.length; i += 3) {
                x = list[i];
                y = list[+ 1];
                x0 = list[- 3];
                y0 = list[- 2];
                if (attr.renderer) {
                    lineConfig = {
                        type: 'line',
                        smooth: false,
                        step: step,
                        x: x,
                        y: y,
                        x0: x0,
                        y0: y0
                    };
                    changes = attr.renderer.call(this, this, lineConfig, {store: this.getStore()}, start + i/3);
                    ctx.save();
                        Ext.apply(ctx, changes);
                        // Fill the area if we need to, using the fill color and transparent strokes. 
                        if (attr.fillArea) {
                            saveOpacity = ctx.strokeOpacity;
                            ctx.save();
                                ctx.strokeOpacity = 0;
                                if (step) {
                                    ctx.lineTo(x, y0);
                                } else {
                                    ctx.lineTo(x, y);
                                }
                                ctx.lineTo(x, xAxis);
                                ctx.lineTo(x0, xAxis);
                                ctx.lineTo(x0, y0);
                                ctx.closePath();
                                ctx.fillStroke(attr, true);
                            ctx.restore();
                            ctx.strokeOpacity = saveOpacity;
                            ctx.beginPath();
                        }
                        // Draw the line (or the 2 lines if 'step') on top of the filled area. 
                        ctx.moveTo(x0, y0);
                        if (step) {
                            ctx.lineTo(x, y0);
                            ctx.closePath();
                            ctx.stroke();
                            ctx.beginPath();
                            ctx.moveTo(x, y0);
                        }
                        ctx.lineTo(x, y);
                        ctx.closePath();
                        ctx.stroke();
                    ctx.restore();
                    ctx.beginPath();
                    ctx.moveTo(x, y);
                } else {
                    if (step) {
                        ctx.lineTo(x, y0);
                    }
                    ctx.lineTo(x, y);
                }
            }
        }
    },
 
    drawLabel: function (text, dataX, dataY, labelId, rect) {
        var me = this,
            attr = me.attr,
            label = me.getBoundMarker('labels')[0],
            labelTpl = label.getTemplate(),
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            surfaceMatrix = me.surfaceMatrix,
            labelX, labelY,
            labelOverflowPadding = attr.labelOverflowPadding,
            flipXY = attr.flipXY,
            halfHeight, labelBBox,
            changes, hasPendingChanges;
 
        // The coordinates below (data point converted to surface coordinates) 
        // are just for the renderer to give it a notion of where the label will be positioned. 
        // The actual position of the label will be different 
        // (unless the renderer returns x/y coordinates in the changes object) 
        // and depend on several things including the size of the text, 
        // which has to be measured after the renderer call, 
        // since text can be modified by the renderer. 
        labelCfg.x = surfaceMatrix.x(dataX, dataY);
        labelCfg.y = surfaceMatrix.y(dataX, dataY);
 
        if (flipXY) {
            labelCfg.rotationRads = Math.PI * 0.5;
        } else {
            labelCfg.rotationRads = 0;
        }
 
        labelCfg.text = text;
 
        if (labelTpl.attr.renderer) {
            changes = labelTpl.attr.renderer.call(me, text, label, labelCfg, {store: me.getStore()}, labelId);
            if (typeof changes === 'string') {
                labelCfg.text = changes;
            } else if (typeof changes === 'object') {
                if ('text' in changes) {
                    labelCfg.text = changes.text;
                }
                hasPendingChanges = true;
            }
        }
 
        labelBBox = me.getMarkerBBox('labels', labelId, true);
        if (!labelBBox) {
            me.putMarker('labels', labelCfg, labelId);
            labelBBox = me.getMarkerBBox('labels', labelId, true);
        }
 
        halfHeight = labelBBox.height / 2;
        labelX = dataX;
 
        switch (labelTpl.attr.display) {
            case 'under':
                labelY = dataY - halfHeight - labelOverflowPadding;
                break;
            case 'rotate':
                labelX += labelOverflowPadding;
                labelY = dataY - labelOverflowPadding;
                labelCfg.rotationRads = -Math.PI / 4;
                break;
            default: // 'over' 
                labelY = dataY + halfHeight + labelOverflowPadding;
        }
 
        labelCfg.x = surfaceMatrix.x(labelX, labelY);
        labelCfg.y = surfaceMatrix.y(labelX, labelY);
 
        if (hasPendingChanges) {
            Ext.apply(labelCfg, changes);
        }
 
        me.putMarker('labels', labelCfg, labelId);
    },
 
    renderAggregates: function (aggregates, start, end, surface, ctx, clip, rect) {
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            labels = attr.labels,
            drawLabels = labels && me.getBoundMarker('labels'),
            matrix = attr.matrix,
            surfaceMatrix = surface.matrix,
            pixel = surface.devicePixelRatio,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            markerCfg = {},
            list = me.list || (me.list = []),
            x, y, i, index,
            minXs = aggregates.minX,
            maxXs = aggregates.maxX,
            minYs = aggregates.minY,
            maxYs = aggregates.maxY,
            idx = aggregates.startIdx;
 
        list.length = 0;
        for (= start; i < end; i++) {
            var minX = minXs[i],
                maxX = maxXs[i],
                minY = minYs[i],
                maxY = maxYs[i];
 
            if (minX < maxX) {
                list.push(minX * xx + dx, minY * yy + dy, idx[i]);
                list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
            } else if (minX > maxX) {
                list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
                list.push(minX * xx + dx, minY * yy + dy, idx[i]);
            } else {
                list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);
            }
        }
 
        if (list.length) {
            for (= 0; i < list.length; i += 3) {
                x = list[i];
                y = list[+ 1];
                index = list[+ 2];
                if (attr.renderer) {
                    markerCfg = {
                        type: 'marker',
                        x: x,
                        y: y
                    };
                    markerCfg = attr.renderer.call(me, me, markerCfg, {store: me.getStore()}, start + i/3) || {};
                }
                markerCfg.translationX = surfaceMatrix.x(x, y);
                markerCfg.translationY = surfaceMatrix.y(x, y);
                me.putMarker('markers', markerCfg, index, !attr.renderer);
 
                if (drawLabels && labels[index]) {
                    me.drawLabel(labels[index], x, y, index, rect);
                }
            }
            me.drawStroke(surface, ctx, start, end, list, rect[1] - pixel);
            if (!attr.renderer) {
                var lastPointX = dataX[dataX.length - 1] * xx + dx + pixel,
                    lastPointY = dataY[dataY.length - 1] * yy + dy,
                    bottomY = rect[1] - pixel,
                    firstPointX = dataX[0] * xx + dx - pixel,
                    firstPointY = dataY[0] * yy + dy;
                ctx.lineTo(lastPointX, lastPointY);
                ctx.lineTo(lastPointX, bottomY);
                ctx.lineTo(firstPointX, bottomY);
                ctx.lineTo(firstPointX, firstPointY);
            }
            ctx.closePath();
 
            if (attr.transformFillStroke) {
                attr.matrix.toContext(ctx);
            }
            if (attr.preciseStroke) {
                if (attr.fillArea) {
                    ctx.fill();
                }
                if (attr.transformFillStroke) {
                    attr.inverseMatrix.toContext(ctx);
                }
                me.drawStroke(surface, ctx, start, end, list, rect[1] - pixel);
                if (attr.transformFillStroke) {
                    attr.matrix.toContext(ctx);
                }
                ctx.stroke();
            } else {
                // Prevent the reverse transform to fix floating point err. 
                if (attr.fillArea) {
                    ctx.fillStroke(attr, true);
                } else {
                    ctx.stroke(true);
                }
            }
        }
    }
});