import constant from "./constant";
import jiggle from "./jiggle";

/**
 * Modified link force algorithm
 * - simplify calculations for parameters locked for spring model
 * - replace the use of links {} with loop. greatly reduce memory usage
 * - removed other unused functions
 * Alpha should be constant 1 for accurate simulation
 */
export default function() {
  var dataSizeFactor,
      distance = constant(30),
      distances = [],
      nodes,
      stableVelocity = 0,
      stableVeloHandler = null,
      latestVelocityDiff = 0,
      iterations = 1;

  function force(alpha) {
    let n = nodes.length;
    // Cache old velocity for comparison later
    if (stableVeloHandler!==null && stableVelocity>=0) {
      for (let i = n-1, node; i>=0; i--) {
        node = nodes[i];
        node.oldvx = node.vx;
        node.oldvy = node.vy;
      }
    }
    
    // Each iteration in a tick
    for (var k = 0, source, target, i, j, x, y, l; k < iterations; ++k) {
      // For each link
      for (i = 1; i < n; i++) for (j = 0; j < i; j++) {
        // jiggle so l won't be zero and divide by zero error after this
        source = nodes[i];
        target = nodes[j];
        x = target.x + target.vx - source.x - source.vx || jiggle();
        y = target.y + target.vy - source.y - source.vy || jiggle();
        l = Math.sqrt(x * x + y * y);
        l = (l - distances[i*(i-1)/2+j]) / l  * dataSizeFactor * alpha;
        x *= l, y *= l;
        target.vx -= x;
        target.vy -= y;
        source.vx += x;
        source.vy += y;
      }
    }

    // Calculate velocity changes, aka force applied.
    if (stableVeloHandler!==null && stableVelocity>=0) {
      let velocityDiff = 0;
      for (let i = n-1, node; i>=0; i--) {
        node = nodes[i];
        velocityDiff += Math.abs(Math.hypot(node.vx-node.oldvx, node.vy-node.oldvy));
      }
      velocityDiff /= n;
      latestVelocityDiff = velocityDiff;

      if(velocityDiff<stableVelocity){
        stableVeloHandler();
      }
    }
  }

  function initialize() {
    if (!nodes) return;
    // 0.5 to divide the force to two part for source and target node
    dataSizeFactor = 0.5/(nodes.length-1);
    initializeDistance();
  }

  function initializeDistance() {
    if (!nodes) return;
    for (let i = 1, n = nodes.length; i < n; i++) {
      for (let j = 0; j < i; j++) {
        distances.push(distance(nodes[i], nodes[j]));
      }
    }
  }

  force.initialize = function(_) {
    nodes = _;
    initialize();
  };

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

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

  force.latestAccel = function () {
    return latestVelocityDiff;
  };

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

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