/*
 *  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.
 */

$(window).on('load',(function () {
    // for each record, unroll the array pointed to by "fieldpath" into a new
    // record for each element of the array
    function unnest (table, fieldpath, dest) {
        var faccess = accessor(fieldpath);
        return $.map(table, function (record, index) {
            var ra = [];
            var nested = faccess(record);
            for (var i = 0; i < nested.length; i++) {
                var newrec = $.extend({}, record);
                newrec[dest] = nested[i];
                ra.push(newrec);
            }
            return ra;
        });
    }

    // for each record, project "fieldpath" into "dest".
    function extract (table, fieldpath, dest) {
        var faccess = accessor(fieldpath);
        return $.map(table, function (record, index) {
            var newrec = $.extend({}, record);
            newrec[dest] = faccess(newrec);
            return newrec;
        });
    }

    // creates a function that will traverse tree of objects based the '.'
    // delimited "path"
    function accessor (path) {
        path = path.split(".");
        return function (obj) {
            for (var i = 0; i < path.length; i++)
                obj = obj[path[i]];
            return obj;
        }
    }

    // sample use of unnest/extract
    function extractminortimes (profile) {
        var t1 = unnest([profile], "fragmentProfile", "ma");
        var t2 = unnest(t1, "ma.minorFragmentProfile", "mi");

        var timetable = $.map(t2, function (record, index) {
            var newrec = {
                "name" : record.ma.majorFragmentId + "-" +
                    record.mi.minorFragmentId,
                "category" : record.ma.majorFragmentId,
                "start" : (record.mi.startTime - record.start) / 1000.0,
                "end" : (record.mi.endTime - record.start) / 1000.0
            };
            return newrec;
        });

        timetable.sort(function (r1, r2) {
            if (r1.category == r2.category) {
                //return r1.name > r2.name;
                return r1.end - r1.start > r2.end - r2.start ? 1 : -1;
            }
            else return r1.category > r2.category ? 1 : -1;

        });
        return timetable;
    }

    // write the "fieldpaths" for the table "table" into the "domtable"
    function builddomtable (domtable, table, fieldpaths) {
        var faccessors = $.map(fieldpaths, function (d, i) {
            return accessor(d);
        });

        var domrow = domtable.append("tr");
        for (var i = 0; i < fieldpaths.length; i++)
            domrow.append("th").text(fieldpaths[i]);
        for (var i = 0; i < table.length; i++) {
            domrow = domtable.append("tr");
            for (var j = 0; j < faccessors.length; j++)
                domrow.append("td").text(faccessors[j](table[i]));
        }
    }

    // parse the short physical plan into a dagreeD3 structure
    function parseplan (planstring) {
        //Map for implicit links
        var implicitSrcMap = {};
        var g = new dagreD3.Digraph();
        //Produce 2D array (3 x M): [[0:majorMinor] [1:] [2:opName]] / [[<major>-<minor>, "<indent>", opName]]
        let opPlanArray = planstring.trim().split("\n");
        var operatorRegex = new RegExp("^([0-9-]+)( *)([a-zA-Z]*)");
        //Regex to capture source operator 
        var srcOpRegex = new RegExp("srcOp=[0-9-]+");
        var opTuple = $.map(opPlanArray, 
            function (lineStr) { 
              //Tokenize via Regex
              let opToken = operatorRegex.exec(lineStr).slice(1);
              //Extract Implicit Source via Regex
              let implicitSrc = srcOpRegex.exec(lineStr);
              let srcOp = null;
              let tgtOp = opToken[0];
              if (implicitSrc != null) {
                srcOp = implicitSrc.toString().split("=")[1];
                implicitSrcMap[tgtOp]=srcOp;
              }
              return [opToken];
            });


        // parse, build & inject nodes
        for (var i = 0; i < opTuple.length; i++) {
            let majorMinor = opTuple[i][0];
            let majorId = parseInt(majorMinor.split("-")[0]);
            let opName = opTuple[i][2];
            g.addNode(majorMinor, {
                label: opName + " " + majorMinor,
                fragment: majorId
            });
        }

        // Define edges by traversing graph from root node (Screen)
        //NOTE: The indentation value in opTuple which is opTuple[1] represents the relative level of each operator in tree w.r.t root operator
        var nodeStack = [opTuple[0]]; //Add Root
        for (var i = 1; i < opTuple.length; i++) {
            let top = nodeStack.pop();
            let currItem = opTuple[i];
            let stackTopIndent = top[1].length;
            let currItemIndent = currItem[1].length; //Since tokenizing gives indent size at index 1
            //Compare top-of-stack indent with current iterItem indent
            while (stackTopIndent >= currItemIndent) {
                top = nodeStack.pop();
                stackTopIndent = top[1].length;
            }

            //Found parent
            //Add edge if Implicit src exists
            //Refer: https://dagrejs.github.io/project/dagre-d3/latest/demo/style-attrs.html / https://github.com/d3/d3-shape#curves
            if (implicitSrcMap[currItem[0]] != null) {
                //Note: Order matters because it affects layout (currently: BT)
                //Ref: //https://github.com/dagrejs/dagre/issues/116
                g.addEdge(null, currItem[0], implicitSrcMap[currItem[0]], {
                    style: "fill:none; stroke:gray; stroke-width:3px; stroke-dasharray: 5,5;marker-end:none"
                });
                
            }
            //Adding edge
            g.addEdge(null, currItem[0], top[0]);

            if (currItemIndent != stackTopIndent)
                nodeStack.push(top);
            if (currItemIndent >= stackTopIndent)
                nodeStack.push(currItem);
        }

        return g;
    }

    // graph a "planstring" into the d3 svg handle "svg"
    function buildplangraph (svg, planstring) {
        var padding = 20;
        var graph = parseplan(planstring);

        var renderer = new dagreD3.Renderer();
        renderer.zoom(function () {return function (graph, root) {}});

        var oldDrawNodes = renderer.drawNodes();
        renderer.drawNodes(function(graph, root) {
            var svgNodes = oldDrawNodes(graph, root);
            svgNodes.each(function(u) {
                var fc = d3.rgb(globalconfig.majorcolorscale(graph.node(u).fragment));
                d3.select(this).select("rect")
                    .style("fill", graph.node(u).label.split(" ")[0].endsWith("Exchange") ? "white" : fc)
                    .style("stroke", "#000")
                    .style("stroke-width", "1px")
            });
            return svgNodes;
        });

        var oldDrawEdgePaths = renderer.drawEdgePaths();
        renderer.drawEdgePaths(function(graph, root) {
            var svgEdgePaths = oldDrawEdgePaths(graph, root);
            svgEdgePaths.each(function(u) {
                d3.select(this).select("path")
                    .style("fill", "none")
                    .style("stroke", "#000")
                    .style("stroke-width", "1px")
            });
            return svgEdgePaths;
        });

        var shiftedgroup = svg.append("g")
            .attr("transform", "translate(" + padding + "," + padding + ")");
        var layout = dagreD3.layout().nodeSep(20).rankDir("BT");
        var result = renderer.layout(layout).run(graph, shiftedgroup);

        svg.attr("width", result.graph().width + 2 * padding)
            .attr("height", result.graph().height + 2 * padding);
    }

    // Fragment Gantt Chart
    function buildtimingchart (svgdest, timetable) {
        var chartprops = {
            "w" : 850,
            "h" : 70,
            "svg" : svgdest,
            "bheight" : 2,
            "bpad" : 0,
            "margin" : 35,
            "scaler" : null,
            "colorer" : null,
        };

        chartprops.h = timetable.length * (chartprops.bheight + chartprops.bpad * 2)

        chartprops.svg
            .attr("width", chartprops.w + 2 * chartprops.margin)
            .attr("height", chartprops.h + 2 * chartprops.margin)
            .attr("class", "svg");

        chartprops.scaler = d3.scale.linear()
            .domain([d3.min(timetable, accessor("start")),
                     d3.max(timetable, accessor("end"))])
            .range([0, chartprops.w - chartprops.bpad * 2]);
        chartprops.colorer = globalconfig.majorcolorscale;

        // backdrop
        chartprops.svg.append("g")
          .selectAll("rect")
            .data(timetable)
            .enter()
          // TODO: Prototype to jump to related Major Fragment
          //.append("a")
          //  .attr("xlink:href", function(d) {
          //     return "#fragment-" + parseInt(d.category);
          //   })
          .append("rect")
            .attr("x", 0)
            .attr("y", function(d, i) {return i * (chartprops.bheight + 2 * chartprops.bpad);})
            .attr("width", chartprops.w)
            .attr("height", chartprops.bheight + chartprops.bpad * 2)
            .attr("stroke", "none")
            .attr("fill", function(d) {return d3.rgb(chartprops.colorer(d.category));})
            .attr("opacity", 0.1)
            .attr("transform", "translate(" + chartprops.margin + "," +
                  chartprops.margin + ")")
            .attr("style", "cursor: pointer;")
          .append("title")
            .text(function(d) {
                var id = parseInt(d.category);
                return ((id < 10) ? ("0" + id) : id) + "-XX-XX";
             });

        // bars
        chartprops.svg.append('g')
          .selectAll("rect")
            .data(timetable)
            .enter()
          // TODO: Prototype to jump to related Major Fragment
          //.append("a")
          //  .attr("xlink:href", function(d) {
          //     return "#fragment-" + parseInt(d.category);
          //   })
          .append("rect")
             //.attr("rx", 3)
             //.attr("ry", 3)
            .attr("x", function(d) {return chartprops.scaler(d.start) + chartprops.bpad;})
            .attr("y", function(d, i) {return i * (chartprops.bheight + 2 * chartprops.bpad) + chartprops.bpad;})
            .attr("width", function(d) {return (chartprops.scaler(d.end) - chartprops.scaler(d.start));})
            .attr("height", chartprops.bheight)
            .attr("stroke", "none")
            .attr("fill", function(d) {return d3.rgb(chartprops.colorer(d.category));})
            .attr("transform", "translate(" + chartprops.margin + "," +
                  chartprops.margin + ")")
            .attr("style", "cursor: pointer;")
          .append("title")
            .text(function(d) {
                var id = parseInt(d.category);
                return ((id < 10) ? ("0" + id) : id) + "-XX-XX";
            });

        // grid lines
        chartprops.svg.append("g")
            .attr("transform", "translate(" +
                  (chartprops.bpad + chartprops.margin) + "," +
                  (chartprops.h + chartprops.margin) + ")")
            .attr("class", "grid")
            .call(d3.svg.axis()
                  .scale(chartprops.scaler)
                  .tickSize(-chartprops.h, 0)
                  .tickFormat(""))
            .style("stroke", "#000")
            .style("opacity", 0.2);

        // ticks
        chartprops.svg.append("g")
            .attr("transform", "translate(" +
                  (chartprops.bpad + chartprops.margin) + "," +
                  (chartprops.h + chartprops.margin) + ")")
            .attr("class", "grid")
            .call(d3.svg.axis()
                  .scale(chartprops.scaler)
                  .orient('bottom')
                  .tickSize(0, 0)
                  .tickFormat(d3.format(".2f")));

        // X Axis Label (Centered above graph)
        chartprops.svg.append("text")
            .attr("transform", "rotate(0)")
            .attr("y", 0)
            .attr("x", chartprops.w/2 + chartprops.margin)
            .attr("dy", "1.5em")
            .style("text-anchor", "middle")
            .text("Elapsed Timeline");

        //Y Axis (center of y axis)
        chartprops.svg.append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 0)
            // Aligning to center of Y-axis with minimum Svg height
            .attr("x",0 - Math.max(chartprops.h/2 + chartprops.margin, 36))
            .attr("dy", "1.5em")
            .style("text-anchor", "middle")
            .html("Fragments"); 
    }

    function loadprofile (queryid, callback) {
        $.ajax({
            type: "GET",
            dataType: "json",
            url: "/profiles/" + queryid + ".json",
            success: callback,
            error: function (x, y, z) {
                console.log(x);
                console.log(y);
                console.log(z);
            }
        });
    }

    function setupglobalconfig (profile) {
        globalconfig.profile = profile;
        globalconfig.majorcolorscale = d3.scale.category20()
            .domain([0, d3.max(profile.fragmentProfile, accessor("majorFragmentId"))]);

    }

    String.prototype.endsWith = function(suffix) {
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };

    loadprofile(globalconfig.queryid, function (profile) {
        setupglobalconfig(profile);

        var queryvisualdrawn = false;
        var timingoverviewdrawn = false;
        var jsonprofileshown = false;

        // trigger svg drawing when visible
        $('#query-tabs').on('shown.bs.tab', function (e) {
            if (queryvisualdrawn || !e.target.href.endsWith("#query-visual")) return;
            buildplangraph(d3.select("#query-visual-canvas"), profile.plan);
            queryvisualdrawn = true;
        })
        $('#fragment-accordion').on('shown.bs.collapse', function (e) {
            if (timingoverviewdrawn || e.target.id != "fragment-overview") return;
            buildtimingchart(d3.select("#fragment-overview-canvas"), extractminortimes(profile));
            timingoverviewdrawn = true;
        });

        // select default tabs
        $('#fragment-overview').collapse('show');
        $('#operator-overview').collapse('show');
        $('#query-tabs a[href="#query-query"]').tab('show');


        // add json profile on click
        $('#full-json-profile-json').on('shown.bs.collapse', function (e) {
            if (jsonprofileshown) return;
            $('#full-json-profile-json').html(JSON.stringify(globalconfig.profile, null, 4));
        });

        //builddomtable(d3.select("#timing-table")
        //            .append("tbody"), extractminortimes(profile),
        //            ["name", "start", "end"]);
    });
}));
