
import { Arithmetic, Vector, Scalar } from "../arithmetic";
import { Duration, MicroDuration, random, System } from "../helpers";

// read-only config used to initialize the system
export type Config = {
  dt: MicroDuration, // e.g. 5 miliseconds
  count: number,
  // Box in which to generate random particles with uniform distribution, and random velocities
  topLeft: {x: number, y: number},
  bottomRight: {x: number, y: number},
  maxSpeed: number,
}

export type State = {
  position: Vector,
  velocity: Vector,
}

export type Observables = {
  particleCount: number,
  averageSpeed: number,

  // bounding-box
  topLeft: {x: number, y: number},
  bottomRight: {x: number, y: number},
}

export type SimpleKinematicsSystem = System<State, Observables, {}>;

export function SimpleKinematicsSystem(config: Config): SimpleKinematicsSystem {
  const arith = Arithmetic({ count: 0, capacity: 1000 });
  const { Vector, Scalar } = arith;

  let time: Duration = 0;

  const state: State = {
    position: Vector.allocate(),
    velocity: Vector.allocate(),
  };
  const { velocity, position } = state;

  function reset() {
    arith.setCount(config.count);
    const count = arith.getCount();

    for (let i = 0; i < count; i++) {
      // Random position inside the bounding box
      position.x[i] = random(config.topLeft.x, config.bottomRight.x);
      position.y[i] = random(config.topLeft.y, config.bottomRight.y);

      // Random velocity between -maxVelocity and +maxVelocity
      velocity.x[i] = random(-1, 1) * config.maxSpeed;
      velocity.y[i] = random(-1, 1) * config.maxSpeed;
    }
  }
  reset();

  // TODO: how to initialize this without code-duplication?
  const observables: Observables = {
    // TODO: Is there a better way of doing this?
    // These are dummy values that will be overwritten
    particleCount: 0,
    averageSpeed: 0,
    topLeft: {x: 0, y: 0},
    bottomRight: {x: 0, y: 0},
  };

  const speed: Scalar = Scalar.allocate();
  function refreshObservables() {
    const count = arith.getCount();
    observables.particleCount = count;

    if (count === 0) return;

    // bounding-box
    let minX = Infinity, minY = Infinity;
    let maxX = -Infinity, maxY = -Infinity;

    for (let i = 0; i < count; i++) {
      const x = position.x[i];
      const y = position.y[i];
      if (x < minX) { minX = x; }
      if (x > maxX) { maxX = x; }
      if (y < minY) { minY = y; }
      if (y > maxY) { maxY = y; }
    }
    observables.topLeft.x = minX;
    observables.topLeft.y = minY;
    observables.bottomRight.x = maxX;
    observables.bottomRight.y = maxY;

    Vector.magnitude(speed, velocity);
    // Note that this would produce division by zero for `count === 0`.
    observables.averageSpeed = Scalar.average(speed);
  }

  // const acceleration: Vector = Vector.allocate();
  // const force: Vector = Vector.allocate();
  // const momentum: Vector = Vector.allocate();
  // const kineticEnergy: Scalar = Vector.allocate();
  function microFlow(dt: MicroDuration) {
    // position = position + dt*velocity
    Vector.addScaledByConstant(position, position, dt, velocity)
  }

  // Suppose that we need to flow for 43 miliseconds, and dt is 10 miliseconds.
  // Then we do micro-flow 4 times totalling 40 miliseconds. We don't do micro-flow for 3 seconds
  // - that could cause problems with integration.
  // Instead we remember this leftover time, so the next time we flow, we add this leftover time to the total time to flow.
  let leftover_time: Duration = 0;
  // Integrates the micro-flow, updates global time, and recomputes the observables.
  function flow(t: Duration) {
    // TODO: They way we compute leftover time is not so great. There must be a better way.
    const end_time = time + t + leftover_time;

    while (true) {
      const next_time = time + config.dt;
      if (next_time <= end_time) {
        time = next_time;
        microFlow(config.dt);
      } else {
        // assume at the start
        //   dt = 10 ms
        //
        //   time = 0
        //   leftover_time = 0
        //   t = 43 ms
        // then here
        //   next_time = 50 ms
        //   time      = 40 ms
        //   end_time  = 43 ms
        // so actually we have 3 miliseconds leftover, i.e. we set
        leftover_time = end_time - time;
        break;
      }
    }
  }

  return {
    state,
    observables,
    flow,
    refreshObservables,
    totalTime() { return time; },
    reset,
    capabilities: {},
  };
}

