/**
 * @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: {
                /**
                 * @cfg {Object} [curve={type: 'linear'}]
                 * The type of curve that connects the data points.
                 *
                 * For example:
                 *
                 *     // The data points are connected by line segments.
                 *     // This is the default setting.
                 *     curve: {
                 *         type: 'linear'
                 *     }
                 *
                 *     // Cardinal spline interpolation is used to produce the curve
                 *     // that connects the data points. The `tension` parameter can
                 *     // be used to control the smoothness of the curve. A tension
                 *     // of 0 corresponds to infinite tension, which results in straight
                 *     // lines between data points. A tension of 1 corresponds to
                 *     // no tension, allowing the spline to take the path of least
                 *     // total bend. With tension values greater than 1, the curve
                 *     // behaves like a compressed spring, pushed to take a longer path.
                 *     // A cardinal spline with a tension of 0.5 is a special case.
                 *     // It is then called a Catmull-Rom spline. Catmull-Rom splines are
                 *     // thought to be esthetically pleasing and are quite common.
                 *     // Note: spline interpolation only works on gapless data.
                 *     curve: {
                 *         type: 'cardinal,
                 *         tension: 0.5
                 *     }
                 *
                 *     // Produces a natural cubic spline with the second derivative
                 *     // of the spline set to zero at the endpoints.
                 *     curve: {
                 *         type: 'natural'
                 *     }
                 *
                 *     // The data points are connected by alternating horizontal and
                 *     // vertical lines. The y-value changes after the x-value.
                 *     curve: {
                 *         type: 'step-after'
                 *     }
                 *
                 */
                curve: 'default',
 
                /**
                 * @cfg {Boolean} [fillArea=false]
                 * `true` if the sprite paints the area underneath the line.
                 */
                fillArea: 'bool',
 
                /**
                 * @cfg {"gap"/"connect"/"origin"} [nullStyle="gap"]
                 * Possible values:
                 * 'gap' - null points are rendered as gaps.
                 * 'connect' - non-null points are connected across null points, so that
                 * there is no gap, unless null points are at the beginning/end of the line.
                 * Only the visible data points are connected - if a visible data point
                 * is followed by a series of null points that go off screen and eventually
                 * terminate with a non-null point, the connection won't be made.
                 * 'origin' - null data points are rendered at the origin,
                 * which is the y-coordinate of a point where the x and y axes meet.
                 * This requires that at least the x-coordinate of a point is a valid value.
                 */
                nullStyle: 'enums(gap,connect,origin)',
 
                /**
                 * @cfg {Boolean} [preciseStroke=true]
                 * `true` if the line uses precise stroke.
                 */
                preciseStroke: 'bool',
 
                /**
                 * @private
                 * The x-axis associated with the Line series.
                 * We need to know the position of the x-axis to fill the area underneath
                 * the stroke properly.
                 */
                xAxis: 'default',
 
                /**
                 * @cfg {Number} [yCap=Math.pow(2, 20)]
                 * Absolute maximum y-value.
                 * Larger values will be capped to avoid rendering issues.
                 */
                yCap: 'default' // The 'default' processor is used here as we don't want this attribute to animate. 
            },
 
            defaults: {
                curve: {
                    type: 'linear'
                },
                nullStyle: 'connect',
                fillArea: false,
                preciseStroke: true,
                xAxis: null,
                yCap: Math.pow(2, 20),
                yJump: 50
            },
 
            triggers: {
                dataX: 'dataX,bbox,curve',
                dataY: 'dataY,bbox,curve',
                curve: 'curve'
            },
 
            updaters: {
                curve: 'curveUpdater'
            }
        }
    },
 
    list: null,
 
    curveUpdater: function (attr) {
        var me = this,
            dataX = attr.dataX,
            dataY = attr.dataY,
            curve = attr.curve,
            smoothable = dataX && dataY && dataX.length > 2 && dataY.length > 2,
            type = curve.type;
 
        if (smoothable) {
            if (type === 'natural') {
                me.smoothX = Ext.draw.Draw.naturalSpline(dataX);
                me.smoothY = Ext.draw.Draw.naturalSpline(dataY);
            } else if (type === 'cardinal') {
                me.smoothX = Ext.draw.Draw.cardinalSpline(dataX, curve.tension);
                me.smoothY = Ext.draw.Draw.cardinalSpline(dataY, curve.tension);
            } else {
                smoothable = false;
            }
        }
 
        if (!smoothable) {
            delete me.smoothX;
            delete me.smoothY;
        }
    },
 
    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;
    },
 
    drawStrip: function (ctx, strip) {
        ctx.moveTo(strip[0], strip[1]);
        for (var i = 2, ln = strip.length; i < ln; i += 2) {
            ctx.lineTo(strip[i], strip[+ 1]);
        }
    },
 
    drawStraightStroke: function (surface, ctx, start, end, list, xAxis) {
        var me = this,
            attr = me.attr,
            nullStyle = attr.nullStyle,
            isConnect = nullStyle === 'connect',
            isOrigin = nullStyle === 'origin',
            renderer = attr.renderer,
            curve = attr.curve,
            step = curve.type === 'step-after',
            needMoveTo = true,
            ln = list.length,
            lineConfig = {
                type: 'line',
                smooth: false,
                step: step
            };
 
        var rendererChanges, params, stripStartX,
            isValidX0, isValidX, isValidX1,
            isValidPoint0, isValidPoint, isValidPoint1,
            isGap, lastValidPoint, px, py,
            x, y, x0, y0, x1, y1, i;
 
        // 'strip' stores last continuous segment of the stroke, 
        // which we may need to re-build, if there's a fill as well. 
        // For example, if the renderer returned a style that needs 
        // to be applied to the current step, or we reached a null 
        // point in the data, where we have to fill the current continuous 
        // segment, we build and close a path that will be filled, then 
        // re-build the stroke path, using coordinates saved in the 'strip', 
        // and render the stroke on top of the fill. 
        var strip = [];
 
        ctx.beginPath();
        for (= 3; i < ln; i += 3) {
            x0 = list[- 3];
            y0 = list[- 2];
            x = list[i];
            y = list[+ 1];
            x1 = list[+ 3];
            y1 = list[+ 4];
 
            isValidX0 = Ext.isNumber(x0);
            isValidX = Ext.isNumber(x);
            isValidX1 = Ext.isNumber(x1);
 
            isValidPoint0 = isValidX0 && Ext.isNumber(y0);
            isValidPoint = isValidX && Ext.isNumber(y);
            isValidPoint1 = isValidX1 && Ext.isNumber(y1);
 
            if (isOrigin) {
                // If only the y-component isn't a valid number, 
                // we can 'fix' it by setting it to value of y-origin. 
                if (!isValidPoint0 && isValidX0) {
                    y0 = xAxis;
                    isValidPoint0 = true;
                }
                if (!isValidPoint && isValidX) {
                    y = xAxis;
                    isValidPoint = true;
                }
                if (!isValidPoint1 && isValidX1) {
                    y1 = xAxis;
                    isValidPoint1 = true;
                }
            }
 
            if (renderer) {
                lineConfig.x = x;
                lineConfig.y = y;
                lineConfig.x0 = x0;
                lineConfig.y0 = y0;
                params = [me, lineConfig, me.rendererData, start + i/3];
                // callback(fn, scope, args, delay, caller) 
                rendererChanges = Ext.callback(renderer, null, params, 0, me.getSeries());
            }
 
            if (isGap && isConnect && isValidPoint0 && lastValidPoint) {
                px = lastValidPoint[0];
                py = lastValidPoint[1];
                if (needMoveTo) {
                    ctx.beginPath();
                    ctx.moveTo(px, py);
                    strip.push(px, py);
                    stripStartX = px;
                    needMoveTo = false;
                }
 
                if (step) {
                    ctx.lineTo(x0, py);
                    strip.push(x0, py);
                }
                ctx.lineTo(x0, y0);
                strip.push(x0, y0);
 
                lastValidPoint = [x0, y0];
                isGap = false;
            }
 
            // Special case where we have an uninterrupted segment, followed 
            // by a gap, then a valid point, then another gap. The uninterrupted 
            // segment should be connenected with the dot situated between the gaps. 
            if (isConnect && lastValidPoint && isValidPoint && !isValidPoint0) {
                x0 = lastValidPoint[0];
                y0 = lastValidPoint[1];
                isValidPoint0 = true;
            }
 
            // Remember last valid point to connect the gap 
            // when the next valid point is encountered. 
            if (isValidPoint) {
                lastValidPoint = [x, y];
            }
 
            if (isValidPoint0 && isValidPoint) {
                if (needMoveTo) {
                    ctx.beginPath();
                    ctx.moveTo(x0, y0);
                    strip.push(x0, y0);
                    stripStartX = x0;
                    needMoveTo = false;
                }
            } else {
                isGap = true;
                continue;
            }
 
            if (step) {
                ctx.lineTo(x, y0);
                strip.push(x, y0);
            }
            ctx.lineTo(x, y);
            strip.push(x, y);
 
            // If the next point is a gap, then we need to fill what 
            // has been already rendered so far. The same applies 
            // if the renderer returned some changes to apply to 
            // the current step. 
            if ( rendererChanges || !isValidPoint1 ) {
                ctx.save();
                Ext.apply(ctx, rendererChanges);
                rendererChanges = null;
 
                if (attr.fillArea) {
                    ctx.lineTo(x, xAxis);
                    ctx.lineTo(stripStartX, xAxis);
                    ctx.closePath();
                    ctx.fill();
                }
 
                // Draw the line on top of the filled area. 
                ctx.beginPath();
                me.drawStrip(ctx, strip);
                strip = [];
                ctx.stroke();
                ctx.restore();
 
                ctx.beginPath();
                // Take note that the starting point of a path has been reset 
                // (as a result of filling a sub-path) and needs to be set again 
                // for the line to continue in a proper manner. 
                needMoveTo = true;
            }
        }
    },
 
    calculateScale: function (count, end) {
        var power = 0,
            n = count;
        while (< end && count > 0) {
            power++;
            n += count >> power;
        }
        return Math.pow(2, power > 0 ? power - 1 : power);
    },
 
    drawSmoothStroke: function (surface, ctx, start, end, list, xAxis) {
        var me = this,
            attr = me.attr,
            step = attr.step,
            matrix = attr.matrix,
            renderer = attr.renderer,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            smoothX = me.smoothX,
            smoothY = me.smoothY,
            scale = me.calculateScale(attr.dataX.length, end),
            cx1, cy1, cx2, cy2, x, y, x0, y0,
            i, j, changes, params,
            lineConfig = {
                type: 'line',
                smooth: true,
                step: step
            };
 
        ctx.beginPath();
        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 = surface.roundPixel(list[+ 3]);
            y = list[+ 4];
            x0 = surface.roundPixel(list[i]);
            y0 = list[+ 1];
 
            if (renderer) {
                lineConfig.x0 = x0;
                lineConfig.y0 = y0;
                lineConfig.cx1 = cx1;
                lineConfig.cy1 = cy1;
                lineConfig.cx2 = cx2;
                lineConfig.cy2 = cy2;
                lineConfig.x = x;
                lineConfig.y = y;
                params = [me, lineConfig, me.rendererData, start + i/3 + 1];
                changes = Ext.callback(renderer, null, params, 0, me.getSeries());
                ctx.save();
                Ext.apply(ctx, changes);
            }
 
            if (attr.fillArea) {
                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.fill();
                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();
 
            if (renderer) {
                ctx.restore();
            }
 
            ctx.beginPath();
            ctx.moveTo(x, y);
        }
        // Prevent the last visible segment from being stroked twice 
        // (second time by the ctx.fillStroke inside Path sprite 'render' method) 
        ctx.beginPath();
    },
 
    drawLabel: function (text, dataX, dataY, labelId, rect) {
        var me = this,
            attr = me.attr,
            label = me.getMarker('labels'),
            labelTpl = label.getTemplate(),
            labelCfg = me.labelCfg || (me.labelCfg = {}),
            surfaceMatrix = me.surfaceMatrix,
            labelX, labelY,
            labelOverflowPadding = attr.labelOverflowPadding,
            halfHeight, labelBBox,
            changes, params, 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 (attr.flipXY) {
            labelCfg.rotationRads = Math.PI * 0.5;
        } else {
            labelCfg.rotationRads = 0;
        }
 
        labelCfg.text = text;
 
        if (labelTpl.attr.renderer) {
            params = [text, label, labelCfg, me.rendererData, labelId];
            changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
            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);
    },
 
    drawMarker: function (x, y, index) {
        var me = this,
            attr = me.attr,
            renderer = attr.renderer,
            surfaceMatrix = me.surfaceMatrix,
            markerCfg = {},
            changes, params;
 
        if (renderer && me.getMarker('markers')) {
            markerCfg.type = 'marker';
            markerCfg.x = x;
            markerCfg.y = y;
            params = [me, markerCfg, me.rendererData, index];
            changes = Ext.callback(renderer, null, params, 0, me.getSeries());
            if (changes) {
                Ext.apply(markerCfg, changes);
            }
        }
 
        markerCfg.translationX = surfaceMatrix.x(x, y);
        markerCfg.translationY = surfaceMatrix.y(x, y);
 
        delete markerCfg.x;
        delete markerCfg.y;
 
        me.putMarker('markers', markerCfg, index, !renderer);
    },
 
    drawStroke: function (surface, ctx, start, end, list, xAxis) {
        var me = this,
            isSmooth = me.smoothX && me.smoothY;
 
        if (isSmooth) {
            me.drawSmoothStroke(surface, ctx, start, end, list, xAxis);
        } else {
            me.drawStraightStroke(surface, ctx, start, end, list, xAxis);
        }
    },
 
    renderAggregates: function (aggregates, start, end, surface, ctx, clip, rect) {
        var me = this,
            attr = me.attr,
            dataX = attr.dataX,
            dataY = attr.dataY,
            labels = attr.labels,
            xAxis = attr.xAxis,
            yCap = attr.yCap,
            isSmooth = attr.smooth && me.smoothX && me.smoothY,
            isDrawLabels = labels && me.getMarker('labels'),
            isDrawMarkers = me.getMarker('markers'),
            matrix = attr.matrix,
            pixel = surface.devicePixelRatio,
            xx = matrix.getXX(),
            yy = matrix.getYY(),
            dx = matrix.getDX(),
            dy = matrix.getDY(),
            list = me.list || (me.list = []),
            minXs = aggregates.minX,
            maxXs = aggregates.maxX,
            minYs = aggregates.minY,
            maxYs = aggregates.maxY,
            idx = aggregates.startIdx,
            isContinuousLine = true,
            isValidMinX, isValidMaxX,
            isValidMinY, isValidMaxY,
            xAxisOrigin, isVerticalX,
            x, y, i, index;
 
        me.rendererData = {store: me.getStore()};
        list.length = 0;
 
        // Say we have 7 y-items (attr.dataY): [20, 19, 17, 15, 11, 10, 14] 
        //         and 7 x-items (attr.dataX): [0,   1,  2,  3,  4,  5,  6]. 
        // Then aggregates.startIdx is an aggregated index, 
        // where every other item is skipped on each aggregation level: 
        // [0, 1, 2, 3, 4, 5, 6, 
        //  0, 2, 4, 6, 
        //  0, 4, 
        //  0] 
        // aggregates.minY 
        // [20, 19, 17, 15, 11, 10, 14, 
        //  19, 15, 10, 14, 
        //  15, 10, 
        //  10] 
        // aggregates.maxY 
        // [20, 19, 17, 15, 11, 10, 14, 
        //  20, 17, 11, 14, 
        //  20, 14, 
        //  20] 
        // aggregates.minX is 
        // [0, 1, 2, 3, 4, 5, 6, 
        //  1, 3, 5, 6, // TODO: why this order for min? 
        //  3, 5,       // TODO: why this inconsistency? 
        //  5] 
        // aggregates.maxX is 
        // [0, 1, 2, 3, 4, 5, 6, 
        //  0, 2, 4, 6, 
        //  0, 6, 
        //  0] 
 
        // Create a list of the form [x0, y0, idx0, x1, y1, idx1, ...], 
        // where each x,y pair is a coordinate representing original data point 
        // at the idx position. 
        for (= start; i < end; i++) {
            var minX = minXs[i],
                maxX = maxXs[i],
                minY = minYs[i],
                maxY = maxYs[i];
 
            isValidMinX = Ext.isNumber(minX);
            isValidMinY = Ext.isNumber(minY);
            isValidMaxX = Ext.isNumber(maxX);
            isValidMaxY = Ext.isNumber(maxY);
 
            if (minX < maxX) {
                list.push(
                    isValidMinX ? (minX * xx + dx) : null,
                    isValidMinY ? (minY * yy + dy) : null,
                    idx[i]
                );
                list.push(
                    isValidMaxX ? (maxX * xx + dx) : null,
                    isValidMaxY ? (maxY * yy + dy) : null,
                    idx[i]
                );
            } else if (minX > maxX) {
                list.push(
                    isValidMaxX ? (maxX * xx + dx) : null,
                    isValidMaxY ? (maxY * yy + dy) : null,
                    idx[i]
                );
                list.push(
                    isValidMinX ? (minX * xx + dx) : null,
                    isValidMinY ? (minY * yy + dy) : null,
                    idx[i]
                );
            } else {
                list.push(
                    isValidMaxX ? (maxX * xx + dx) : null,
                    isValidMaxY ? (maxY * yy + dy) : null,
                    idx[i]
                );
            }
        }
 
        if (list.length) {
            for (= 0; i < list.length; i += 3) {
                x = list[i];
                y = list[+ 1];
                if (Ext.isNumber(x) && Ext.isNumber(y)) {
                    if (> yCap) {
                        y = yCap;
                    } else if (< -yCap) {
                        y = -yCap;
                    }
                    list[+ 1] = y;
                } else {
                    isContinuousLine = false;
                    continue;
                }
                index = list[+ 2];
                if (isDrawMarkers) {
                    me.drawMarker(x, y, index);
                }
                if (isDrawLabels && labels[index]) {
                    me.drawLabel(labels[index], x, y, index, rect);
                }
            }
 
            me.isContinuousLine = isContinuousLine;
            if (isSmooth && !isContinuousLine) {
                Ext.raise("Line smoothing in only supported for gapless data, " +
                    "where all data points are finite numbers.");
            }
 
            if (xAxis) {
                isVerticalX = xAxis.getAlignment() === 'vertical';
                if (Ext.isNumber(xAxis.floatingAtCoord)) {
                    xAxisOrigin = (isVerticalX ? rect[2] : rect[3]) - xAxis.floatingAtCoord;
                } else {
                    xAxisOrigin = isVerticalX ? rect[0] : rect[1];
                }
            } else {
                xAxisOrigin = attr.flipXY ? rect[0] : rect[1];
            }
 
            if (attr.preciseStroke) {
                if (attr.fillArea) {
                    ctx.fill();
                }
                if (attr.transformFillStroke) {
                    attr.inverseMatrix.toContext(ctx);
                }
                me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
                if (attr.transformFillStroke) {
                    attr.matrix.toContext(ctx);
                }
                ctx.stroke();
            } else {
                me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
 
                if (isContinuousLine && isSmooth && attr.fillArea && !attr.renderer) {
                    var lastPointX = dataX[dataX.length - 1] * xx + dx + pixel,
                        lastPointY = dataY[dataY.length - 1] * yy + dy,
                        firstPointX = dataX[0] * xx + dx - pixel,
                        firstPointY = dataY[0] * yy + dy;
                    // Fill the area from the series to the xAxis in case there 
                    // are no gaps and no renderer is used, in which case the 
                    // area would be filled per uninterrupted segment or per 
                    // step, instead of being filled a single pass. 
                    ctx.lineTo(lastPointX, lastPointY);
                    ctx.lineTo(lastPointX, xAxisOrigin - attr.lineWidth);
                    ctx.lineTo(firstPointX, xAxisOrigin - attr.lineWidth);
                    ctx.lineTo(firstPointX, firstPointY);
                }
 
                if (attr.transformFillStroke) {
                    attr.matrix.toContext(ctx);
                }
                // Prevent the reverse transform to fix floating point error. 
                if (attr.fillArea) {
                    ctx.fillStroke(attr, true);
                } else {
                    ctx.stroke(true);
                }
            }
        }
    }
});