/* ************************************************************************

   Copyrigtht: OETIKER+PARTNER AG
   License:    GPLv3 or later
   Authors:    Tobias Oetiker
   Utf8Check:  äöü

************************************************************************ */

/**
 * Create a D3.js based, interactive chart.
 *
 */

var ID;

qx.Class.define("ep.visualizer.chart.BrowserChart", {
    extend : qx.ui.core.Widget,

    /**
     * @param instance {String} identifying the visualizer instance
     * @param recId {Number} stable identifier for the extopus node
     * @param chartDef {Array} chart description array
     *
     * <pre code='javascript'>
     * { [
     *       { cmd: 'LINE', width: 1, color, '#ff00ff', legend: 'text' },
     *       { cmd: 'AREA', stack: 1, color, '#df33ff', legend: 'more text' },
     *        ...
     *    ]
     * }
     * </pre>
     */
    construct : function(instance,recId,chartDef) {
        this.base(arguments);
        this._setLayout(new qx.ui.layout.VBox(10));
        this.set({
            instance: instance,
            recId: recId
        });
        // the chart
        var d3Obj = this.__d3Obj = new qxd3.Svg();
        var margin = this.self(arguments).MARGIN;
        this.__chart = d3Obj.getD3SvgNode()
        .append("g")
            .attr("transform", "translate("+margin.left+","+margin.top+")");

        this.__d3 = d3Obj.getD3();

        // add the svg object into the LoadingBox
        this._add(d3Obj,{flex: 1});

        if (chartDef){
            this.setChartDef(chartDef);
        }

        // insert our global CSS once only
        var CSS = this.self(arguments).BASECSS;
        var selector;
        for (selector in CSS){
            d3Obj.addCssRule(selector, CSS[selector]);
        }

        this.__dataNode = [];


        d3Obj.addListener('appear',this.setSize,this);

        d3Obj.addListener('resize',this.setSize,this);

        var timer = this.__timer = new qx.event.Timer(5 * 1000);

        d3Obj.addListener('disappear', function() {
            timer.stop()
        }, this);

        d3Obj.addListener('appear', function() {
            timer.start()
        }, this);

        timer.addListener('interval', function() {
            this.trackingRedraw();
        }, this);

        timer.start();
    },

    statics : {
        /**
         * Some basic css for the D3 chart <code>browserchart</code>
         */
        BASECSS: {
            'svg': {
                "font": "10px sans-serif"
            },
            '.axis': {
                "shape-rendering": 'crispEdges'
            },
            '.axis path, .axis line': {
                'fill': 'none',
                'stroke-width': '1px'
            },
            '.x.axis path': {
                'stroke': '#000'
            },
            '.x.axis line': {
                'stroke': '#000',
                'stroke-opacity': '.2',
                'stroke-dasharray': '1,1'
            },
            '.y.axis line': {
                'stroke': '#000',
                'stroke-opacity': '.2',
                'stroke-dasharray': '1,1'
            }
        },
        MARGIN: {
            left: 50,
            top: 10,
            bottom: 15,
            right: 10
        }
    },

    properties : {
        /**
         * inbytes, outbytes ...
         */
        view : {
            init     : null,
            nullable : true
        },
        /**
         * amount of time in the chart (in seconds)
         */
        timeWidth : {
            init     : null,
            check    : 'Integer',
            nullable : true
        },
        /**
         * at what point in time is the end of the chart, if no time is give the chart will update automatically
         */
        endTime : {
            init     : null,
            check    : 'Integer',
            nullable : true
        },
        /**
         * track current time
         */
        trackCurrentTime: {
            init     : null,
            check    : 'Boolean',
            nullable : true
        },
        /**
         * instance name of the table. This lets us identify ourselves when requesting data from the server
         */
        instance : {
            init     : null,
            nullable : true
        },
        /**
         * extopus Id aka recId ... to identify the chart we are talking about
         */
        recId: {
            init     : null,
            check    : 'Integer',
            nullable : true
         },
         /**
          * chart defs (chart definitions)
          */
        chartDef: {
            init     : [],
            check    : 'Array',
            nullable : true
        }

    },

    members : {
        __timer : null,
        __d3: null,
        __chart: null,
        __xAxisPainter: null,
        __yAxisPainter: null,
        __linePainter: null,
        __areaPainter: null,
        __xScale: null,
        __yScale: null,
        __zoomRectNode: null,
        __xAxisNode: null,
        __yAxisNode: null,
        __dataNode: null,
        __clipPath: null,
        __chartWidth: null,
        __fetchWait: null,
        __fetchAgain: null,
        __legendContainer: null,
        __d3Obj: null,
        __data: null,
        getDataArea: function(){
            if (this.__dataArea) return this.__dataArea;
            return this.__dataArea =
                this.__chart.insert("g",':first-child');
        },
        getXAxisPainter: function(){
            if (this.__xAxisPainter) return this.__xAxisPainter;
            var customTimeFormat = this.__d3.time.format.multi([
                [".%L", function(d) { return d.getMilliseconds(); }],
                [":%S", function(d) { return d.getSeconds(); }],
                ["%H:%M", function(d) { return d.getMinutes(); }],
                ["%H:%M", function(d) { return d.getHours(); }],
                ["%m-%d", function(d) { return d.getDay() && d.getDate() != 1; }],
                ["%Y-%m-%d", function(d) { return true }]
            ]);
            this.__xAxisPainter = this.__d3.svg.axis()
                .scale(this.getXScale())
                .orient("bottom")
                .tickPadding(6)
                .tickFormat(customTimeFormat);

            return this.__xAxisPainter;
        },

        getYAxisPainter: function(){
            if (this.__yAxisPainter) return this.__yAxisPainter;
            var si = ['p','n','y','m','','k','M','G','T','P'];
            var d3 = this.__d3;
            var commasFormatter = d3.format(",.1f");
            this.__yAxisPainter = d3.svg.axis()
                .scale(this.getYScale())
                .orient("left")
                .tickPadding(6)
                .tickFormat(function(d){
                    if (d == 0){
                        return d;
                    }
                    var log = Math.log(d)/Math.LN10;
                    var idx = log < 0 ? Math.ceil(log/3) : Math.floor(log/3);
                    var factor = Math.pow(10,idx*3);
                    return commasFormatter(d/factor) + si[idx+4];
                });

            return this.__yAxisPainter;
        },

        getXScale: function(){
            if (this.__xScale) return this.__xScale;
            this.__xScale = this.__d3.time.scale();
            this.__xScale.domain([new Date(new Date().getTime() - 24*3600*1000),new Date()]);
            return this.__xScale;
        },

        getYScale: function(){
            if (this.__yScale) return this.__yScale;
            this.__yScale = this.__d3.scale.linear();
            return this.__yScale;
        },

        getDataPainter: function(id){
            switch(this.getChartDef()[id].cmd){
                case 'LINE':
                    return this.getLinePainter();
                    break;
                case 'AREA':
                    return this.getAreaPainter();
                    break;
                default:
                    this.debug("invalid cmd");
                    break;
            }
        },

        getLinePainter: function(){
            if (this.__linePainter) return this.__linePainter;
            var xScale = this.getXScale();
            var yScale = this.getYScale();
            this.__linePainter = this.__d3.svg.line()
                .interpolate("step-before")
                .x(function(d){ return xScale(d.date); })
                .y(function(d){ return yScale(d.y); })
                .defined(function(d){ return d.d });
            return this.__linePainter;
        },

        getAreaPainter: function(){
            if (this.__areaPainter) return this.__areaPainter;
            var xScale = this.getXScale();
            var yScale = this.getYScale();
            this.__areaPainter = this.__d3.svg.area()
                .interpolate("step-before")
                .x(function(d){ return xScale(d.date); })
                .y0(function(d){ return yScale(d.y0);})
                .y1(function(d){ return yScale(d.y); })
                .defined(function(d){ return d.d });
            return this.__areaPainter;
        },

        getZoomRectNode: function(){
            if (this.__zoomRectNode) return this.__zoomRectNode;
            var that = this;
            var zoomer = this.__zoomer = this.__d3.behavior.zoom()
                .scaleExtent([0.0001, 1000])
                .on("zoom", function (){that.redraw()});

            zoomer.x(this.getXScale());

            this.__zoomRectNode = this.__chart.append("rect")
                .style({
                    cursor: 'move',
                    fill: 'none',
                    'pointer-events': 'all'
                })
                .call(zoomer);

            return this.__zoomRectNode;
        },

        getXAxisNode: function(){
            if (this.__xAxisNode) return this.__xAxisNode;
            this.__xAxisNode = this.__chart.append("g")
                .attr("class", "x axis");
            return this.__xAxisNode;
        },

        getYAxisNode: function(){
            if (this.__yAxisNode) return this.__yAxisNode;
            this.__yAxisNode = this.__chart.append("g")
                .attr("class", "y axis");
            return this.__yAxisNode;
        },

        getClipPath: function(){
            if (this.__clipPath) return this.__clipPath;
            ID++;
            this.__clipPathID = this.getInstance() + '-clipPath-' + ID;
            this.__clipPath = this.__chart.append("clipPath")
                .attr('id',this.__clipPathID)
                .append("rect");
            return this.__clipPath;
        },

        getDataNode: function(id){
            if (this.__dataNode[id]) return this.__dataNode[id];
            var chartDef = this.getChartDef()[id];
            var dataArea = this.getDataArea();
            switch(chartDef.cmd){
                case 'LINE':
                    this.__dataNode[id] = dataArea.append("path")
                    .attr("clip-path", "url(#"+this.__clipPathID+")")
                    .attr('stroke', chartDef.color)
                    .attr('stroke-width', 2)
                    .attr("fill", "none")
                    .attr("shape-rendering", 'crispEdges');
                    break;
                case 'AREA':
                    this.__dataNode[id] = dataArea.append("path")
                    .attr("clip-path", "url(#"+this.__clipPathID+")")
                    .attr("fill",chartDef.color);
                    break;
                default:
                    this.debug("unsupported cmd:" + chartDef.cmd);
                    break;
            }
            return this.__dataNode[id];
        },

        resetChart: function(newValue, oldValue, propertyName){
            if (this.__dataNode){
                for (var i=0;i<this.__dataNode.length;i++){
                    this.__dataNode[i].remove();
                }
            }
            this.__dataNode = [];
            this.__data = null;
            this.setupLegend();
        },
        setupLegend: function(){
            var lc;
            if (! this.__legendContainer ) {
                var margin = this.self(arguments).MARGIN;
                lc = this.__legendContainer = new qx.ui.core.Widget().set({
                    paddingLeft: margin.left
                });
                lc._setLayout(new qx.ui.layout.Flow(15,5));
                this._add(lc);
            }
            else {
                lc = this.__legendContainer;
                lc._removeAll();
            }
            this.getChartDef().forEach(function(item){
                var label = new qx.ui.core.Widget();
                label._setLayout(new qx.ui.layout.HBox(5).set({alignY: 'middle'}));
                var cu = qx.util.ColorUtil;
                var borderColorHsb = cu.rgbToHsb(cu.hex6StringToRgb(item.color));
                borderColorHsb[2] *= 0.9;
                var borderColor = cu.rgbToHexString(cu.hsbToRgb(borderColorHsb));
                var box = new qx.ui.core.Widget().set({
                    width: 12,
                    height: 12,
                    allowGrowX: false,
                    allowGrowY: false,
                    marginBottom: 6,
                    decorator: new qx.ui.decoration.Decorator().set({
                        color: [borderColor],
                        width: [1],
                        style: ['solid'],
                        backgroundColor: item.color
                    })
                });
                label._add(box);
                var legend = new qx.ui.basic.Label().set({
                    value: item.legend
                });
                label._add(legend);
                lc._add(label);
            });
        },
        setSize: function(){
            var el = this.__d3Obj.getContentElement().getDomElement();
            if (!el) return;
            // sync screen before we measure things
            qx.html.Element.flush();
            var margin = this.self(arguments).MARGIN;
            var width = qx.bom.element.Dimension.getWidth(el) - margin.left - margin.right;
            var height = qx.bom.element.Dimension.getHeight(el) - margin.top - margin.bottom;
            this.__chartWidth = width;

            var xScale = this.getXScale();
            var yScale = this.getYScale();

            xScale.range([0,width]);
            yScale.range([height,0]);


            this.getClipPath()
                .attr("width",width)
                .attr("height",height);

            this.getYAxisPainter()
                .tickSize(width)
                .ticks(Math.round(height/50));

            this.getXAxisPainter().tickSize(-height,0);

            for (var i=0;i<this.getChartDef().length;i++){
                this.getDataNode(i);
            }
            this.getYAxisNode()
                .attr("transform", "translate(" + width + ",0)");
            this.getXAxisNode()
                .attr("transform", "translate(0," + height + ")");

            this.getZoomRectNode()
                .attr('width',width)
                .attr('height',height);

            this.__zoomer.size([width,height]);
            this.redraw();
        },

        trackingRedraw: function(){
            if (this.getTrackCurrentTime()){
                var dates = this.getXScale().domain();
                var interval = dates[1].getTime() - dates[0].getTime();
                dates[1] = new Date();
                dates[0] = new Date(dates[1].getTime() - interval);
                this.getXScale().domain(dates);
                this.redraw();
            }
        },

        yScaleRedraw: function(){
            var dates = this.getXScale().domain();
            if (this.__data){
                var maxValue = 0;
                for(var i=0;i<this.getChartDef().length;i++){
                    if (!this.__data.data[i]) continue;
                    this.__data.data[i].forEach(function(item){
                        if (item.y > maxValue && item.date >= dates[0] && item.date <= dates[1]){
                            maxValue = item.y;
                        }
                    })
                }
                this.getYScale().domain([0,maxValue]).nice();

                this.getYAxisNode().call(this.getYAxisPainter());

                for(var i=0;i<this.getChartDef().length;i++){
                    var node = this.getDataNode(i);
                    if (node.data().length > 0){
                        node.attr("d",this.getDataPainter(i));
                    }
                }
            }
        },

        redraw: function(){
            var dates = this.getXScale().domain();
            var start = Math.round(dates[0].getTime()/1000);
            var end = Math.round(dates[1].getTime()/1000);
            var dataStep = Math.round((end-start)/ this.__chartWidth);
            var extra = Math.round(end - start );
            var d3 = this.__d3;

            start -= extra;
            end += extra;


            this.yScaleRedraw();

            this.getXAxisNode().call(this.getXAxisPainter());


            if (this.__fetchWait){
                this.__fetchAgain = 1;
                return;
            }

            var existingData = this.dataSlicer(start,end,dataStep);
            if (existingData.missingStart == existingData.missingEnd){
                return;
            }

            var needChartDef = this.getChartDef().length == 0;

            var rpc = ep.data.Server.getInstance();
            var that = this;
            this.__fetchWait = 1;
            rpc.callAsyncSmart(function(ret){

                var d3Data = that.__data = that.d3DataTransformer(ret,dataStep);

                var maxValue = 0;

                for(var i=0;i<ret.length;i++){
                    var dataNode = that.getDataNode(i);
                    if (existingData.prepend){
                        d3Data.data[i] = existingData.prepend[i].concat(d3Data.data[i],existingData.append[i]);
                    }
                    dataNode.data([d3Data.data[i]]);
                }

                that.yScaleRedraw();
                that.__fetchWait = 0;
                // if we skipped one, lets redraw again just to be sure we got it all
                if (this.__fetchAgain == 1){
                    qx.event.Timer.once(that.redraw,that,0);
                    this.__fetchAgain = 0;
                }
            },
            'visualize', this.getInstance(), {
                recId    : this.getRecId(),
                step     : dataStep,
                start    : existingData.missingStart,
                end      : existingData.missingEnd,
                view     : this.getView()
            });
        },

        /* figure out what data range we are missing and keep the bits we already have */

        dataSlicer: function(start,end,dataStep){
            var oldData = this.__data;
            var missingStart = start;
            var missingEnd = end;
            var prepend = [];
            var append = [];
            var keepData = oldData != null && typeof(oldData) == 'object' && Math.round(oldData.dataStep) == Math.round(dataStep);
            if (!keepData) {
                return { missingStart: missingStart, missingEnd: missingEnd};
            }
            var oldStart = oldData.data[0][0].date.getTime()/1000;
            var oldEnd = oldData.data[0][oldData.data[0].length-1].date.getTime()/1000;
            // prepend the existing data to the new data
            var prependMode = oldStart <= start && oldEnd >= start;
            var appendMode = oldEnd >= end && oldStart <= end;
            for (var i=0;i<oldData.data.length;i++){
                append[i] = [];
                prepend[i] = [];
                var len = oldData.data[i].length;
                for (var ii=0;ii<len;ii++){
                    var item = oldData.data[i][ii];
                    var date = item.date.getTime()/1000;
                    if ( prependMode && date >= start ) {
                        prepend[i].push(item);
                        missingStart = date+dataStep;
                    }
                    if (appendMode && date <= end ){
                        append[i].push(item);
                        if ( date < missingEnd ) {
                            missingEnd = date-dataStep;
                        }
                    }
                }
            }
            /* lets make sure don't trip over ourselves */
            if (missingStart > missingEnd){
                this.debug("missingStart:"+missingStart+" missingEnd:"+missingEnd);
                missingEnd = missingStart;
            }

            return {
                missingStart: missingStart,
                missingEnd: missingEnd,
                append: append,
                prepend: prepend
            }
        },

        d3DataTransformer: function(data,dataStep){
            var d3 = this.__d3;
            if (data == null || typeof(data) != 'object' || data[0] == null){
                return null;
            }
            var minStep = 24*3600;
            var minStart = (new Date).getTime()/1000;
            data.forEach(function(d){
                if (d.status != 'ok') return;
                if (minStep == null || minStep > d.step){
                    minStep = d.step;
                    minStart = d.start;
                }
            });

            var d3Data = [];
            for (var i=0; i<data.length;i++){
                d3Data[i] = [];
                var chartDef = this.getChartDef()[i];
                if (data[i].status != 'ok') {
                    data[i].values = [NaN];
                    data[i].step = 3600*24;
                }
                var stack = chartDef.stack;
                var len = data[i].values.length;
                var st = minStep/data[i].step;
                for (var ii=0;ii*st < len ; ii++){
                    var y0 = 0;
                    if (stack && i > 0 && d3Data[i-1][ii] != null && typeof(d3Data[i-1][ii]) == 'object'){
                        y0 = d3Data[i-1][ii].y;
                    }
                    var yval = parseFloat(data[i].values[Math.round(ii*st)]);
                    d3Data[i][ii] = {
                        y: (isNaN(yval) ? 0 : yval) +y0,
                        y0: y0,
                        d: !isNaN(yval),
                        date: new Date((minStart+ii*minStep)*1000)
                    }
                }
            }
            return {data:d3Data,dataStep: dataStep}
        }
    },

    destruct : function() {
        this.__timer.stop();
    }
});