import {dispatch} from "d3-dispatch";
import constant from "./constant";
import interpBruteForce from "./interpolation/interpBruteForce";
import interpolationPivots from "./interpolation/interpolationPivots";
import {takeSampleFrom} from "./interpolation/helpers";

/**
 * An implementation of Chalmers, Morrison, and Ross' 2002 hybrid layout
 * algorithm with an option to use the 2003 pivot-based near neighbour searching
 * method.
 * It performs the 1996 neighbour sampling spring simulation model, on a
 * "sample set", sqrt(n) samples of the data.
 * Other data points are then interpolated into the model.
 * Finally, another spring simulation may be performed on the entire dataset to
 * clean up the model.
 * @param {object} sim    - D3 Simulation object
 * @param {object} forceS - Pre-configured D3 force object for the sample set.
                            The ending handler will be re-configured.
                            Neighbour sampling force is expected, but other D3
                            forces may also work.
 * @param {object} forceF - Pre-configured D3 force object for the simultion ran
                            on the entire dataset at the end.
                            Neighbour sampling force is expected, but other D3
                            forces may also work.
                            The force should not have any ending condition.
 */
export default function (sim, forceS, forceF) {
  var
    SAMPLE_ITERATIONS = 300,
    FULL_ITERATIONS   = 20,
    interpDistanceFn,
    NUM_PIVOTS         = 0,
    INTERP_FINE_ITS   = 20,
    sample = [],
    remainder = [],
    simulation = sim,
    forceSample = forceS,
    forceFull = forceF,
    event = d3.dispatch("sampleTick", "fullTick", "startInterp", "end"),
    initAlready = false,
    nodes,
    alreadyRanIterations,
    hybrid;

    if(simulation != undefined) initSimulation();
    if(forceS != undefined || forceF != undefined) initForces();

  // Performed on first run
  function initialize() {
    initAlready = true;
    console.log("Initializing Hybrid");
    alreadyRanIterations = 0;
    simulation
      .on("tick", sampleTick)
      .on("end", sampleEnded)
      .nodes(sample)
      .force("Sample force", forceSample);
    console.log("Initialized Hybrid");
  }

  function initForces(){
    if (forceSample.onStableVelo) {
      forceSample.onStableVelo(sampleEnded);
    }

    // Set default value for interpDistanceFn if not been specified yet
    if(interpDistanceFn === undefined) {
      if(forceFull.distance == 'function')
        interpDistanceFn = forceFull.distance();
      else
        interpDistanceFn = constant(300);
    }
  }

  function initSimulation(){
    nodes = simulation.nodes();
    simulation
      .stop()
      .alphaDecay(0)
      .alpha(1)

    let sets = takeSampleFrom(nodes, Math.sqrt(nodes.length));
    sample = sets.sample;
    remainder = sets.remainder;
  }

  // Sample simulation ticked 1 frame, keep track of number of iterations here.
  function sampleTick() {
    event.call("sampleTick");
    if(++alreadyRanIterations >= SAMPLE_ITERATIONS){
      sampleEnded();
    }
  }

  // Full simulation ticked 1 frame, keep track of number of iterations here.
  function fullTick() {
    event.call("fullTick");
    if(++alreadyRanIterations >= FULL_ITERATIONS){
      simulation.stop();
      initAlready = false;
      simulation.force("Full force", null);
      event.call("end");
    }
  }

  function sampleEnded() {
    simulation.stop();
    simulation.force("Sample force", null);
    // Reset velocity of all nodes
    for (let i=sample.length-1; i>=0; i--){
      sample[i].vx=0;
      sample[i].vy=0;
    }

    event.call("startInterp");
    if (NUM_PIVOTS>=1) {
      interpolationPivots(sample, remainder, NUM_PIVOTS, interpDistanceFn, INTERP_FINE_ITS);
    } else {
      interpBruteForce(sample, remainder, interpDistanceFn, INTERP_FINE_ITS);
    }

    event.call("fullTick");
    alreadyRanIterations = 0;
    simulation
      .on("tick", null)
      .on("end", null) // The ending condition should be iterations count
      .nodes(nodes);

    if (FULL_ITERATIONS<1 || forceF === undefined || forceF === null) {
      event.call("end");
      return;
    }
    simulation
      .on("tick", fullTick)
      .force("Full force", forceFull)
      .restart();
  }

  return hybrid = {
    restart: function () {
      if(!initAlready) initialize();
      simulation.restart();
      return hybrid;
    },

    stop: function () {
      simulation.stop();
      return hybrid;
    },

    numPivots: function (_) {
      return arguments.length ? (NUM_PIVOTS = +_, hybrid) : NUM_PIVOTS;
    },

    sampleIterations: function (_) {
      return arguments.length ? (SAMPLE_ITERATIONS = +_, hybrid) : SAMPLE_ITERATIONS;
    },

    fullIterations: function (_) {
      return arguments.length ? (FULL_ITERATIONS = +_, hybrid) : FULL_ITERATIONS;
    },

    interpFindTuneIts: function (_) {
      return arguments.length ? (INTERP_FINE_ITS = +_, hybrid) : INTERP_FINE_ITS;
    },

    on: function (name, _) {
      return arguments.length > 1 ? (event.on(name, _), hybrid) : event.on(name);
    },

    subSet: function (_) {
      return arguments.length ? (sample = _, hybrid) : sample;
    },

    nonSubSet: function (_) {
      return arguments.length ? (remainder = _, hybrid) : remainder;
    },

    interpDistanceFn: function (_) {
      return arguments.length ? (interpDistanceFn = typeof _ === "function" ? _ : constant(+_), hybrid) : interpDistanceFn;
    },

    simulation: function (_) {
      return arguments.length ? (toInit = true, simulation = _, hybrid) : simulation;
    },

    forceSample: function (_) {
      return arguments.length ? (forceSample = _, initForces(), hybrid) : forceSample;
    },

    forceFull: function (_) {
      return arguments.length ? (forceFull = _, initForces(), hybrid) : forceFull;
    },

  };
}
