import {pointOnCircle, sumDistError} from "./helpers";
import jiggle from "../jiggle";

/**
 * Phase 2 and 3 of each node to be interpolated.
 * After a parent and high-dimensional distance to parent is found, imagine a
 * circle around the parent with the distance as radius.
 * Phase 2: Determine the quadrant of the circle the node is likely to be in.
 *          Then, perform a binary search on the quardrant to find the best
 *          position on the quadrant and place the node there.
 *          The sum of ( difference between high-low-d distances between the
 *          node and other sampled nodes ) is used to determine the prefered
 *          position (lower is better).
 * Phase 3: A subset of points whose 2D position is known are selected.
            For several iterations:
 *            Calculate the total force between the node and other nodes in the subset
 *            Apply the force to the node's position
 * @param  {object} node           - the node to be interpolated
 * @param  {object} nearNeighbour  - the parent of the node
 * @param  {number} radius         - high-dimensional distance to the parent
 * @param  {list} sampleSubset     - subset for phase 3, list of nodes
 * @param  {list} realDistances    - high-dimensional distances from the node to each
                                     of the node in sampleSubset, list of numbers
                                     index must correspond to sampleSubset
 * @param  {Integer} endingIts     - Number of iterations for phase 3
 */
export function placeNearToNearestNeighbour(node, nearNeighbour, radius, sampleSubset, realDistances, endingIts) {
  let
    sumDistErrorByAngle = function(angle){
      return sumDistError(pointOnCircle(nearNeighbour.x, nearNeighbour.y, angle, radius), sampleSubset, realDistances);
    },
    dist0 = sumDistErrorByAngle(0),
    dist90 = sumDistErrorByAngle(90),
    dist180 = sumDistErrorByAngle(180),
    dist270 = sumDistErrorByAngle(270),
    lowBound = 0.0,
    highBound = 0.0;

  // Determine the closest quadrant
  if (dist0 == dist180) {
    if (dist90 > dist270)
      lowBound = highBound = 270;
    else
      lowBound = highBound = 90;
  } else if (dist90 == dist270) {
    if (dist0 > dist180)
      lowBound = highBound = 180;
    else
      lowBound = highBound = 0;
  } else if (dist0 > dist180) {
    if (dist90 > dist270) {
      lowBound = 180;
      highBound = 270;
    } else {
      lowBound = 90;
      highBound = 180;
    }
  } else {
    if (dist90 > dist270) {
      lowBound = 270;
      highBound = 360;
    } else {
      lowBound = 0;
      highBound = 90;
    }
  }

  // Determine the angle
  let angle = binarySearchMin(lowBound, highBound,sumDistErrorByAngle);
  let newPoint = pointOnCircle(nearNeighbour.x, nearNeighbour.y, angle, radius);
  node.x = newPoint.x;
  node.y = newPoint.y;

  // Phase 3
  let
    multiplier = 1/sampleSubset.length,
    sumForces;
  for (let i = 0; i < endingIts; i++) {
    sumForces = sumForcesToSample(node, sampleSubset, realDistances);
    node.x += sumForces.x*multiplier;
    node.y += sumForces.y*multiplier;
  }
}

function sumForcesToSample(node, samples, sampleCache) {
  let nodeVx = 0,
      nodeVy = 0,
      x, y, l, i, sample;

  for (i = samples.length-1; i >=0 ; i--) {
    sample = samples[i];

    // jiggle so l won't be zero and divide by zero error after this
    x = node.x - sample.x || jiggle();
    y = node.y - sample.y || jiggle();
    l = Math.sqrt(x * x + y * y);
    l = (l - sampleCache[i]) / l;
    x *= l, y *= l;
    nodeVx -= x;
    nodeVy -= y;
  }
  return {x: nodeVx, y: nodeVy};
}

/**
 * Perform binary search on an integer range to find the value x where f(x) is minimum.
 * This implies that f(x) should be binary-searchable over the specified range.
 * @param  {integer} lb     - lower bound of the integer range, must be <= to hb
 * @param  {integer} hb     - higher bound of the integer range
 * @param  {function(x)} fn - function that takes in a number x and returns a number
 * @return {integer}        - an integer x where f(x) is minimum
 */
function binarySearchMin(lb, hb, fn) {
  while (lb <= hb) {
    if(lb === hb) return lb;

    if(hb-lb == 1) {
      if (fn(lb) >= fn(hb)) return hb;
      else return lb;
    }

    let
      range = hb-lb,
      valLowerHalf = fn(lb + range/4),
      valHigherHalf = fn(lb + range*3/4);

    if (valLowerHalf > valHigherHalf)
      lb = Math.floor((lb + hb) / 2);
    else if (valLowerHalf < valHigherHalf)
      hb = Math.ceil((lb + hb) / 2);
    else {
      lb += Math.floor(range/4);
      hb -= Math.ceil(range/4);
    }
  }
  return -1;
}
