/**
 * This directive displays a d3 node edge graph for a given UQ author
 */
'use strict';

angular.module('uql.authors')
  .controller('uqlAuthorsStaffCollaborationCtrl', ['$scope', 'UQL_CONFIG_AUTHORS', function ($scope, UQL_CONFIG_AUTHORS) {
    $scope.expectedViz = 'staff-collaboration';
    $scope.filename = 'report-' + $scope.expectedViz + '.csv';

    /**
     * Take the data returned from our json call and format it
     * suitably for output
     *
     * @param {String} authorUsername
     * @param {Object[]} data
     * @return {Array[]}
     */
    $scope.formatData = function (authorUsername, data) {
      if (!angular.isArray(data.nodes) || !angular.isArray(data.edges)) {
        return false;
      }

      var author;
      var collaborators = [];
      var defaultImage = UQL_CONFIG_AUTHORS.imagesBaseUrl + '/' + UQL_CONFIG_AUTHORS.defaultAuthorImage;

      // create a lookup table for the node index as this is
      // how d3 maps edges
      for (var i = 0, il = data.nodes.length; i < il; i++) {
        var person = data.nodes[i];
        person.name = makeName(person);
        if (person.uqr_id && (person.image_exists === 1)) {
          person.image = UQL_CONFIG_AUTHORS.authorImageUrlBase + person.uqr_id + '.jpg';
        } else {
          person.image = defaultImage;
        }
        if (authorUsername === person.username) {
          author = person;
          continue;
        }
        for (var j = 0, jl = data.edges.length; j < jl; j++) {
          var row = data.edges[j];
          if (row.author2 === person.username) {
            person.collaborations = parseInt(row.collaborations, 10);
            collaborators.push(person);
            break;
          }
        }
      }

      // sort descending
      collaborators.sort(function (a, b) {
        return b.collaborations - a.collaborations;
      });

      return {
        author: author,
        collaborators: collaborators
      };
    };

    /**
     * Format the data suitable for a table
     *
     * @param {Object[]} data
     * @return {Array[]}
     */
    $scope.formatTableData = function (data) {
      if (!angular.isArray(data.nodes) || !angular.isArray(data.edges)) {
        return false;
      }

      var returnVal = [
        [
          {type: 'string', name: 'Collaborating Author'},
          {type: 'number', name: 'Collaborative Publications'}
        ]
      ];

      var staff_lookup = {};

      for (var i = 0, l = data.nodes.length; i < l; i++) {
        var el = data.nodes[i];
        staff_lookup[el.username] = el;
      }

      for (var j = 0, jl = data.edges.length; j < jl; j++) {
        var row = data.edges[j];
        var staff = staff_lookup[row.author2];
        if (angular.isDefined(staff) && staff.username !== row.author1) {
          returnVal.push([
            makeName(staff),
            parseInt(row.collaborations, 10)
          ]);
        }
      }

      return returnVal;
    };

    /**
     * Make a name from the different name attributes
     *
     * @param {array} person
     */
    function makeName(person) {
      return person.family_name + ', ' + person.given_name + ', ' + person.title;
    }
  }])
  .directive('uqlAuthorsStaffCollaboration', ['$compile', function ($compile) {
    return {
      restrict: 'E',
      controller: 'uqlAuthorsStaffCollaborationCtrl',
      template: '<div></div>',
      replace: true,
      scope: {
        viz: '=',
        jsonData: '=data'
      },
      link: function postLink(scope, element, attrs) {
        var startAngle = 90; // where will the first collaborator appear around the circle
        var pointRadius = 20; // how big will the collaborators be
        var vizPadding = pointRadius * 2; // distance from the edges
        var radiusRange; // global function passed around
        var circleRadius; // the big circle's radiusRange
        var minVal = 120; // closest collaborator can be to the centre
        // these can be passed in by calling site
        var height = attrs.height || 480;
        var width = attrs.width || 640;

        /**
         * This is a container we will use to put/display text
         *
         * @param svg
         * @param centerXPos
         * @param centerYPos
         */
        function addTextContainer(svg, centerXPos, centerYPos) {
          var textCont = svg.append('g').attr('id', 'text-container');
          textCont.attr('transform', 'translate(' + centerXPos + ', ' + centerYPos + ')');
        }

        /**
         * THis is a container we will use to hold collaborators
         *
         * @param svg
         * @param centerXPos
         * @param centerYPos
         */
        function addCollaboratorContainer(svg, centerXPos, centerYPos) {
// display the top collaborators for the author here
          var topCollab = svg.append('g').attr('id', 'top-collab');

          topCollab.append('clipPath')
            .attr('id', 'authorClipCircle')
            .append('circle')
            .attr('r', ((pointRadius * 2) - 1))
            .attr('transform', 'translate(' + (pointRadius * 2.5) + ', ' + (pointRadius * 2.4) + ')');

          topCollab.append('clipPath')
            .attr('id', function () {
              return 'clipCircle';
            })
            .append('circle')
            .attr('r', pointRadius - 2)
            .attr('transform', 'translate(' + (pointRadius * 1.3) + ', ' + (pointRadius * 1.1) + ')');

          topCollab.attr('transform', 'translate(' + centerXPos + ', ' + centerYPos + ')');
        }

        /**
         * This is where we set up the graph
         */
        function initGraph(height, width) {
          // remove anything for repeat showings
          d3.select('#chart_div > span').remove();

          // create the svg canvas to display graph on
          var svg = d3.select('#chart_div').append('svg')
            .attr('width', width)
            .attr('height', height)
            .attr('viewbox', '0 0 ' + width + ' ' + height)
            .attr('preserveAspectRatio', 'xMidYMid');

          // need a circle so find constraining dimension
          var heightCircleConstraint = height - (vizPadding * 2);
          var widthCircleConstraint = width - (vizPadding * 2);
          // global as used in radiusRange function
          circleRadius = d3.min([heightCircleConstraint, widthCircleConstraint]) / 2;

          // attach everything to the group that is centered around middle
          var centerXPos = width / 2;
          var centerYPos = height / 2;

          addCollaboratorContainer(svg, centerXPos, centerYPos);
          addTextContainer(svg, centerXPos, centerYPos);
        }

        /**
         * Add text to the passed in container
         *
         * @param authorContainers
         * @param axes
         * @param cssClass
         */
        function addCollaboratorText(authorContainers, axes, cssClass) {
          authorContainers.append('text')
            .attr('class', cssClass)
            .attr('text-anchor', 'middle')
            .attr('dy', Math.round(pointRadius * 1.6) + 'px')
            .attr('transform', function (d, i) {
              var rotate = ((i / axes) * 360) - startAngle;
              return 'rotate(' + -(rotate) + ')';
            })
            .text(function (d) {
              return d.name;
            });
        }

        /**
         * Add the names below the circles
         *
         * @param authorContainers
         * @param textContainer
         * @param axes
         * @param duration_func
         */
        function showCollaboratorNames(authorContainers, textContainer, axes, duration_func) {
          authorContainers.attr('class', function (d) {
            return d.username + ' author-text';
          })
            .style('opacity', 0)
            .on('mouseover', function (d) {
              bringToFront(textContainer[0][0], d3.select('.' + d.username + '.author-text')[0][0]);
            });

          addCollaboratorText(authorContainers, axes, 'shadow-text');

          addCollaboratorText(authorContainers, axes, 'name-text');

          authorContainers.transition()
            .delay(function (d, i) {
              return duration_func(d, i);
            })
            .duration(300)
            .style('opacity', 1);
        }

        /**
         * Add the image in a circle for the collaborators
         *
         * @param collabContainers
         * @param fill
         * @param axes
         */
        function addCollaboratorImages(collabContainers, fill, axes) {
          collabContainers.append('svg:circle')
            .attr('class', 'collab-circle')
            .attr('stroke', function (d, i) {
              return fill(i);
            })
            .attr('r', pointRadius);

          collabContainers.append('svg:image')
            .attr('class', 'collab-image')
            .attr('xlink:href', function (d) {
              return d.image;
            })
            .attr('transform', function (d, i) {
              var rotate = ((i / axes) * 360) - startAngle;
              return 'rotate(' + -(rotate) +
              ')translate(-' + (pointRadius * 1.3) + ', -' + (pointRadius * 1.1) + ')';
            })
            .attr('clip-path', function () {
              return 'url(#clipCircle)';
            })
            .attr('height', pointRadius * 2.5)
            .attr('width', pointRadius * 2.5);
        }

        /**
         * Show images for all the collaborators
         * @param collabContainers
         * @param textContainer
         * @param fill
         * @param duration_func
         * @param axes
         */
        function showCollaboratorImages(collabContainers, textContainer, fill, duration_func, axes) {
          collabContainers.attr('class', function (d) {
            return d.username + ' collaborators';
          })
            .on('click', function (d) {
              if (scope.clickfn) {
                return scope.clickfn(d);
              }
            })
            .on('mouseover', function (d) {
              bringToFront(textContainer[0][0], d3.select('.' + d.username + '.author-text')[0][0]);
            });

          var lines = collabContainers.append('svg:line')
            .attr('x2', 0)
            .attr('class', 'collab-line')
            .style('stroke', function (d, i) {
              return fill(i);
            });

          lines.transition()
            .duration(duration_func)
            .attr('x2', function (d) {
              var translate = radiusRange(d.collaborations);
              return -(translate);
            });

          addCollaboratorImages(collabContainers, fill, axes);
        }

        /**
         * This is where the magic of showing the collaborators happens
         * using D3's idiosyncratic syntax
         *
         * globals: radiusRange, minVal
         *
         * @param {Object[]} collaborations  authors collaborations
         */
        function showCollaborators(collaborations) {
          // find the largest and smallest edge numbers
          var edgeExtent = d3.extent(collaborations, function (d) {
            return d.collaborations;
          });

          // now create a linear scale to position the collaborators a given distance
          // from the center of the circle.  We map the min and max collabs we got above
          // to
          radiusRange = d3.scale.linear().domain(edgeExtent)
            .rangeRound([(circleRadius), minVal]);

          var axes = collaborations.length;

          var fill = d3.scale.category20b();

          var canvas = d3.select('#top-collab');
          var textContainer = d3.select('#text-container');

          var duration_func = function (d, i) {
            return (i + 2) * 150; // arbitrary number to slow down the animation
          };

          var collabContainers = radiusCurve('collaborators', canvas, collaborations, axes, duration_func);
          showCollaboratorImages(collabContainers, textContainer, fill, duration_func, axes);

          var authorContainers = radiusCurve('author-text', textContainer, collaborations, axes, duration_func);
          showCollaboratorNames(authorContainers, textContainer, axes, duration_func);
        }

        /**
         * Add the authors image in a circle to the passed in canvas
         *
         * @param authorCanvas
         * @param author
         */
        function addAuthorImage(authorCanvas, author) {
          var filter = authorCanvas.append('defs')
            .append('filter')
            .attr('id', 'author-drop-shadow');

          filter.append('feGaussianBlur')
            .attr('in', 'SourceAlpha')
            .attr('stdDeviation', 4)
            .attr('result', 'blur');
          filter.append('feOffset')
            .attr('in', 'blur')
            .attr('dx', 2)
            .attr('dy', 2)
            .attr('result', 'offsetBlur');

          var feMerge = filter.append('feMerge');

          feMerge.append('feMergeNode')
            .attr('in', 'offsetBlur');
          feMerge.append('feMergeNode')
            .attr('in', 'SourceGraphic');

          authorCanvas.append('svg:circle')
            .attr('class', 'author-circle')
            .attr('r', pointRadius * 2)
            .attr('filter', 'url(#author-drop-shadow)');

          authorCanvas.append('svg:image')
            .attr('class', 'author-image')
            .attr('xlink:href', author.image)
            .attr('transform', 'translate(-' + (pointRadius * 2.5) + ', -' + (pointRadius * 2.4) + ')')
            .attr('clip-path', 'url(#authorClipCircle)')
            .attr('height', pointRadius * 5)
            .attr('width', pointRadius * 5);

        }

        /**
         * Add the authors name to the passed in canvas
         *
         * @param authorCanvas
         * @param author
         */
        function addAuthorName(authorCanvas, author) {
          authorCanvas.append('text')
            .attr('class', 'shadow-text')
            .attr('text-anchor', 'middle')
            .attr('dy', Math.round(pointRadius * 2.8) + 'px')
            .text(author.name);

          authorCanvas.append('text')
            .attr('text-anchor', 'middle')
            .attr('dy', Math.round(pointRadius * 2.8) + 'px')
            .text(author.name);
        }

        /**
         * Show the author in the middle
         *
         * @param {Object} author  author
         */
        function showAuthor(author) {
          var canvas = d3.select('#top-collab');

          d3.select('g.author')
            .transition()
            .duration(1000)
            .remove();

          var authorCanvas = canvas.append('svg:g')
            .attr('class', 'author');

          addAuthorImage(authorCanvas, author);
          addAuthorName(authorCanvas, author);
        }

        /**
         * @param className
         * @param canvas
         * @param collaborations
         * @param axes
         * @param durationFunc
         * @returns {*}
         */
        function radiusCurve(className, canvas, collaborations, axes, durationFunc) {
          // remove all existing elements
          d3.selectAll('.' + className)
            .classed(className, false)
            .remove();

          var collaborators = canvas.selectAll(className)
            .data(collaborations, function (d) {
              // I never want the old data, so do this to make
              // sure nothing is in enter
              return d.username + d.collaborations + Date.now();
            });


          // create new groups for them, rotating them around their axis
          // so the text displays correctly
          var collabContainers = collaborators.enter().append('svg:g')
            .attr('transform', function (d, i) {
              var rotate = ((i / axes) * 360) - startAngle;
              var translate;
              if (className === 'author-text') {
                translate = radiusRange(d.collaborations);
              } else {
                translate = 0;
              }
              return 'rotate(' + rotate + ')translate(' + translate + ')';
            });

          if (className !== 'author-text') {
            // animate them to the outer edge of the circle
            collabContainers.transition()
              .duration(durationFunc)
              .attr('transform', function (d, i) {
                var rotate = ((i / axes) * 360) - startAngle;
                var translate = radiusRange(d.collaborations);
                return 'rotate(' + rotate + ')translate(' + translate + ')';
              });
          }

          return collabContainers;
        }

        /**
         * Bring this element to the front of container
         *
         * @param {Object} container
         * @param {Object} element
         */
        function bringToFront(container, element) {
          container.appendChild(element);
        }

        // go...
        var maxGraphAuthors = 15;
        var htmlText = '<div id="chart_div"><span>Graph loading...</span></div>';
        htmlText += '<uql-table filename="{{filename}}" data="tableData"></uql-table>';
        element.html('<span>' + htmlText + '</span>');
        $compile(element.contents())(scope);

        initGraph(height, width);

        scope.$watch('jsonData', function (jsonData) {
          if ((scope.viz.name === scope.expectedViz) && angular.isObject(jsonData) && jsonData.hasOwnProperty('nodes')) {
            var formattedData = scope.formatData(attrs.author, jsonData);
            if (formattedData) {
              showCollaborators(formattedData.collaborators.slice(0, maxGraphAuthors));
              showAuthor(formattedData.author);
            }
            scope.tableData = scope.formatTableData(jsonData);
          }
        });
      }
    };
  }]);
