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

// Inspired by
// https://github.com/samizdatco/arbor/blob/master/docs/sample-project/main.js

function renderGraph(elem) {

    var canvas = $(elem).get(0);
    canvas.width = $(window).width();
    canvas.height = $(window).height();
    var ctx = canvas.getContext("2d");
    var gfx = arbor.Graphics(canvas);
    var psys;

    var totaltrans = 0;
    var weights = {};
    var texts = {};
    var update = false;

    var myRenderer = {
        init: function(system){ 
            psys = system;
            psys.screenSize(canvas.width, canvas.height)
            psys.screenPadding(20);
            myRenderer.initMouseHandling();
        },

        signal_update: function() {
            update = true;
        },

        redraw: function() { 
            
            if(!psys)
                return;
            
            if(update) {
                totaltrans = calculate_total_transmitted(psys);
                weights = calculate_weights(psys, totaltrans);
                texts = calculate_texts(psys, totaltrans);
                update = false;
            }



            ctx.fillStyle = "white";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            var x = 0;
            

            psys.eachEdge(function(edge, pt1, pt2) {

                var len = Math.sqrt(Math.pow(pt2.x - pt1.x,2) + Math.pow(pt2.y - pt1.y,2));
                var sublen = len - (Math.max(50, 20 + gfx.textWidth(edge.target.name)) / 2);
                var thirdlen = len/3;
                var theta = Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x);
                
                var newpt2 = {
                    x : pt1.x + (Math.cos(theta) * sublen),
                    y : pt1.y + (Math.sin(theta) * sublen)
                };

                var thirdpt = {
                    x: pt1.x + (Math.cos(theta) * thirdlen),
                    y: pt1.y + (Math.sin(theta) * thirdlen)
                }

                weight = weights[edge.source.name + edge.target.name];
                
                if(!weights[edge.source.name + edge.target.name])
                {
                    totaltrans = calculate_total_transmitted(psys);
                    weights = calculate_weights(psys, totaltrans);
                }

                ctx.strokeStyle = "rgba(0,0,0, .333)";
                ctx.lineWidth = 25 * weight + 5;
                ctx.beginPath();

                var arrlen = 15;
                ctx.moveTo(pt1.x, pt1.y);
                ctx.lineTo(newpt2.x, newpt2.y);
                ctx.lineTo(newpt2.x - arrlen * Math.cos(theta-Math.PI/6), newpt2.y - arrlen * Math.sin(theta - Math.PI/6));
                ctx.moveTo(newpt2.x, newpt2.y);
                ctx.lineTo(newpt2.x - arrlen * Math.cos(theta+Math.PI/6), newpt2.y - arrlen * Math.sin(theta + Math.PI/6));

                
                if (texts[edge.source.name + edge.target.name] == null)
                {
                    totaltrans = calculate_total_transmitted(psys);
                    texts = calculate_texts(psys, totaltrans);
                }

                gfx.text(texts[edge.source.name + edge.target.name], thirdpt.x, thirdpt.y + 10, {color:"black", align:"center", font:"Arial", size:10})
                ctx.stroke();
            });

            psys.eachNode(function(node, pt) {
                var col;

                var real_trans = gather_stream_count(node.data[":stats"], "default", "600");
                
                if(node.data[":type"] === "bolt") {
                    var cap = Math.min(node.data[":capacity"], 1);
                    var red = Math.floor(cap * 225) + 30;
                    var green = Math.floor(255 - red);
                    var blue = Math.floor(green/5);
                    col = arbor.colors.encode({r:red,g:green,b:blue,a:1});
                } else {
                    col = "#0000FF";
                }
                
                var w = Math.max(55, 25 + gfx.textWidth(node.name));
                
                gfx.oval(pt.x - w/2, pt.y - w/2, w, w, {fill: col});
                gfx.text(node.name, pt.x, pt.y+3, {color:"white", align:"center", font:"Arial", size:12});
                gfx.text(node.name, pt.x, pt.y+3, {color:"white", align:"center", font:"Arial", size:12});
                
                gfx.text(parseFloat(node.data[":latency"]).toFixed(2) + " ms", pt.x, pt.y + 17, {color:"white", align:"center", font:"Arial", size:12});
                
            });

            // Draw gradient sidebar
            ctx.rect(0,0,50,canvas.height);
            var grd = ctx.createLinearGradient(0,0,50,canvas.height);
            grd.addColorStop(0, '#1ee12d');
            grd.addColorStop(1, '#ff0000');
            ctx.fillStyle=grd;
            ctx.fillRect(0,0,50,canvas.height);
            
            
        },
        
        initMouseHandling:function() {
            var dragged = null;

            var clicked = false;
            
            var handler = {
                clicked:function(e){
                    var pos = $(canvas).offset();
                    _mouseP = arbor.Point(e.pageX-pos.left, e.pageY - pos.top);
                    dragged = psys.nearest(_mouseP);
                    
                    if(dragged && dragged.node !== null) {
                        dragged.node.fixed = true;
                    }
                    
                    clicked = true;
                    setTimeout(function(){clicked = false;}, 50);

                    $(canvas).bind('mousemove', handler.dragged);
                    $(window).bind('mouseup', handler.dropped);
                    
                    return false;
                },
                
                dragged:function(e) {

                    var pos = $(canvas).offset();
                    var s = arbor.Point(e.pageX-pos.left, e.pageY-pos.top);
                    
                    if(dragged && dragged.node != null) {
                        var p = psys.fromScreen(s);
                        dragged.node.p = p;
                    }
                    
                    return false;
                    
                },

                dropped:function(e) {
                    if(clicked) {
                        if(dragged.distance < 50) {
                            if(dragged && dragged.node != null) { 
                                window.location = dragged.node.data[":link"];
                            }
                        }
                    }

                    if(dragged === null || dragged.node === undefined) return;
                    if(dragged.node !== null) dragged.node.fixed = false;
                    dragged.node.tempMass = 1000;
                    dragged = null;
                    $(canvas).unbind('mousemove', handler.dragged);
                    $(window).unbind('mouseup', handler.dropped);
                    _mouseP = null;
                    return false;
                }
                
            }
            
            $(canvas).mousedown(handler.clicked);
        }
    }
    
    return myRenderer;
}

