/*
 * 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.
 */
'use strict';

angular.module('dgc.lineage').controller('Lineage_ioController', ['$element', '$scope', '$state', '$stateParams', 'lodash', 'LineageResource', 'd3', 'DetailsResource', '$q',
    function($element, $scope, $state, $stateParams, _, LineageResource, d3, DetailsResource, $q) {
        var guidsList = [];

        function inVertObj(edgs) {
            var newEdgsObj = {};

            $.each(edgs, function(key, value) {
                for (var k = 0; k < value.length; k++) {
                    newEdgsObj[value[k]] = newEdgsObj[value[k]] || [];
                    newEdgsObj[value[k]] = [key];
                }
            });
            return newEdgsObj;
        } 

        function getCombinedLineageData(tableData, callRender) {
            LineageResource.get({
                tableName: tableData.tableName,
                type: 'outputs'
            }, function lineageSuccess(response1) {

                LineageResource.get({
                    tableName: tableData.tableName,
                    type: 'inputs'
                }, function lineageSuccess(response) {
                    response.results.values.edges = inVertObj(response.results.values.edges);

                    angular.forEach(response.results.values.edges, function(value, key) {
                        angular.forEach(response1.results.values.edges, function(value1, key1) {
                            if (key === key1) {
                                var array1 = value;
                                angular.forEach(value1, function(value2) {
                                    array1.push(value2);
                                });
                                response.results.values.edges[key] = array1;
                                response1.results.values.edges[key] = array1;
                            }
                        });
                    });

                    angular.extend(response.results.values.edges, response1.results.values.edges);
                    angular.extend(response.results.values.vertices, response1.results.values.vertices);

                    if (!_.isEmpty(response.results.values.vertices)) {
                        loadProcess(response.results.values.edges, response.results.values.vertices)
                            .then(function(res) {
                                guidsList = res;

                                $scope.lineageData = transformData(response.results);

                                if (callRender) {
                                    render();
                                }
                            }); 
                    } else {
                        $scope.requested = false;
                    }
                });

            });

        }


        function loadProcess(edges, vertices) {

            var urlCalls = [];
            var deferred = $q.defer();
            for (var guid in edges) {
                if (!vertices.hasOwnProperty(guid)) {
                    urlCalls.push(DetailsResource.get({
                        id: guid
                    }).$promise);
                }

            }
            $q.all(urlCalls)
                .then(function(results) {
                    deferred.resolve(results);
                });
            return deferred.promise;
        }

        $scope.type = $element.parent().attr('data-table-type');
        $scope.requested = true;
        $scope.height = $element[0].offsetHeight;
        $scope.width = $element[0].offsetWidth;

        function render() {
            renderGraph($scope.lineageData, {
                eleObj: $element,
                element: $element[0],
                height: $scope.height,
                width: $scope.width
            });
            $scope.rendered = true;
        }

        $scope.onReset = function() {
            renderGraph($scope.lineageData, {
                eleObj: $element,
                element: $element[0],
                height: $scope.height,
                width: $scope.width
            });
        };

        $scope.$on('render-lineage', function(event, lineageData) {
            if (lineageData.type === $scope.type) {
                if (!$scope.lineageData) {
                    if ($scope.requested) {
                        if ($scope.type === 'io') {
                            console.log($scope.type);
                            getCombinedLineageData(lineageData, true);
                        } else {
                            getCombinedLineageData(lineageData, true);
                        } 
                    }
                } else {
                    render();
                }
            }
        });

        function transformData(metaData) {
            var edges = metaData.values.edges,
                vertices = metaData.values.vertices,
                nodes = {};

            function getNode(guid) {
                var name, type, tip;
                if (vertices.hasOwnProperty(guid)) {
                    name = vertices[guid].values.name;
                    type = vertices[guid].values.vertexId.values.typeName;
                } else {
                    var loadProcess = getLoadProcessTypes(guid);
                    if (typeof loadProcess !== "undefined") {
                        name = loadProcess.name;
                        type = loadProcess.typeName;
                        tip = loadProcess.tip;
                    } else {
                        name = 'Load Process';
                        type = 'Load Process';
                    }
                }
                var vertex = {
                    guid: guid,
                    name: name,
                    type: type,
                    tip: tip
                };
                if (!nodes.hasOwnProperty(guid)) {
                    nodes[guid] = vertex;
                }
                return nodes[guid];
            }

            function getLoadProcessTypes(guid) {
                var procesRes = [];
                angular.forEach(guidsList, function(value) {
                    if (value.id.id === guid) {
                        procesRes.name = value.values.name;
                        procesRes.typeName = value.typeName;
                        procesRes.tip = value.values.queryText;
                    }
                });
                return procesRes;
            }

            function attachParent(edge, node) {
                edge.forEach(function eachPoint(childGuid) {
                    var childNode = getNode(childGuid);
                    node.children = node.children || [];
                    node.children.push(childNode);
                    childNode.parent = node.guid;
                });
            }

            /* Loop through all edges and attach them to correct parent */
            for (var guid in edges) {
                var edge = edges[guid],
                    node = getNode(guid);

                /* Attach parent to each endpoint of edge */
                attachParent(edge, node);
            }

            var starTingObj = {
                name: 'root',
                guid: 'root',
                children: []
            };

            angular.forEach(nodes, function(value) {
                if (!value.hasOwnProperty('parent')) {
                    starTingObj.children.push(value);
                }
            });

            return starTingObj;
        }

        function renderGraph(data, container) {
            // ************** Generate the tree diagram  *****************
            var element = d3.select(container.element),
                widthg = Math.max(container.width, 1100),
                heightg = Math.max((window.innerHeight - 400), 500),

                totalNodes = 0,
                maxLabelLength = 0,
                selectedNode = null,
                draggingNode = null,
                dragListener = null,
                dragStarted = true,
                domNode = null,
                multiParents = null,
                nodes = null,
                tooltip = null,
                node = null,
                i = 0,
                duration = 750,
                root,
                depthwidth = 10;


            var viewerWidth = widthg - 15,
                viewerHeight = heightg;

            var tree = d3.layout.tree().size([viewerHeight, viewerWidth]);
            /*.size([viewerHeight, viewerWidth]);   nodeSize([100, 200]);*/

            container.eleObj.find(".graph").html('');
            container.eleObj.find("svg").remove();

            // define a d3 diagonal projection for use by the node paths later on.
            var diagonal = d3.svg.diagonal()
                .projection(function(d) {
                    return [d.y, d.x];
                });

            // A recursive helper function for performing some setup by walking through all nodes

            function visit(parent, visitFn, childrenFn) {
                if (!parent) return;

                visitFn(parent);

                var children = childrenFn(parent);
                if (children) {
                    var count = children.length;
                    for (var i = 0; i < count; i++) {
                        visit(children[i], visitFn, childrenFn);
                    }
                }
            }

            // Call visit function to establish maxLabelLength
            visit(data, function(d) {
                totalNodes++;
                maxLabelLength = Math.max(d.name.length, maxLabelLength);

            }, function(d) {
                return d.children && d.children.length > 0 ? d.children : null;
            });


            // sort the tree according to the node names

            function sortTree() {
                tree.sort(function(a, b) {
                    return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
                });
            }
            // Sort the tree initially incase the JSON isn't in a sorted order.
            sortTree();

            // Define the zoom function for the zoomable tree  
            function zoom() {
                svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
            }

            // define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
            var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
            /* Initialize tooltip */
            tooltip = d3.tip()
                .attr('class', 'd3-tip')
                .html(function(d) {
                    return '<pre class="alert alert-success">' + d.name + '</pre>';
                });

            // define the baseSvg, attaching a class for styling and the zoomListener
            var baseSvg = element.append('svg')
                .attr("width", viewerWidth)
                .attr("height", viewerHeight)
                .attr("class", "overlay")
                .call(zoomListener)
                .on("dblclick.zoom", function() {
                    return null;
                })
                .call(tooltip);


            // Define the drag listeners for drag/drop behaviour of nodes.
            dragListener = d3.behavior.drag()
                .on("dragstart", function(d) {
                    if (d === root) {
                        return;
                    }
                    dragStarted = true;
                    nodes = tree.nodes(d);
                    d3.event.sourceEvent.stopPropagation();
                    // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
                })
                .on("dragend", function(d) {
                    if (d === root) {
                        return;
                    }
                    domNode = this;
                    if (selectedNode) {
                        // now remove the element from the parent, and insert it into the new elements children
                        var index = draggingNode.parent.children.indexOf(draggingNode);
                        if (index > -1) {
                            draggingNode.parent.children.splice(index, 1);
                        }
                        if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
                            if (typeof selectedNode.children !== 'undefined') {
                                selectedNode.children.push(draggingNode);
                            } else {
                                selectedNode._children.push(draggingNode);
                            }
                        } else {
                            selectedNode.children = [];
                            selectedNode.children.push(draggingNode);
                        }
                        // Make sure that the node being added to is expanded so user can see added node is correctly moved
                        expand(selectedNode);
                        sortTree();
                        endDrag();
                    } else {
                        endDrag();
                    }
                });

            function endDrag() {
                selectedNode = null;
                d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
                d3.select(domNode).attr('class', 'node');
                // now restore the mouseover event or we won't be able to drag a 2nd time
                d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
                updateTempConnector();
                if (draggingNode !== null) {
                    update(root);
                    centerNode(draggingNode);
                    draggingNode = null;
                }
            }


            function expand(d) {
                if (d._children) {
                    d.children = d._children;
                    d.children.forEach(expand);
                    d._children = null;
                }
            }

            // Function to update the temporary connector indicating dragging affiliation
            var updateTempConnector = function() {
                var data = [];
                if (draggingNode !== null && selectedNode !== null) {
                    // have to flip the source coordinates since we did this for the existing connectors on the original tree
                    data = [{
                        source: {
                            x: selectedNode.y0,
                            y: selectedNode.x0
                        },
                        target: {
                            x: draggingNode.y0,
                            y: draggingNode.x0
                        }
                    }];
                }
                var link = svgGroup.selectAll(".templink").data(data);

                link.enter().append("path")
                    .attr("class", "templink")
                    .attr("d", d3.svg.diagonal())
                    .attr('pointer-events', 'none');

                link.attr("d", d3.svg.diagonal());

                link.exit().remove();
            };

            // Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.

            function centerNode(source) {
                var scale = (depthwidth === 10) ? zoomListener.scale() : 0.4;
                var x = -source.y0;
                var y = -source.x0;
                x = x * scale - 130;
                y = y * scale + viewerHeight / 2;
                d3.select('g').transition()
                    .duration(duration)
                    .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
                zoomListener.scale(scale);
                zoomListener.translate([x, y]);
            }

            // Toggle children function

            // function toggleChildren(d) {
            //     if (d.children) {
            //         d._children = d.children;
            //         d.children = null;
            //     } else if (d._children) {
            //         d.children = d._children;
            //         d._children = null;
            //     }
            //     return d;
            // }

            // Toggle children on click.

            // function click(d) {
            //     if (d3.event.defaultPrevented) return; // click suppressed
            //     d = toggleChildren(d);
            //     update(d);
            //     //centerNode(d);
            // }

            //arrow
            baseSvg.append("svg:defs")
                .append("svg:marker")
                .attr("id", "arrow")
                .attr("viewBox", "0 0 10 10")
                .attr("refX", 22)
                .attr("refY", 5)
                .attr("markerUnits", "strokeWidth")
                .attr("markerWidth", 6)
                .attr("markerHeight", 9)
                .attr("orient", "auto")
                .append("svg:path")
                .attr("d", "M 0 0 L 10 5 L 0 10 z");

            //marker for input type graph
            baseSvg.append("svg:defs")
                .append("svg:marker")
                .attr("id", "input-arrow")
                .attr("viewBox", "0 0 10 10")
                .attr("refX", -15)
                .attr("refY", 5)
                .attr("markerUnits", "strokeWidth")
                .attr("markerWidth", 6)
                .attr("markerHeight", 9)
                .attr("orient", "auto")
                .append("svg:path")
                .attr("d", "M -2 5 L 8 0 L 8 10 z");

            function update(source) {
                // Compute the new height, function counts total children of root node and sets tree height accordingly.
                // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
                // This makes the layout more consistent.
                var levelWidth = [1];
                var childCount = function(level, n) {

                    if (n.children && n.children.length > 0) {
                        if (levelWidth.length <= level + 1) levelWidth.push(0);

                        levelWidth[level + 1] += n.children.length;
                        n.children.forEach(function(d) {
                            childCount(level + 1, d);
                        });
                    }
                };
                childCount(0, root);
                tree = tree.nodeSize([50, 100]);

                // Compute the new tree layout.
                var nodes = tree.nodes(root).reverse();

                nodes = _.uniq(nodes, 'guid');

                _.each(nodes, function(o, i) {
                    var itemsOfTheSameDepth = _.where(nodes, {
                        depth: o.depth
                    });
                    var indexOfCurrent = _.indexOf(itemsOfTheSameDepth, o);
                    var interval = viewerHeight / itemsOfTheSameDepth.length;
                    nodes[i].x = interval / 2 + (interval * indexOfCurrent);
                });

                var links = tree.links(nodes);

                _.each(links, function(o, i) {
                    //links[i].target = _.find(nodes, {guid: o.target.id});
                    links[i].target = _.find(nodes, {
                        guid: o.target.guid
                    });
                });

                // Set widths between levels based on maxLabelLength.
                nodes.forEach(function(d) {
                    if (levelWidth.length > 1 && depthwidth === 10) {
                        for (var o = 0; o < levelWidth.length; o++) {
                            if (levelWidth[o] > 4) {
                                depthwidth = 70;
                                break;
                            }
                        }
                    }
                    var maxLebal = maxLabelLength;
                    if (depthwidth === 10) {
                        maxLebal = 20;
                    }
                    d.y = (d.depth * (maxLebal * depthwidth));
                });

                // Update the nodes…
                node = svgGroup.selectAll("g.node")
                    .data(nodes, function(d) {
                        return d.id || (d.id = ++i);
                    });

                // Enter any new nodes at the parent's previous position.
                var nodeEnter = node.enter().append("g")
                    .call(dragListener)
                    .attr('class', function(d) {
                        if (d.guid === "root") {
                            return "hide";
                        } else {
                            return "";
                        }
                    })
                    .classed('node', true)
                    .attr("transform", function() {
                        return "translate(" + source.y0 + "," + source.x0 + ")";
                    });
                //.on('click', click);

                nodeEnter.append("image")
                    .attr("class", "nodeImage")
                    .attr("xlink:href", function(d) {
                        return d.type === 'Table' ? '../img/tableicon.png' : '../img/process.png';
                    })
                    .on('mouseover', function(d) {
                        if (d.type === 'LoadProcess' || 'Table') {
                            tooltip.show(d);
                        }
                    })
                    .on('dblclick', function(d) {
                        $state.go("details", {
                            id: d.guid
                        });
                    })
                    .on('mouseout', function(d) {
                        if (d.type === 'LoadProcess' || 'Table') {
                            tooltip.hide(d);
                        }
                    })
                    .attr("x", "-18px")
                    .attr("y", "-18px")
                    .attr("width", "34px")
                    .attr("height", "34px");

                nodeEnter.append("text")
                    .attr("x", function(d) {
                        return d.children || d._children ? -10 : 10;
                    })
                    .attr("dx", function(d) {
                        return d.children ? 50 : -50;
                    })
                    .attr("dy", -24)
                    .attr('class', 'place-label')
                    .attr("text-anchor", function(d) {
                        return d.children || d._children ? "end" : "start";
                    })
                    .text(function(d) {
                        var nameDis = (d.name.length > 15) ? d.name.substring(0, 15) + "..." : d.name;
                        $(this).attr('title', d.name);
                        return nameDis;
                    })
                    .style("fill-opacity", 0);

                // Update the text to reflect whether node has children or not.
                node.select('text')
                    .attr("x", function(d) {
                        return d.children || d._children ? -10 : 10;
                    })
                    .attr("text-anchor", function(d) {
                        return d.children || d._children ? "end" : "start";
                    })
                    .text(function(d) {
                        var nameDis = (d.name.length > 15) ? d.name.substring(0, 15) + "..." : d.name;
                        $(this).attr('title', d.name);
                        return nameDis;
                    });

                // Change the circle fill depending on whether it has children and is collapsed
                // Change the circle fill depending on whether it has children and is collapsed
                node.select("image.nodeImage")
                    .attr("r", 4.5)
                    .attr("xlink:href", function(d) {
                        if (d._children) {
                            return d.type === 'Table' ? '../img/tableicon1.png' : '../img/process1.png';
                        }
                        return d.type === 'Table' ? '../img/tableicon.png' : '../img/process.png';
                    });


                // Transition nodes to their new position.
                var nodeUpdate = node.transition()
                    .duration(duration)
                    .attr("transform", function(d) {
                        return "translate(" + d.y + "," + d.x + ")";
                    });

                // Fade the text in
                nodeUpdate.select("text")
                    .style("fill-opacity", 1);

                // Transition exiting nodes to the parent's new position.
                var nodeExit = node.exit().transition()
                    .duration(duration)
                    .attr("transform", function() {
                        return "translate(" + source.y + "," + source.x + ")";
                    })
                    .remove();

                nodeExit.select("circle")
                    .attr("r", 0);

                nodeExit.select("text")
                    .style("fill-opacity", 0);

                // Update the links…
                var link = svgGroup.selectAll("path.link")
                    .data(links);
                // .data(links, function(d) {
                //     return d.target.id;
                // });

                // Enter any new links at the parent's previous position.
                link.enter().insert("path", "g")
                    .attr('class', function(d) {
                        if (d.source.guid === "root") {
                            return "hide";
                        } else {
                            return "";
                        }
                    })
                    .classed('link', true)
                    .style('stroke', 'green')
                    .attr("d", function() {
                        var o = {
                            x: source.x0,
                            y: source.y0
                        };
                        return diagonal({
                            source: o,
                            target: o
                        });
                    });

                // Transition links to their new position.
                link.transition()
                    .duration(duration)
                    .attr("d", diagonal);

                // Transition exiting nodes to the parent's new position.
                link.exit().transition()
                    .duration(duration)
                    .attr("d", function() {
                        var o = {
                            x: source.x,
                            y: source.y
                        };
                        return diagonal({
                            source: o,
                            target: o
                        });
                    })
                    .remove();

                // Stash the old positions for transition.
                nodes.forEach(function(d) {
                    d.x0 = d.x;
                    d.y0 = d.y;
                });

                if ($scope.type === 'inputs') {
                    link.attr("marker-start", "url(#input-arrow)"); //if input
                } else {
                    link.attr("marker-end", "url(#arrow)"); //if input
                }
            }

            // Append a group which holds all nodes and which the zoom Listener can act upon.
            var svgGroup = baseSvg.append("g")
                .attr("transform", "translate(0,0)");

            // Define the root
            root = data;
            root.x0 = viewerWidth / 2;
            root.y0 = viewerHeight / 2;

            // Layout the tree initially and center on the root node.
            update(root);
            centerNode(root);
            $scope.requested = false;
            var couplingParent1 = tree.nodes(root).filter(function(d) {
                return d.name === 'cluster';
            })[0];
            var couplingChild1 = tree.nodes(root).filter(function(d) {
                return d.name === 'JSONConverter';
            })[0];

            multiParents = [{
                parent: couplingParent1,
                child: couplingChild1
            }];

            multiParents.forEach(function() {
                svgGroup.append("path", "g");
            });
        }

    }
]);