/**
 * @license Highcharts JS v6.0.1 (2017-10-05)
 * Boost module
 *
 * (c) 2010-2017 Highsoft AS
 * Author: Torstein Honsi
 *
 * License: www.highcharts.com/license
 */
'use strict';
(function(factory) {
    if (typeof module === 'object' && module.exports) {
        module.exports = factory;
    } else {
        factory(Highcharts);
    }
}(function(Highcharts) {
    (function(H) {
        /**
         * License: www.highcharts.com/license
         * Author: Torstein Honsi, Christer Vasseng
         * 
         * This is an experimental Highcharts module that draws long data series on a canvas
         * in order to increase performance of the initial load time and tooltip responsiveness.
         *
         * Compatible with HTML5 canvas compatible browsers (not IE < 9).
         *
         *
         * 
         * Development plan
         * - Column range.
         * - Heatmap. Modify the heatmap-canvas demo so that it uses this module.
         * - Treemap.
         * - Check how it works with Highstock and data grouping. Currently it only works when navigator.adaptToUpdatedData
         *   is false. It is also recommended to set scrollbar.liveRedraw to false.
         * - Check inverted charts.
         * - Check reversed axes.
         * - Chart callback should be async after last series is drawn. (But not necessarily, we don't do
        	 that with initial series animation).
         * - Cache full-size image so we don't have to redraw on hide/show and zoom up. But k-d-tree still
         *   needs to be built.
         * - Test IE9 and IE10.
         * - Stacking is not perhaps not correct since it doesn't use the translation given in 
         *   the translate method. If this gets to complicated, a possible way out would be to 
         *   have a simplified renderCanvas method that simply draws the areaPath on a canvas.
         *
         * If this module is taken in as part of the core
         * - All the loading logic should be merged with core. Update styles in the core.
         * - Most of the method wraps should probably be added directly in parent methods.
         *
         * Notes for boost mode
         * - Area lines are not drawn
         * - Point markers are not drawn on line-type series
         * - Lines are not drawn on scatter charts
         * - Zones and negativeColor don't work
         * - Initial point colors aren't rendered
         * - Columns are always one pixel wide. Don't set the threshold too low.
         *
         * Optimizing tips for users
         * - For scatter plots, use a marker.radius of 1 or less. It results in a rectangle being drawn, which is 
         *   considerably faster than a circle.
         * - Set extremes (min, max) explicitly on the axes in order for Highcharts to avoid computing extremes.
         * - Set enableMouseTracking to false on the series to improve total rendering time.
         * - The default threshold is set based on one series. If you have multiple, dense series, the combined
         *   number of points drawn gets higher, and you may want to set the threshold lower in order to 
         *   use optimizations.
         */


        var win = H.win,
            doc = win.document,
            noop = function() {},
            Color = H.Color,
            Series = H.Series,
            seriesTypes = H.seriesTypes,
            each = H.each,
            extend = H.extend,
            addEvent = H.addEvent,
            fireEvent = H.fireEvent,
            isNumber = H.isNumber,
            merge = H.merge,
            pick = H.pick,
            wrap = H.wrap,
            CHUNK_SIZE = 50000,
            destroyLoadingDiv;

        function eachAsync(arr, fn, finalFunc, chunkSize, i) {
            i = i || 0;
            chunkSize = chunkSize || CHUNK_SIZE;

            var threshold = i + chunkSize,
                proceed = true;

            while (proceed && i < threshold && i < arr.length) {
                proceed = fn(arr[i], i);
                i = i + 1;
            }
            if (proceed) {
                if (i < arr.length) {
                    setTimeout(function() {
                        eachAsync(arr, fn, finalFunc, chunkSize, i);
                    });
                } else if (finalFunc) {
                    finalFunc();
                }
            }
        }

        /*
         * Returns true if the chart is in series boost mode
         * @param chart {Highchart.Chart} - the chart to check
         * @returns {Boolean} - true if the chart is in series boost mode
         */
        function isChartSeriesBoosting(chart) {
            var threshold = (chart.options.boost ? chart.options.boost.seriesThreshold : 0) ||
                chart.options.chart.seriesBoostThreshold ||
                10;

            return chart.series.length >= threshold;
        }

        H.initCanvasBoost = function() {

            if (H.seriesTypes.heatmap) {
                H.wrap(H.seriesTypes.heatmap.prototype, 'drawPoints', function() {
                    var ctx = this.getContext();
                    if (ctx) {

                        // draw the columns
                        each(this.points, function(point) {
                            var plotY = point.plotY,
                                shapeArgs,
                                pointAttr;

                            if (plotY !== undefined && !isNaN(plotY) && point.y !== null) {
                                shapeArgs = point.shapeArgs;


                                pointAttr = point.series.pointAttribs(point);


                                ctx.fillStyle = pointAttr.fill;
                                ctx.fillRect(shapeArgs.x, shapeArgs.y, shapeArgs.width, shapeArgs.height);
                            }
                        });

                        this.canvasToSVG();

                    } else {
                        this.chart.showLoading('Your browser doesn\'t support HTML5 canvas, <br>please use a modern browser');

                        // Uncomment this to provide low-level (slow) support in oldIE. It will cause script errors on
                        // charts with more than a few thousand points.
                        // arguments[0].call(this);
                    }
                });
            }


            /**
             * Override a bunch of methods the same way. If the number of points is below the threshold,
             * run the original method. If not, check for a canvas version or do nothing.
             */
            // each(['translate', 'generatePoints', 'drawTracker', 'drawPoints', 'render'], function (method) {
            // 	function branch(proceed) {
            // 		var letItPass = this.options.stacking && (method === 'translate' || method === 'generatePoints');
            // 		if (((this.processedXData || this.options.data).length < (this.options.boostThreshold || Number.MAX_VALUE) ||
            // 				letItPass) || !isChartSeriesBoosting(this.chart)) {

            // 			// Clear image
            // 			if (method === 'render' && this.image) {
            // 				this.image.attr({ href: '' });
            // 				this.animate = null; // We're zooming in, don't run animation
            // 			}

            // 			proceed.call(this);

            // 		// If a canvas version of the method exists, like renderCanvas(), run
            // 		} else if (this[method + 'Canvas']) {

            // 			this[method + 'Canvas']();
            // 		}
            // 	}
            // 	wrap(Series.prototype, method, branch);

            // 	// A special case for some types - its translate method is already wrapped
            // 	if (method === 'translate') {
            // 		each(['arearange', 'bubble', 'column'], function (type) {
            // 			if (seriesTypes[type]) {
            // 				wrap(seriesTypes[type].prototype, method, branch);
            // 			}
            // 		});
            // 	}
            // });

            H.extend(Series.prototype, {
                directTouch: false,
                pointRange: 0,
                allowDG: false, // No data grouping, let boost handle large data 
                hasExtremes: function(checkX) {
                    var options = this.options,
                        data = options.data,
                        xAxis = this.xAxis && this.xAxis.options,
                        yAxis = this.yAxis && this.yAxis.options;
                    return data.length > (options.boostThreshold || Number.MAX_VALUE) && isNumber(yAxis.min) && isNumber(yAxis.max) &&
                        (!checkX || (isNumber(xAxis.min) && isNumber(xAxis.max)));
                },

                /**
                 * If implemented in the core, parts of this can probably be shared with other similar
                 * methods in Highcharts.
                 */
                destroyGraphics: function() {
                    var series = this,
                        points = this.points,
                        point,
                        i;

                    if (points) {
                        for (i = 0; i < points.length; i = i + 1) {
                            point = points[i];
                            if (point && point.graphic) {
                                point.graphic = point.graphic.destroy();
                            }
                        }
                    }

                    each(['graph', 'area', 'tracker'], function(prop) {
                        if (series[prop]) {
                            series[prop] = series[prop].destroy();
                        }
                    });
                },

                /**
                 * Create a hidden canvas to draw the graph on. The contents is later copied over 
                 * to an SVG image element.
                 */
                getContext: function() {
                    var chart = this.chart,
                        width = chart.chartWidth,
                        height = chart.chartHeight,
                        targetGroup = this.group,
                        target = this,
                        ctx,
                        swapXY = function(proceed, x, y, a, b, c, d) {
                            proceed.call(this, y, x, a, b, c, d);
                        };

                    if (isChartSeriesBoosting(chart)) {
                        target = chart;
                        targetGroup = chart.seriesGroup;
                    }

                    ctx = target.ctx;

                    if (!target.canvas) {
                        target.canvas = doc.createElement('canvas');

                        target.image = chart.renderer.image(
                            '',
                            0,
                            0,
                            width,
                            height
                        ).add(targetGroup);

                        target.ctx = ctx = target.canvas.getContext('2d');

                        if (chart.inverted) {
                            each(['moveTo', 'lineTo', 'rect', 'arc'], function(fn) {
                                wrap(ctx, fn, swapXY);
                            });
                        }

                        target.boostClipRect = chart.renderer.clipRect(
                            chart.plotLeft,
                            chart.plotTop,
                            chart.plotWidth,
                            chart.chartHeight
                        );

                        target.image.clip(target.boostClipRect);

                    } else if (!(target instanceof H.Chart)) {
                        // ctx.clearRect(0, 0, width, height);
                    }

                    if (target.canvas.width !== width) {
                        target.canvas.width = width;
                    }

                    if (target.canvas.height !== height) {
                        target.canvas.height = height;
                    }

                    target.image.attr({
                        x: 0,
                        y: 0,
                        width: width,
                        height: height,
                        style: 'pointer-events: none'
                    });

                    target.boostClipRect.attr({
                        x: 0,
                        y: 0,
                        width: chart.plotWidth,
                        height: chart.chartHeight
                    });

                    return ctx;
                },

                /** 
                 * Draw the canvas image inside an SVG image
                 */
                canvasToSVG: function() {
                    if (!isChartSeriesBoosting(this.chart)) {
                        this.image.attr({
                            href: this.canvas.toDataURL('image/png')
                        });
                    } else if (this.image) {
                        this.image.attr({
                            href: ''
                        });
                    }
                },

                cvsLineTo: function(ctx, clientX, plotY) {
                    ctx.lineTo(clientX, plotY);
                },

                renderCanvas: function() {
                    var series = this,
                        options = series.options,
                        chart = series.chart,
                        xAxis = this.xAxis,
                        yAxis = this.yAxis,
                        activeBoostSettings = chart.options.boost || {},
                        boostSettings = {
                            timeRendering: activeBoostSettings.timeRendering || false,
                            timeSeriesProcessing: activeBoostSettings.timeSeriesProcessing || false,
                            timeSetup: activeBoostSettings.timeSetup || false
                        },
                        ctx,
                        c = 0,
                        xData = series.processedXData,
                        yData = series.processedYData,
                        rawData = options.data,
                        xExtremes = xAxis.getExtremes(),
                        xMin = xExtremes.min,
                        xMax = xExtremes.max,
                        yExtremes = yAxis.getExtremes(),
                        yMin = yExtremes.min,
                        yMax = yExtremes.max,
                        pointTaken = {},
                        lastClientX,
                        sampling = !!series.sampling,
                        points,
                        r = options.marker && options.marker.radius,
                        cvsDrawPoint = this.cvsDrawPoint,
                        cvsLineTo = options.lineWidth ? this.cvsLineTo : false,
                        cvsMarker = r && r <= 1 ?
                        this.cvsMarkerSquare :
                        this.cvsMarkerCircle,
                        strokeBatch = this.cvsStrokeBatch || 1000,
                        enableMouseTracking = options.enableMouseTracking !== false,
                        lastPoint,
                        threshold = options.threshold,
                        yBottom = yAxis.getThreshold(threshold),
                        hasThreshold = isNumber(threshold),
                        translatedThreshold = yBottom,
                        doFill = this.fill,
                        isRange = series.pointArrayMap && series.pointArrayMap.join(',') === 'low,high',
                        isStacked = !!options.stacking,
                        cropStart = series.cropStart || 0,
                        loadingOptions = chart.options.loading,
                        requireSorting = series.requireSorting,
                        wasNull,
                        connectNulls = options.connectNulls,
                        useRaw = !xData,
                        minVal,
                        maxVal,
                        minI,
                        maxI,
                        kdIndex,
                        sdata = isStacked ? series.data : (xData || rawData),
                        fillColor = series.fillOpacity ?
                        new Color(series.color).setOpacity(pick(options.fillOpacity, 0.75)).get() :
                        series.color,

                        stroke = function() {
                            if (doFill) {
                                ctx.fillStyle = fillColor;
                                ctx.fill();
                            } else {
                                ctx.strokeStyle = series.color;
                                ctx.lineWidth = options.lineWidth;
                                ctx.stroke();
                            }
                        },

                        drawPoint = function(clientX, plotY, yBottom, i) {
                            if (c === 0) {
                                ctx.beginPath();

                                if (cvsLineTo) {
                                    ctx.lineJoin = 'round';
                                }
                            }

                            if (chart.scroller && series.options.className === 'highcharts-navigator-series') {
                                plotY += chart.scroller.top;
                                if (yBottom) {
                                    yBottom += chart.scroller.top;
                                }
                            } else {
                                plotY += chart.plotTop;
                            }

                            clientX += chart.plotLeft;

                            if (wasNull) {
                                ctx.moveTo(clientX, plotY);
                            } else {
                                if (cvsDrawPoint) {
                                    cvsDrawPoint(ctx, clientX, plotY, yBottom, lastPoint);
                                } else if (cvsLineTo) {
                                    cvsLineTo(ctx, clientX, plotY);
                                } else if (cvsMarker) {
                                    cvsMarker.call(series, ctx, clientX, plotY, r, i);
                                }
                            }

                            // We need to stroke the line for every 1000 pixels. It will crash the browser
                            // memory use if we stroke too infrequently.
                            c = c + 1;
                            if (c === strokeBatch) {
                                stroke();
                                c = 0;
                            }

                            // Area charts need to keep track of the last point
                            lastPoint = {
                                clientX: clientX,
                                plotY: plotY,
                                yBottom: yBottom
                            };
                        },

                        addKDPoint = function(clientX, plotY, i) {
                            // Avoid more string concatination than required
                            kdIndex = clientX + ',' + plotY;

                            // The k-d tree requires series points. Reduce the amount of points, since the time to build the 
                            // tree increases exponentially.
                            if (enableMouseTracking && !pointTaken[kdIndex]) {
                                pointTaken[kdIndex] = true;

                                if (chart.inverted) {
                                    clientX = xAxis.len - clientX;
                                    plotY = yAxis.len - plotY;
                                }

                                points.push({
                                    clientX: clientX,
                                    plotX: clientX,
                                    plotY: plotY,
                                    i: cropStart + i
                                });
                            }
                        };

                    // If we are zooming out from SVG mode, destroy the graphics
                    if (this.points || this.graph) {
                        this.destroyGraphics();
                    }

                    // The group
                    series.plotGroup(
                        'group',
                        'series',
                        series.visible ? 'visible' : 'hidden',
                        options.zIndex,
                        chart.seriesGroup
                    );

                    series.markerGroup = series.group;
                    // addEvent(series, 'destroy', function () {
                    // 	series.markerGroup = null;
                    // });

                    points = this.points = [];
                    ctx = this.getContext();
                    series.buildKDTree = noop; // Do not start building while drawing 

                    // Display a loading indicator
                    if (rawData.length > 99999) {
                        chart.options.loading = merge(loadingOptions, {
                            labelStyle: {
                                backgroundColor: H.color('#ffffff').setOpacity(0.75).get(),
                                padding: '1em',
                                borderRadius: '0.5em'
                            },
                            style: {
                                backgroundColor: 'none',
                                opacity: 1
                            }
                        });
                        clearTimeout(destroyLoadingDiv);
                        chart.showLoading('Drawing...');
                        chart.options.loading = loadingOptions; // reset
                    }

                    if (boostSettings.timeRendering) {
                        console.time('canvas rendering'); // eslint-disable-line no-console
                    }

                    // Loop over the points
                    eachAsync(sdata, function(d, i) {
                        var x,
                            y,
                            clientX,
                            plotY,
                            isNull,
                            low,
                            isNextInside = false,
                            isPrevInside = false,
                            nx = false,
                            px = false,
                            chartDestroyed = typeof chart.index === 'undefined',
                            isYInside = true;

                        if (!chartDestroyed) {
                            if (useRaw) {
                                x = d[0];
                                y = d[1];

                                if (sdata[i + 1]) {
                                    nx = sdata[i + 1][0];
                                }

                                if (sdata[i - 1]) {
                                    px = sdata[i - 1][0];
                                }
                            } else {
                                x = d;
                                y = yData[i];

                                if (sdata[i + 1]) {
                                    nx = sdata[i + 1];
                                }

                                if (sdata[i - 1]) {
                                    px = sdata[i - 1];
                                }
                            }

                            if (nx && nx >= xMin && nx <= xMax) {
                                isNextInside = true;
                            }

                            if (px && px >= xMin && px <= xMax) {
                                isPrevInside = true;
                            }

                            // Resolve low and high for range series
                            if (isRange) {
                                if (useRaw) {
                                    y = d.slice(1, 3);
                                }
                                low = y[0];
                                y = y[1];
                            } else if (isStacked) {
                                x = d.x;
                                y = d.stackY;
                                low = y - d.y;
                            }

                            isNull = y === null;

                            // Optimize for scatter zooming
                            if (!requireSorting) {
                                isYInside = y >= yMin && y <= yMax;
                            }

                            if (!isNull &&
                                (
                                    (x >= xMin && x <= xMax && isYInside) ||
                                    (isNextInside || isPrevInside)
                                )) {


                                clientX = Math.round(xAxis.toPixels(x, true));

                                if (sampling) {
                                    if (minI === undefined || clientX === lastClientX) {
                                        if (!isRange) {
                                            low = y;
                                        }
                                        if (maxI === undefined || y > maxVal) {
                                            maxVal = y;
                                            maxI = i;
                                        }
                                        if (minI === undefined || low < minVal) {
                                            minVal = low;
                                            minI = i;
                                        }

                                    }
                                    if (clientX !== lastClientX) { // Add points and reset
                                        if (minI !== undefined) { // then maxI is also a number
                                            plotY = yAxis.toPixels(maxVal, true);
                                            yBottom = yAxis.toPixels(minVal, true);
                                            drawPoint(
                                                clientX,
                                                hasThreshold ? Math.min(plotY, translatedThreshold) : plotY,
                                                hasThreshold ? Math.max(yBottom, translatedThreshold) : yBottom,
                                                i
                                            );
                                            addKDPoint(clientX, plotY, maxI);
                                            if (yBottom !== plotY) {
                                                addKDPoint(clientX, yBottom, minI);
                                            }
                                        }

                                        minI = maxI = undefined;
                                        lastClientX = clientX;
                                    }
                                } else {
                                    plotY = Math.round(yAxis.toPixels(y, true));
                                    drawPoint(clientX, plotY, yBottom, i);
                                    addKDPoint(clientX, plotY, i);
                                }
                            }
                            wasNull = isNull && !connectNulls;

                            if (i % CHUNK_SIZE === 0) {
                                series.canvasToSVG();
                            }
                        }

                        return !chartDestroyed;
                    }, function() {
                        var loadingDiv = chart.loadingDiv,
                            loadingShown = chart.loadingShown;
                        stroke();
                        series.canvasToSVG();

                        if (boostSettings.timeRendering) {
                            console.timeEnd('canvas rendering'); // eslint-disable-line no-console
                        }

                        fireEvent(series, 'renderedCanvas');

                        // Do not use chart.hideLoading, as it runs JS animation and will be blocked by buildKDTree.
                        // CSS animation looks good, but then it must be deleted in timeout. If we add the module to core,
                        // change hideLoading so we can skip this block.
                        if (loadingShown) {
                            extend(loadingDiv.style, {
                                transition: 'opacity 250ms',
                                opacity: 0
                            });
                            chart.loadingShown = false;
                            destroyLoadingDiv = setTimeout(function() {
                                if (loadingDiv.parentNode) { // In exporting it is falsy
                                    loadingDiv.parentNode.removeChild(loadingDiv);
                                }
                                chart.loadingDiv = chart.loadingSpan = null;
                            }, 250);
                        }

                        // Pass tests in Pointer. 
                        // Replace this with a single property, and replace when zooming in
                        // below boostThreshold.
                        series.directTouch = false;
                        series.options.stickyTracking = true;

                        delete series.buildKDTree; // Go back to prototype, ready to build
                        series.buildKDTree();

                        // Don't do async on export, the exportChart, getSVGForExport and getSVG methods are not chained for it.
                    }, chart.renderer.forExport ? Number.MAX_VALUE : undefined);
                }
            });

            wrap(Series.prototype, 'setData', function(proceed) {
                if (!this.hasExtremes || !this.hasExtremes(true) || this.type === 'heatmap') {
                    proceed.apply(this, Array.prototype.slice.call(arguments, 1));
                }
            });

            wrap(Series.prototype, 'processData', function(proceed) {
                if (!this.hasExtremes || !this.hasExtremes(true) || this.type === 'heatmap') {
                    proceed.apply(this, Array.prototype.slice.call(arguments, 1));
                }
            });

            seriesTypes.scatter.prototype.cvsMarkerCircle = function(ctx, clientX, plotY, r) {
                ctx.moveTo(clientX, plotY);
                ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false);
            };

            // Rect is twice as fast as arc, should be used for small markers
            seriesTypes.scatter.prototype.cvsMarkerSquare = function(ctx, clientX, plotY, r) {
                ctx.rect(clientX - r, plotY - r, r * 2, r * 2);
            };
            seriesTypes.scatter.prototype.fill = true;

            if (seriesTypes.bubble) {
                seriesTypes.bubble.prototype.cvsMarkerCircle = function(ctx, clientX, plotY, r, i) {
                    ctx.moveTo(clientX, plotY);
                    ctx.arc(clientX, plotY, this.radii && this.radii[i], 0, 2 * Math.PI, false);
                };
                seriesTypes.bubble.prototype.cvsStrokeBatch = 1;
            }

            extend(seriesTypes.area.prototype, {
                cvsDrawPoint: function(ctx, clientX, plotY, yBottom, lastPoint) {
                    if (lastPoint && clientX !== lastPoint.clientX) {
                        ctx.moveTo(lastPoint.clientX, lastPoint.yBottom);
                        ctx.lineTo(lastPoint.clientX, lastPoint.plotY);
                        ctx.lineTo(clientX, plotY);
                        ctx.lineTo(clientX, yBottom);
                    }
                },
                fill: true,
                fillOpacity: true,
                sampling: true
            });

            extend(seriesTypes.column.prototype, {
                cvsDrawPoint: function(ctx, clientX, plotY, yBottom) {
                    ctx.rect(clientX - 1, plotY, 1, yBottom - plotY);
                },
                fill: true,
                sampling: true
            });

            H.Chart.prototype.callbacks.push(function(chart) {
                function canvasToSVG() {
                    if (chart.image && chart.canvas) {
                        chart.image.attr({
                            href: chart.canvas.toDataURL('image/png')
                        });
                    }
                }

                function clear() {
                    if (chart.image) {
                        chart.image.attr({
                            href: ''
                        });
                    }

                    if (chart.canvas) {
                        chart.canvas.getContext('2d').clearRect(
                            0,
                            0,
                            chart.canvas.width,
                            chart.canvas.height
                        );
                    }
                }

                addEvent(chart, 'predraw', clear);
                addEvent(chart, 'render', canvasToSVG);
            });
        };

    }(Highcharts));
}));
