/**
## jquery.flot.drawSeries.js

This plugin is used by flot for drawing lines, plots, bars or area.

### Public methods
*/

(function($) {
    "use strict";

    function DrawSeries() {
        function plotLine(datapoints, xoffset, yoffset, axisx, axisy, ctx, steps) {
            var points = datapoints.points,
                ps = datapoints.pointsize,
                prevx = null,
                prevy = null;
            var x1 = 0.0,
                y1 = 0.0,
                x2 = 0.0,
                y2 = 0.0,
                mx = null,
                my = null,
                i = 0;

            ctx.beginPath();
            for (i = ps; i < points.length; i += ps) {
                x1 = points[i - ps];
                y1 = points[i - ps + 1];
                x2 = points[i];
                y2 = points[i + 1];

                if (x1 === null || x2 === null) {
                    mx = null;
                    my = null;
                    continue;
                }

                if (isNaN(x1) || isNaN(x2) || isNaN(y1) || isNaN(y2)) {
                    prevx = null;
                    prevy = null;
                    continue;
                }

                if (steps) {
                    if (mx !== null && my !== null) {
                        // if middle point exists, transfer p2 -> p1 and p1 -> mp
                        x2 = x1;
                        y2 = y1;
                        x1 = mx;
                        y1 = my;

                        // 'remove' middle point
                        mx = null;
                        my = null;

                        // subtract pointsize from i to have current point p1 handled again
                        i -= ps;
                    } else if (y1 !== y2 && x1 !== x2) {
                        // create a middle point
                        y2 = y1;
                        mx = x2;
                        my = y1;
                    }
                }

                // clip with ymin
                if (y1 <= y2 && y1 < axisy.min) {
                    if (y2 < axisy.min) {
                        // line segment is outside
                        continue;
                    }
                    // compute new intersection point
                    x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y1 = axisy.min;
                } else if (y2 <= y1 && y2 < axisy.min) {
                    if (y1 < axisy.min) {
                        continue;
                    }

                    x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y2 = axisy.min;
                }

                // clip with ymax
                if (y1 >= y2 && y1 > axisy.max) {
                    if (y2 > axisy.max) {
                        continue;
                    }

                    x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y1 = axisy.max;
                } else if (y2 >= y1 && y2 > axisy.max) {
                    if (y1 > axisy.max) {
                        continue;
                    }

                    x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y2 = axisy.max;
                }

                // clip with xmin
                if (x1 <= x2 && x1 < axisx.min) {
                    if (x2 < axisx.min) {
                        continue;
                    }

                    y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x1 = axisx.min;
                } else if (x2 <= x1 && x2 < axisx.min) {
                    if (x1 < axisx.min) {
                        continue;
                    }

                    y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x2 = axisx.min;
                }

                // clip with xmax
                if (x1 >= x2 && x1 > axisx.max) {
                    if (x2 > axisx.max) {
                        continue;
                    }

                    y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x1 = axisx.max;
                } else if (x2 >= x1 && x2 > axisx.max) {
                    if (x1 > axisx.max) {
                        continue;
                    }

                    y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x2 = axisx.max;
                }

                if (x1 !== prevx || y1 !== prevy) {
                    ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
                }

                prevx = x2;
                prevy = y2;
                ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
            }
            ctx.stroke();
        }

        function plotLineArea(datapoints, axisx, axisy, fillTowards, ctx, steps) {
            var points = datapoints.points,
                ps = datapoints.pointsize,
                bottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min,
                i = 0,
                ypos = 1,
                areaOpen = false,
                segmentStart = 0,
                segmentEnd = 0,
                mx = null,
                my = null;

            // we process each segment in two turns, first forward
            // direction to sketch out top, then once we hit the
            // end we go backwards to sketch the bottom
            while (true) {
                if (ps > 0 && i > points.length + ps) {
                    break;
                }

                i += ps; // ps is negative if going backwards

                var x1 = points[i - ps],
                    y1 = points[i - ps + ypos],
                    x2 = points[i],
                    y2 = points[i + ypos];

                if (ps === -2) {
                    /* going backwards and no value for the bottom provided in the series*/
                    y1 = y2 = bottom;
                }

                if (areaOpen) {
                    if (ps > 0 && x1 != null && x2 == null) {
                        // at turning point
                        segmentEnd = i;
                        ps = -ps;
                        ypos = 2;
                        continue;
                    }

                    if (ps < 0 && i === segmentStart + ps) {
                        // done with the reverse sweep
                        ctx.fill();
                        areaOpen = false;
                        ps = -ps;
                        ypos = 1;
                        i = segmentStart = segmentEnd + ps;
                        continue;
                    }
                }

                if (x1 == null || x2 == null) {
                    mx = null;
                    my = null;
                    continue;
                }

                if (steps) {
                    if (mx !== null && my !== null) {
                        // if middle point exists, transfer p2 -> p1 and p1 -> mp
                        x2 = x1;
                        y2 = y1;
                        x1 = mx;
                        y1 = my;

                        // 'remove' middle point
                        mx = null;
                        my = null;

                        // subtract pointsize from i to have current point p1 handled again
                        i -= ps;
                    } else if (y1 !== y2 && x1 !== x2) {
                        // create a middle point
                        y2 = y1;
                        mx = x2;
                        my = y1;
                    }
                }

                // clip x values

                // clip with xmin
                if (x1 <= x2 && x1 < axisx.min) {
                    if (x2 < axisx.min) {
                        continue;
                    }

                    y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x1 = axisx.min;
                } else if (x2 <= x1 && x2 < axisx.min) {
                    if (x1 < axisx.min) {
                        continue;
                    }

                    y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x2 = axisx.min;
                }

                // clip with xmax
                if (x1 >= x2 && x1 > axisx.max) {
                    if (x2 > axisx.max) {
                        continue;
                    }

                    y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x1 = axisx.max;
                } else if (x2 >= x1 && x2 > axisx.max) {
                    if (x1 > axisx.max) {
                        continue;
                    }

                    y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                    x2 = axisx.max;
                }

                if (!areaOpen) {
                    // open area
                    ctx.beginPath();
                    ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
                    areaOpen = true;
                }

                // now first check the case where both is outside
                if (y1 >= axisy.max && y2 >= axisy.max) {
                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
                    continue;
                } else if (y1 <= axisy.min && y2 <= axisy.min) {
                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
                    continue;
                }

                // else it's a bit more complicated, there might
                // be a flat maxed out rectangle first, then a
                // triangular cutout or reverse; to find these
                // keep track of the current x values
                var x1old = x1,
                    x2old = x2;

                // clip the y values, without shortcutting, we
                // go through all cases in turn

                // clip with ymin
                if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
                    x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y1 = axisy.min;
                } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
                    x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y2 = axisy.min;
                }

                // clip with ymax
                if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
                    x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y1 = axisy.max;
                } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
                    x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                    y2 = axisy.max;
                }

                // if the x value was changed we got a rectangle
                // to fill
                if (x1 !== x1old) {
                    ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
                    // it goes to (x1, y1), but we fill that below
                }

                // fill triangular section, this sometimes result
                // in redundant points if (x1, y1) hasn't changed
                // from previous line to, but we just ignore that
                ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
                ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));

                // fill the other rectangle if it's there
                if (x2 !== x2old) {
                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
                    ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
                }
            }
        }

        /**
        - drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)

         This function is used for drawing lines or area fill.  In case the series has line decimation function
         attached, before starting to draw, as an optimization the points will first be decimated.

         The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
         plotHeight are the corresponding parameters of flot used to determine the drawing surface.
         The function getColorOrGradient is used to compute the fill style of lines and area.
        */
        function drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);
            ctx.lineJoin = "round";

            if (series.lines.dashes && ctx.setLineDash) {
                ctx.setLineDash(series.lines.dashes);
            }

            var datapoints = {
                format: series.datapoints.format,
                points: series.datapoints.points,
                pointsize: series.datapoints.pointsize
            };

            if (series.decimate) {
                datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
            }

            var lw = series.lines.lineWidth;

            ctx.lineWidth = lw;
            ctx.strokeStyle = series.color;
            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight, getColorOrGradient);
            if (fillStyle) {
                ctx.fillStyle = fillStyle;
                plotLineArea(datapoints, series.xaxis, series.yaxis, series.lines.fillTowards || 0, ctx, series.lines.steps);
            }

            if (lw > 0) {
                plotLine(datapoints, 0, 0, series.xaxis, series.yaxis, ctx, series.lines.steps);
            }

            ctx.restore();
        }

        /**
        - drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)

         This function is used for drawing points using a given symbol. In case the series has points decimation
         function attached, before starting to draw, as an optimization the points will first be decimated.

         The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
         plotHeight are the corresponding parameters of flot used to determine the drawing surface.
         The function drawSymbol is used to compute and draw the symbol chosen for the points.
        */
        function drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
            function drawCircle(ctx, x, y, radius, shadow, fill) {
                ctx.moveTo(x + radius, y);
                ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
            }
            drawCircle.fill = true;
            function plotPoints(datapoints, radius, fill, offset, shadow, axisx, axisy, drawSymbolFn) {
                var points = datapoints.points,
                    ps = datapoints.pointsize;

                ctx.beginPath();
                for (var i = 0; i < points.length; i += ps) {
                    var x = points[i],
                        y = points[i + 1];
                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
                        continue;
                    }

                    x = axisx.p2c(x);
                    y = axisy.p2c(y) + offset;

                    drawSymbolFn(ctx, x, y, radius, shadow, fill);
                }
                if (drawSymbolFn.fill && !shadow) {
                    ctx.fill();
                }
                ctx.stroke();
            }

            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);

            var datapoints = {
                format: series.datapoints.format,
                points: series.datapoints.points,
                pointsize: series.datapoints.pointsize
            };

            if (series.decimatePoints) {
                datapoints.points = series.decimatePoints(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
            }

            var lw = series.points.lineWidth,
                radius = series.points.radius,
                symbol = series.points.symbol,
                drawSymbolFn;

            if (symbol === 'circle') {
                drawSymbolFn = drawCircle;
            } else if (typeof symbol === 'string' && drawSymbol && drawSymbol[symbol]) {
                drawSymbolFn = drawSymbol[symbol];
            } else if (typeof drawSymbol === 'function') {
                drawSymbolFn = drawSymbol;
            }

            // If the user sets the line width to 0, we change it to a very
            // small value. A line width of 0 seems to force the default of 1.

            if (lw === 0) {
                lw = 0.0001;
            }

            ctx.lineWidth = lw;
            ctx.fillStyle = getFillStyle(series.points, series.color, null, null, getColorOrGradient);
            ctx.strokeStyle = series.color;
            plotPoints(datapoints, radius,
                true, 0, false,
                series.xaxis, series.yaxis, drawSymbolFn);
            ctx.restore();
        }

        function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
            var left = x + barLeft,
                right = x + barRight,
                bottom = b, top = y,
                drawLeft, drawRight, drawTop, drawBottom = false,
                tmp;

            drawLeft = drawRight = drawTop = true;

            // in horizontal mode, we start the bar from the left
            // instead of from the bottom so it appears to be
            // horizontal rather than vertical
            if (horizontal) {
                drawBottom = drawRight = drawTop = true;
                drawLeft = false;
                left = b;
                right = x;
                top = y + barLeft;
                bottom = y + barRight;

                // account for negative bars
                if (right < left) {
                    tmp = right;
                    right = left;
                    left = tmp;
                    drawLeft = true;
                    drawRight = false;
                }
            } else {
                drawLeft = drawRight = drawTop = true;
                drawBottom = false;
                left = x + barLeft;
                right = x + barRight;
                bottom = b;
                top = y;

                // account for negative bars
                if (top < bottom) {
                    tmp = top;
                    top = bottom;
                    bottom = tmp;
                    drawBottom = true;
                    drawTop = false;
                }
            }

            // clip
            if (right < axisx.min || left > axisx.max ||
                top < axisy.min || bottom > axisy.max) {
                return;
            }

            if (left < axisx.min) {
                left = axisx.min;
                drawLeft = false;
            }

            if (right > axisx.max) {
                right = axisx.max;
                drawRight = false;
            }

            if (bottom < axisy.min) {
                bottom = axisy.min;
                drawBottom = false;
            }

            if (top > axisy.max) {
                top = axisy.max;
                drawTop = false;
            }

            left = axisx.p2c(left);
            bottom = axisy.p2c(bottom);
            right = axisx.p2c(right);
            top = axisy.p2c(top);

            // fill the bar
            if (fillStyleCallback) {
                c.fillStyle = fillStyleCallback(bottom, top);
                c.fillRect(left, top, right - left, bottom - top)
            }

            // draw outline
            if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
                c.beginPath();

                // FIXME: inline moveTo is buggy with excanvas
                c.moveTo(left, bottom);
                if (drawLeft) {
                    c.lineTo(left, top);
                } else {
                    c.moveTo(left, top);
                }

                if (drawTop) {
                    c.lineTo(right, top);
                } else {
                    c.moveTo(right, top);
                }

                if (drawRight) {
                    c.lineTo(right, bottom);
                } else {
                    c.moveTo(right, bottom);
                }

                if (drawBottom) {
                    c.lineTo(left, bottom);
                } else {
                    c.moveTo(left, bottom);
                }

                c.stroke();
            }
        }

        /**
        - drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)

         This function is used for drawing series represented as bars. In case the series has decimation
         function attached, before starting to draw, as an optimization the points will first be decimated.

         The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
         plotHeight are the corresponding parameters of flot used to determine the drawing surface.
         The function getColorOrGradient is used to compute the fill style of bars.
        */
        function drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
            function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
                var points = datapoints.points,
                    ps = datapoints.pointsize,
                    fillTowards = series.bars.fillTowards || 0,
                    defaultBottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min;

                for (var i = 0; i < points.length; i += ps) {
                    if (points[i] == null) {
                        continue;
                    }

                    // Use third point as bottom if pointsize is 3
                    var bottom = ps === 3 ? points[i + 2] : defaultBottom;
                    drawBar(points[i], points[i + 1], bottom, barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
                }
            }

            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);

            var datapoints = {
                format: series.datapoints.format,
                points: series.datapoints.points,
                pointsize: series.datapoints.pointsize
            };

            if (series.decimate) {
                datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth);
            }

            ctx.lineWidth = series.bars.lineWidth;
            ctx.strokeStyle = series.color;

            var barLeft;
            var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
            switch (series.bars.align) {
                case "left":
                    barLeft = 0;
                    break;
                case "right":
                    barLeft = -barWidth;
                    break;
                default:
                    barLeft = -barWidth / 2;
            }

            var fillStyleCallback = series.bars.fill ? function(bottom, top) {
                return getFillStyle(series.bars, series.color, bottom, top, getColorOrGradient);
            } : null;

            plotBars(datapoints, barLeft, barLeft + barWidth, fillStyleCallback, series.xaxis, series.yaxis);
            ctx.restore();
        }

        function getFillStyle(filloptions, seriesColor, bottom, top, getColorOrGradient) {
            var fill = filloptions.fill;
            if (!fill) {
                return null;
            }

            if (filloptions.fillColor) {
                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
            }

            var c = $.color.parse(seriesColor);
            c.a = typeof fill === "number" ? fill : 0.4;
            c.normalize();
            return c.toString();
        }

        this.drawSeriesLines = drawSeriesLines;
        this.drawSeriesPoints = drawSeriesPoints;
        this.drawSeriesBars = drawSeriesBars;
        this.drawBar = drawBar;
    };

    $.plot.drawSeries = new DrawSeries();
})(jQuery);
