// D3 visualization
// TODO: D3 as a service
panelApp.directive('batD3', function () {
  return {
    restrict: 'E',
    terminal: true,
    scope: {
      val: '=val'
    },
    link: function (scope, element, attrs) {
      // Based on code from: http://mbostock.github.com/d3/talk/20111116/bundle.html

      // Initialize Element
      // ------------------
      var div = d3.select(element[0]);

      // Constants
      // ---------
      var w = 600,
          h = 600,
          rx = w / 2,
          ry = h / 2,
          m0,
          rotate = 0;

      // Helpers
      // -------

      // generate element ids that do not have '$'
      var sanitize = function (key) {
        return key.replace('$', 'dollar')
      }

      // TODO: refactor the data transformation to make it faster
      // For instance, build up the ideal structure in inject/degug.js
      var packages = {
        // Lazily construct the package hierarchy from class names.
        root: function(classes) {
          var map = {};

          // add "classes" with no dependencies
          var exist = {},
            toAdd = [];
          classes.forEach(function (cl) {
            exist[cl.name] = true;
          });
          classes.forEach(function (cl) {
            cl.imports.forEach(function (im) {
              if (!exist[im]) {
                toAdd.push(im);
                exist[im] = true;
              }
            });
          });
          toAdd.forEach(function (a) {
            classes.push({
              name: a,
              imports: []
            });
          });

          function find(name, data) {
            var node = map[name], i;
            if (!node) {
              node = map[name] = data || {name: name, children: []};
              if (name.length) {
                node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
                node.parent.children.push(node);
                node.key = name.substring(i + 1);
              }
            }
            return node;
          }

          classes.forEach(function(d) {
            find(d.name, d);
          });

          return map[""];
        },

        // Return a list of imports for the given array of nodes.
        imports: function(nodes) {
          var map = {},
              imports = [];

          // Compute a map from name to node.
          nodes.forEach(function(d) {
            map[d.name] = d;
          });

          // For each import, construct a link from the source to target node.
          nodes.forEach(function(d) {
            if (d.imports) d.imports.forEach(function(i) {
              imports.push({source: map[d.name], target: map[i]});
            });
          });

          return imports;
        }
      };

      // Instantiate and Style D3 Objects
      // --------------------------------

      var cluster = d3.layout.cluster().
        size([360, ry - 120]).
        sort(function(a, b) { return d3.ascending(a.key, b.key); });

      var bundle = d3.layout.bundle();

      var line = d3.svg.line.radial().
        interpolate("bundle").
        tension(.85).
        radius(function(d) { return d.y; }).
        angle(function(d) { return d.x / 180 * Math.PI; });

      var svg = div.append("svg:svg").
              attr("preserveAspectRatio", "xMinYMin meet").
              attr("viewBox", [0, 0, w, h].join(' ')).
              attr("height", h).
            append("svg:g").
              attr("transform", "translate(" + rx + "," + ry + ")");

      // Render the data whenever "val" changes
      // --------------------------------------
      scope.$watch('val', function (newVal, oldVal) {
        var classes;

        if (!newVal || newVal.length === 0) {
          svg.selectAll('*').remove();
          return;
        }

        if (oldVal && oldVal.length === newVal.length) {
          var changed = false;
          for (i = 0; i < oldVal.length; i++) {
            if (oldVal[i].name !== newVal[i].name || newVal[i].imports.length !== oldVal[i].imports.length) {
              changed = true;
              break;
            }
          }
          if (!changed) {
            return;
          }
        }
        classes = newVal.slice(0);
        classes.sort(function (a, b) {
          return .5 - (a.name < b.name);
        });

        svg.selectAll('*').remove();

        svg.append("svg:path")
          .attr("class", "arc")
          .attr("d", d3.svg.arc().outerRadius(ry - 120).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
          .on("mousedown", mousedown);

        var nodes = cluster.nodes(packages.root(classes)),
            links = packages.imports(nodes),
            splines = bundle(links);

        var path = svg.selectAll("path.link")
            .data(links)
          .enter().append("svg:path")
            .attr("class", function(d) { return "link source-" + sanitize(d.source.key) + " target-" + sanitize(d.target.key); })
            .attr("d", function(d, i) { return line(splines[i]); });

        svg.selectAll("g.node")
            .data(nodes.filter(function(n) { return !n.children; }))
          .enter().append("svg:g")
            .attr("class", "node")
            .attr("id", function(d) { return "node-" + sanitize(d.key); })
            .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
          .append("svg:text")
            .attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
            .attr("dy", ".31em")
            .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
            .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
            .text(function(d) { return d.key; })
            .on("mouseover", mouseover)
            .on("mouseout", mouseout);
      });

      /*
      d3.select("input[type=range]").on("change", function() {
        line.tension(this.value / 100);
        path.attr("d", function(d, i) { return line(splines[i]); });
      });
      */

      //TODO: decide where to attach these events

      /*
      d3.select(window)
          .on("mousemove", mousemove)
          .on("mouseup", mouseup);
      */

      function mouse(e) {
        return [e.pageX - rx, e.pageY - ry];
      }

      function mousedown() {
        m0 = mouse(d3.event);
        d3.event.preventDefault();
      }

      function mousemove() {
        if (m0) {
          var m1 = mouse(d3.event),
              dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
          div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
        }
      }

      function mouseup() {
        if (m0) {
          var m1 = mouse(d3.event),
              dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;

          rotate += dm;
          if (rotate > 360) rotate -= 360;
          else if (rotate < 0) rotate += 360;
          m0 = null;

          div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");

          svg
              .attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
            .selectAll("g.node text")
              .attr("dx", function(d) { return (d.x + rotate) % 360 < 180 ? 8 : -8; })
              .attr("text-anchor", function(d) { return (d.x + rotate) % 360 < 180 ? "start" : "end"; })
              .attr("transform", function(d) { return (d.x + rotate) % 360 < 180 ? null : "rotate(180)"; });
        }
      }

      function mouseover(d) {
        svg.selectAll("path.link.target-" + sanitize(d.key))
            .classed("target", true)
            .each(updateNodes("source", true));

        svg.selectAll("path.link.source-" + sanitize(d.key))
            .classed("source", true)
            .each(updateNodes("target", true));
      }

      function mouseout(d) {
        svg.selectAll("path.link.source-" + sanitize(d.key))
            .classed("source", false)
            .each(updateNodes("target", false));

        svg.selectAll("path.link.target-" + sanitize(d.key))
            .classed("target", false)
            .each(updateNodes("source", false));
      }

      function updateNodes(name, value) {
        return function(d) {
          if (value) this.parentNode.appendChild(this);
          svg.select("#node-" + sanitize(d[name].key)).classed(name, value);
        };
      }

      function cross(a, b) {
        return a[0] * b[1] - a[1] * b[0];
      }

      function dot(a, b) {
        return a[0] * b[0] + a[1] * b[1];
      }
    }
  };
});