function calculate_texts(psys, totaltrans) {
    var texts = {};
    psys.eachEdge(function(edge, pt1, pt2) {
        var text = "";
        for(var i = 0; i < edge.target.data[":inputs"].length; i++) {
            var stream = edge.target.data[":inputs"][i][":stream"];
            var sani_stream = edge.target.data[":inputs"][i][":sani-stream"];
            if(stream_checked(sani_stream) 
               && edge.target.data[":inputs"][i][":component"] === edge.source.name) {
                stream_transfered = gather_stream_count(edge.source.data[":stats"], sani_stream, "600");
                text += stream + ": " 
                    + stream_transfered + ": " 
                    + (totaltrans > 0  ? Math.round((stream_transfered/totaltrans) * 100) : 0) + "%\n";
                
            }
        }
        
        texts[edge.source.name + edge.target.name] = text;
    });

    return texts;
}

function calculate_weights(psys, totaltrans) {
    var weights = {};
 
    psys.eachEdge(function(edge, pt1, pt2) {
        var trans = 0;
        for(var i = 0; i < edge.target.data[":inputs"].length; i++) {
            var stream = edge.target.data[":inputs"][i][":sani-stream"];
            if(stream_checked(stream) && edge.target.data[":inputs"][i][":component"] === edge.source.name)
                trans += gather_stream_count(edge.source.data[":stats"], stream, "600");
        }
        weights[edge.source.name + edge.target.name] = (totaltrans > 0 ? trans/totaltrans : 0);
    });
    return weights;
}

function calculate_total_transmitted(psys) {
    var totaltrans = 0;
    var countedmap = {}
    psys.eachEdge(function(node, pt, pt2) {
        if(!countedmap[node.source.name])
            countedmap[node.source.name] = {};

        for(var i = 0; i < node.target.data[":inputs"].length; i++) {
            var stream = node.target.data[":inputs"][i][":stream"];
            if(stream_checked(node.target.data[":inputs"][i][":sani-stream"]))
            {
                if(!countedmap[node.source.name][stream]) {
                    if(node.source.data[":stats"])
                    {
                        var toadd = gather_stream_count(node.source.data[":stats"], node.target.data[":inputs"][i][":sani-stream"], "600");
                        totaltrans += toadd;
                    }
                    countedmap[node.source.name][stream] = true;
                }
            }
        }
        
    });

    return totaltrans;
}

function has_checked_stream_input(inputs) {
    
    for(var i = 0; i < inputs.length; i++) {
        var x = stream_checked(inputs[i][":sani-stream"]);
        if(x) 
            return true;
    }
    return false;
}

function stream_checked(stream) {
    var checked = $("#" + stream).is(":checked");
    return checked;
}

function has_checked_stream_output(jdat, component) {
    var ret = false;
    $.each(jdat, function(k, v) {
        for(var i = 0; i < v[":inputs"].length; i++) {
            if(stream_checked(v[":inputs"][i][":sani-stream"]) 
               && v[":inputs"][i][":component"] == component)
                ret = true;
        }
    });
    return ret;
}

