
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

(function (global) {

    var frameInsight = global.frameInsight = {};

    var DURATION_CHART_DURATION = 3000;
    var DURATION_CHART_PADDING_V = 0.05;
    var DURATION_CHART_PADDING_TOP = 0.05;
    var DURATION_CHART_PADDING_BOTTOM = 0.05;
    var DURATION_CHART_NORMAL_FILL = 'green';
    var DURATION_CHART_SLOW_FILL = 'red';
    var SLOW_THRESHOLD = 50;

    var settings;
    var dpr = window && Math.max(window.devicePixelRatio || 1, 1) || 1;
    var durationChart = {
        ticks: [],
        tickTypes: [],
        tags: []
    };
    var original = {
        setTimeout: global.setTimeout,
        requestAnimationFrame: global.requestAnimationFrame,
        addEventListener: global.addEventListener
    };

    var now = global.performance
        // performance.now has higer accuracy.
        ? performance.now.bind(performance)
        : function () {
            return +new Date();
        };

    instrumentBase();

    /**
     * @public
     * @param {Object} echarts
     * @param {string} durationChartDom
     */
    frameInsight.init = function (echarts, durationChartDom, dontInstrumentECharts) {
        settings = {
            echarts: echarts,
            durationChartDom: durationChartDom
        };

        !dontInstrumentECharts && instrumentECharts();
        initDurationChart();
        startDurationChart();
    };

    function startDurationChart() {
        next();

        function next() {
            renderDurationChart();
            original.requestAnimationFrame.call(global, next);
        }
    }

    function instrumentBase() {
        doInstrumentRegistrar('setTimeout', 0);
        doInstrumentRegistrar('requestAnimationFrame', 0);
        doInstrumentRegistrar('addEventListenter', 1);
    }

    function instrumentECharts() {
        var echarts = settings.echarts;

        var dummyDom = document.createElement('div');
        var dummyChart = echarts.init(dummyDom, null, {width: 10, height: 10});
        var ECClz = dummyChart.constructor;
        dummyChart.dispose();

        ECClz.prototype.setOption = doInstrumentHandler(ECClz.prototype.setOption, 'setOption');
    }

    function doInstrumentRegistrar(name, handlerIndex) {
        global[name] = function () {
            var args = [].slice.call(arguments);
            args[handlerIndex] = doInstrumentHandler(args[handlerIndex], name);
            return original[name].apply(this, args);
        };
    }

    function doInstrumentHandler(orginalHandler, tag) {
        return function () {
            var start = now();
            var result = orginalHandler.apply(this, arguments);
            var end = now();
            addTick(start, end, tag);
            return result;
        };
    }

    function addTick(start, end, tag) {
        var ticks = durationChart.ticks;
        var tickTypes = durationChart.tickTypes;
        var tags = durationChart.tags;

        // Arbitrary number.
        if (end - start > 0.3) {

            // In case that setOption in event listener.
            var lastIndex = tickTypes.length - 1;
            if (tickTypes[lastIndex] === 0) {
                tickTypes[lastIndex] = end;
                tags[lastIndex] = tag;
            }
            else {
                ticks.push(start, end);
                // 0: start, 1: end
                tickTypes.push(0, 1);
                tag = tag;
                tags.push(tag, tag);
            }
        }

        if (!ticks.length) {
            return;
        }

        var newStart = end - DURATION_CHART_DURATION;
        var dropCount = 0;
        for (var i = 0; i < ticks.length; i++) {
            var tick = ticks[i];
            if (tick < newStart) {
                dropCount++;
            }
        }
        if (dropCount > 0) {
            ticks.splice(0, dropCount);
            tickTypes.splice(0, dropCount);
            tags.splice(0, dropCount);
        }
    }

    function initDurationChart() {
        var dom = document.getElementById(settings.durationChartDom);
        var domStyle = dom.style;
        // domStyle.border = '2px solid #333';
        domStyle.boxShadow = '0 0 3px #000';
        domStyle.backgroundColor = '#eee';
        domStyle.padding = '0';
        domStyle.height = '60px';
        domStyle.margin = '10px 20px';

        var domWidth = getSize(dom, 0);
        var domHeight = getSize(dom, 1);

        durationChart.canvas = document.createElement('canvas');
        dom.appendChild(durationChart.canvas);
        durationChart.canvas.style.width = domWidth + 'px';
        durationChart.canvas.style.height = domHeight + 'px';
        durationChart.width = durationChart.canvas.width = domWidth * dpr;
        durationChart.height = durationChart.canvas.height = domHeight * dpr;

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

        var paddingV = durationChart.width * DURATION_CHART_PADDING_V;
        var paddingTop = durationChart.height * DURATION_CHART_PADDING_TOP;
        var paddingBottom = durationChart.height * DURATION_CHART_PADDING_BOTTOM;
        durationChart.bodyLeft = paddingV;
        durationChart.bodyWidth = durationChart.width - 2 * paddingV;
        durationChart.bodyTop = paddingTop;
        durationChart.bodyHeight = durationChart.height - paddingTop - paddingBottom;

        durationChart.renderExtent = [durationChart.bodyLeft, durationChart.bodyLeft + durationChart.bodyWidth];
        var extent = [0, DURATION_CHART_DURATION];
        durationChart.slowThresholdLength =
            linearMap(SLOW_THRESHOLD, extent, durationChart.renderExtent)
            - linearMap(0, extent, durationChart.renderExtent);
    }

    function renderDurationChart() {
        // var renderStart = now();

        var ticks = durationChart.ticks;
        var tickTypes = durationChart.tickTypes;
        var ctx = durationChart.ctx;
        var timeEnd = now();
        var timeExtent = [timeEnd - DURATION_CHART_DURATION, timeEnd];
        var slowThresholdLength = durationChart.slowThresholdLength;

        ctx.clearRect(0, 0, durationChart.width, durationChart.height);

        ctx.fillStyle = DURATION_CHART_NORMAL_FILL;

        var x;
        var slowRects = [];

        if (tickTypes[0] === 1) {
            x = durationChart.bodyLeft;
        }

        for (var i = 0; i < ticks.length; i++) {
            var tick = ticks[i];
            var tickType = tickTypes[i];

            var tickCoord = linearMap(tick, timeExtent, durationChart.renderExtent);
            if (tickType === 0) {
                x = tickCoord;
            }
            else if (tickType === 1) {
                var width = Math.max(tickCoord - x, 0.5);
                ctx.fillRect(x, durationChart.bodyTop, width, durationChart.bodyHeight);
                if (width > slowThresholdLength) {
                    slowRects.push(
                        x + slowThresholdLength,
                        durationChart.bodyTop,
                        width - slowThresholdLength,
                        durationChart.bodyHeight
                    );
                }
            }
        }

        if (slowRects.length) {
            for (var i = 0; i < slowRects.length;) {
                var x = slowRects[i++];
                var y = slowRects[i++];
                var width = slowRects[i++];
                var height = slowRects[i++];
                var canvasGradient = ctx.createLinearGradient(x, y, x + width, y);
                canvasGradient.addColorStop(0, DURATION_CHART_NORMAL_FILL);
                canvasGradient.addColorStop(1, DURATION_CHART_SLOW_FILL);
                ctx.fillStyle = canvasGradient;
                ctx.fillRect(x, y, width, height);
            }
        }

        // var renderDuration = now() - renderStart;
        // if (renderDuration > 1) {
            // console.warn(renderDuration);
        // }
    }

    function linearMap(val, domain, range) {
        var subDomain = domain[1] - domain[0];
        var subRange = range[1] - range[0];

        if (val <= domain[0]) {
            return range[0];
        }
        if (val >= domain[1]) {
            return range[1];
        }

        return (val - domain[0]) / subDomain * subRange + range[0];
    }

    function getSize(root, whIdx) {
        var wh = ['width', 'height'][whIdx];
        var cwh = ['clientWidth', 'clientHeight'][whIdx];
        var plt = ['paddingLeft', 'paddingTop'][whIdx];
        var prb = ['paddingRight', 'paddingBottom'][whIdx];

        // IE8 does not support getComputedStyle, but it use VML.
        var stl = document.defaultView.getComputedStyle(root);

        return (
            (root[cwh] || parseInt10(stl[wh]) || parseInt10(root.style[wh]))
            - (parseInt10(stl[plt]) || 0)
            - (parseInt10(stl[prb]) || 0)
        ) | 0;
    }

    function parseInt10(val) {
        return parseInt(val, 10);
    }

})(window);
