import constant from './constant';
import jiggle from './jiggle';
import {x, y} from './xy';
import {quadtree} from 'd3-quadtree';

/**
 * The refinement of the existing Barnes-Hut implementation in D3
 * to fit the use case of the project. Previously the algorithm stored
 * strength as internal node, now the random child is stored as internal
 * node and the force calculations are done between the node and that internal
 * object if they are sufficiently far away.
 * The check to see if the nodes are far away was also changed to the one described in original Barnes-Hut paper.
 * @return {force} calculated forces.
 */
export default function () {
  var nodes,
    node,
    alpha,
    distance = constant(300),
    theta = 0.5;

    /**
   * Constructs a quadtree at every iteration and apply the forces by visiting
   * each node in a tree.
   * @param  {number} _ - controls the stopping of the
   *     particle simulations.
   */
  function force (_) {
    var i, n = nodes.length, tree = quadtree(nodes, x, y).visitAfter(accumulate);
    for (alpha = _, i = 0; i < n; ++i) {
      node = nodes[i]; tree.visit(apply);
    }
  }

  /**
   * Function used during the tree construction to fill out the nodes with
   * correct data. Internal nodes acquire the random child while the leaf
   * nodes accumulate forces from coincident quadrants.
   * @param  {quadrant} quad - node representing the quadrant in quadtree.
   */
  function accumulate (quad) {
    var q, d, children = [];

    // For internal nodes, accumulate forces from child quadrants.
    if (quad.length) {
      for (var i = 0; i < 4; ++i) {
        if ((q = quad[i]) && (d = q.data)) {
          children.push(d);
        }
      }
      // Choose a random child.
      quad.data = children[Math.floor(Math.random() * children.length)];
      quad.x = quad.data.x;
      quad.y = quad.data.y;
    } else { // For leaf nodes, accumulate forces from coincident quadrants.
      q = quad;
      q.x = q.data.x;
      q.y = q.data.y;
    }
  }

  /**
   * Function that applies the forces for each node. If the objects are
   * far away, the approximation is made. Otherwise, forces are calculated
   * directly between the nodes.
   * @param  {quadrant} quad - node representing the quadrant in quadtree.
   * @param  {number} x1   - lower x bound of the node.
   * @param  {number} _    - lower y bound of the node.
   * @param  {number} x2   - upper x bound of the node.
   * @return {boolean}     - true if the approximation was applied.
   */
  function apply (quad, x1, _, x2) {
    var x = quad.data.x + quad.data.vx - node.x - node.vx,
      y = quad.data.y + quad.data.vy - node.y - node.vy,
      w = x2 - x1,
      l = Math.sqrt(x * x + y * y);

    // Apply the Barnes-Hut approximation if possible.
    // Limit forces for very close nodes; randomize direction if coincident.
    if (w / l < theta) {
      if (x === 0) { x = jiggle(); l += x * x; }
      if (y === 0) { y = jiggle(); l += y * y; }
      if (quad.data) {
        l = (l - +distance(node, quad.data)) / l * alpha;
        x *= l; y *= l;
        quad.data.vx -= x;
        quad.data.vy -= y;
        node.vx += x;
        node.vy += y;
      }
      return true;
    } else if (quad.length) return; // Otherwise, process points directly.

    // Limit forces for very close nodes; randomize direction if coincident.
    if (quad.data !== node || quad.next) {
      if (x === 0) { x = jiggle(); l += x * x; }
      if (y === 0) { y = jiggle(); l += y * y; }
    }

    do {
      if (quad.data !== node) {
        l = (l - +distance(node, quad.data)) / l * alpha;
        x *= l; y *= l;
        quad.data.vx -= x;
        quad.data.vy -= y;
        node.vx += x;
        node.vy += y;
      }
    } while (quad = quad.next);
  }

  /**
    * Calculates the stress. Basically, it computes the difference between
    * high dimensional distance and real distance. The lower the stress is,
    * the better layout.
    * @return {number} - stress of the layout.
    */
  function getStress () {
    var totalDiffSq = 0, totalHighDistSq = 0;
    for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
      for (var j = 0; j < nodes.length; j++) {
        if (i !== j) {
          source = nodes[i]; target = nodes[j];
          realDist = Math.hypot(target.x - source.x, target.y - source.y);
          highDist = +distance(nodes[i], nodes[j]);
          totalDiffSq += Math.pow(realDist - highDist, 2);
          totalHighDistSq += highDist * highDist;
        }
      }
    }
    return Math.sqrt(totalDiffSq / totalHighDistSq);
  }

  // API for initializing the algorithm, setting parameters and querying
  // metrics.
  force.initialize = function (_) {
    nodes = _;
  };

  force.distance = function (_) {
    return arguments.length ? (distance = typeof _ === 'function' ? _ : constant(+_), force) : distance;
  };

  force.theta = function (_) {
    return arguments.length ? (theta = _, force) : theta;
  };

  force.stress = function () {
    return getStress();
  };

  return force;
}