function gather_stream_count(stats, stream, time) {
    var transferred = 0;
    if(stats)
        for(var i = 0; i < stats.length; i++) {
            if(stats[i][":transferred"] != null && stats[i][":transferred"][time] != undefined)
            {
                var stream_trans = stats[i][":transferred"][time][stream];
                if(stream_trans != null)
                    transferred += stream_trans;
            }
        }
    return transferred;
}


function rechoose(jdat, sys, box) {
    var id = box.id;
    if($(box).is(':checked'))
    {
        //Check each node in our json data to see if it has inputs from or outputs to selected streams. If it does, add a node for it.
        $.each(jdat,function(k,v) {
            if( has_checked_stream_input(v[":inputs"]) || has_checked_stream_output(jdat, k))
                sys.addNode(k,v);
        });
           
        //Check each node in our json data and add necessary edges based on selected components.
        $.each(jdat, function(k, v) {
            for(var i = 0; i < v[":inputs"].length; i++)
                if(v[":inputs"][i][":sani-stream"] === id) {
                    
                    sys.addEdge(v[":inputs"][i][":component"], k, v);
                }
        });
    }
    else {
        //Check each node to see if it should be pruned.
        sys.prune(function(node, from, to) {
            return !has_checked_stream_input(node.data[":inputs"]) && !has_checked_stream_output(jdat, node.name);
        });
        
        //Check each edge to see if it represents any selected streams. If not, prune it.
        sys.eachEdge(function(edge, pt1, pt2) {
            var inputs = edge.target.data[":inputs"];
            
            if($.grep(inputs, function(input) {
                
                return input[":component"] === edge.source.name 
                    && stream_checked(input[":sani-stream"]);
            
            }).length == 0)
            {
                sys.pruneEdge(edge);
            }
        });
    }

    //Tell the particle system's renderer that it needs to update its labels, colors, widths, etc.
    sys.renderer.signal_update();
    sys.renderer.redraw();

}

var topology_data;
function update_data(jdat, sys) {
    $.each(jdat, function(k,v) {
        if(sys.getNode(k))
            sys.getNode(k).data = v;
    });
}

function jsError(other) {
  try {
    other();
  } catch (err) {
    getStatic("/templates/json-error-template.html", function(template) {
      $("#json-response-error").append(Mustache.render($(template).filter("#json-error-template").html(),{error: "JS Error", errorMessage: err}));
    });
  }
}

var should_update;
function show_visualization(sys) {
    $.getJSON("/api/v1/topology/"+$.url("?id")+"/visualization-init",function(response,status,jqXHR) {
        getStatic("/templates/topology-page-template.html", function(template) {
            jsError(function() {
                var topologyVisualization = $("#visualization-container");
                if (topologyVisualization.find("canvas").length == 0) {
                    topologyVisualization.append(
                        Mustache.render($(template)
                            .filter("#topology-visualization-container-template")
                            .html(),
                            response));
                }
            });

            if(sys == null)
            {
                sys = arbor.ParticleSystem(20, 1000, 0.15, true, 55, 0.02, 0.6);
                sys.renderer = renderGraph("#topoGraph");
                sys.stop();

                $(".stream-box").click(function () { rechoose(topology_data, sys, this) });
            }

            should_update = true;
            var update_freq_ms = 10000;
            var update = function(should_rechoose){
              if(should_update) {
                $.ajax({
                    url: "/api/v1/topology/"+$.url("?id")+"/visualization",
                    success: function(data, status, jqXHR) {
                        topology_data = data;
                        update_data(topology_data, sys);
                        sys.renderer.signal_update();
                        sys.renderer.redraw();
                        if(should_update)
                            setTimeout(update, update_freq_ms);
                        if(should_rechoose)
                            $(".stream-box").each(function () {
                                rechoose(topology_data, sys, this)
                            });
                    }
                });
              }
            };

            update(true);
            $("#visualization-container").show(500);
            $("#show-hide-visualization").attr('value', 'Hide Visualization');
            $("#show-hide-visualization").unbind("click");
            $("#show-hide-visualization").click(function () { hide_visualization(sys) });
        })
    });
}

function hide_visualization(sys) {
    should_update = false;
    $("#visualization-container").hide(500);
    $("#show-hide-visualization").attr('value', 'Show Visualization');
    $("#show-hide-visualization").unbind("click");
    $("#show-hide-visualization").click(function () { show_visualization(sys) });
}
