import {
  ACCELERATION_VECTOR_ARROWHEAD,
  ACCELERATION_VECTOR_COLOR,
  ACCELERATION_VECTOR_WIDTH,
  OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
  OFFSCREEN_OBJECT_LINE_SCALE,
  OFFSCREEN_OBJECT_LINE_WIDTH,
  PATH_TRACES_COLOR,
  PATH_TRACES_DASHED_OPACITY,
  PATH_TRACES_OPACITY,
  PATH_TRACES_WIDTH,
  SELECTED_GROUP_COLOR,
  SELECTED_MARGIN,
  SELECTED_SINGLE_COLOR,
  VELOCITY_VECTOR_ARROWHEAD,
  VELOCITY_VECTOR_COLOR,
  VELOCITY_VECTOR_WIDTH,
} from './config.js';
import {add, components, copy, direction, div, magnitude, mult, square, zero} from './vector.js';

export class MassObject {
  sim = undefined;
  id = undefined;
  mass = 0;
  density = 1;
  position = {x: undefined, y: undefined};
  velocity = zero;
  acceleration = zero;
  color = {r: undefined, g: undefined, b: undefined};
  timeCreated = undefined;
  forces = []; // [{x, y}]
  history = [];
  alive = true;
  workDoneByPointer = 0;
  workDoneByForces = 0;

  currentPosition = undefined;
  currentAcceleration = undefined;

  constructor(sim, x, y) {
    this.sim = sim;
    this.id = crypto.randomUUID();
    this.position.x = x;
    this.position.y = y;
    this.color.r = Math.random() * 256;
    this.color.g = Math.random() * 256;
    this.color.b = Math.random() * 256;
    this.timeCreated = this.sim.time;
  }

  toJSON() {
    return {
      id: this.id,
      mass: this.mass,
      density: this.density,
      position: this.position,
      velocity: this.velocity,
      color: this.color,
      timeCreated: this.timeCreated,
      alive: this.alive,
      workDoneByPointer: this.workDoneByPointer,
      workDoneByForces: this.workDoneByForces,
    };
  }

  fromJSON(obj) {
    this.id = obj.id;
    this.mass = obj.mass;
    this.density = obj.density;
    this.position = copy(obj.position);
    this.velocity = copy(obj.velocity);
    this.color = obj.color;
    this.timeCreated = obj.timeCreated;
    this.alive = obj.alive;
    this.history = [];
    this.workDoneByPointer = obj.workDoneByPointer;
    this.workDoneByForces = obj.workDoneByForces;
  }

  get age() {
    return this.sim.time - this.timeCreated;
  }

  get radius() {
    // radius should be proportional to cube root of mass
    return Math.pow(this.mass / this.density, 1 / 3);
  }

  getAcceleration() {
    const netForce = this.forces.reduce((t, f) => add(t, f), zero);
    return div(netForce, this.mass);
  }

  get kineticEnergy() {
    return this.mass * square(this.velocity) / 2;
  }

  get momentum() {
    return mult(this.mass, this.velocity);
  }

  drawPath(display) {
    const {ctx} = display;
    const {color: {r, g, b}} = this;

    // Draw path traces
    if (this.sim.getOption('display.traces') && this.history?.length) {
      const dashedTraces = this.sim.getOption('display.dashedTraces');
      const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
      ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
        `rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
      ctx.lineWidth = PATH_TRACES_WIDTH / display.scale;
      ctx.beginPath();
      let dash = false;
      for (let i = 0; i < this.history.length; i++) {
        // if (i % 2 > 0) continue;
        const {position: {x, y}} = this.history[i];
        if (dashedTraces) {
          if (dash) {
            ctx.lineTo(x, y);
          } else {
            ctx.moveTo(x, y);
          }
          dash = !dash;
        } else {
          ctx.lineTo(x, y);
        }
      }
      ctx.stroke();
    }
  }

  drawArrows(display) {
    const {
      color: {r, g, b},
      position: {x, y},
      velocity: {x: vx, y: vy},
      acceleration,
      radius,
    } = this;

    const {
      height: H,
      width: W,
      viewOrigin: {x: ox, y: oy},
    } = display;

    const cx = ox + W / 2;
    const cy = oy + H / 2;

    if (!this.alive) return;

    // If the object is outside the display area, skip drawing arrows
    if (Math.abs(x - cx) - radius >= W / 2 ||
      Math.abs(y - cy) - radius >= H / 2) {
      return;
    }

    // Draw arrow for the velocity
    if (this.sim.getOption('display.velocity')) {
      const vecScale = this.sim.getOption('display.velocityScale');
      const selected = this.sim.system.getSelectedOrCreating();
      const isSelected = selected?.id === this.id;
      let velocity = {x: vx, y: vy};
      if (isSelected) {
        // If this object is being dragged by the user,
        // show the pointer velocity instead of object velocity
        let pointerV = this.sim.pointer.latestVelocity;
        if (this.sim.getOption('compensate.timeScale')) {
          pointerV = div(pointerV, this.sim.timeScale);
        }
        velocity = add(velocity, pointerV);
      }
      const speed = magnitude(velocity);
      const arrowDirection = direction(velocity);
      // Prevent negative numbers by adding 1
      // Make arrow lengths appear consistent regardless of display scale
      let arrowLength = Math.log10(speed + 1) * vecScale;
      if (!this.sim.getOption('display.zoomVectors')) {
        arrowLength /= display.scale;
      }
      // const offset = mult({x: Math.cos(
      const offset = components(radius, arrowDirection)
      const start = add({x, y}, offset);
      const end = add(start, components(arrowLength, arrowDirection));
      const style = VELOCITY_VECTOR_COLOR === 'object color' ?
        `rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
      display.drawArrow(start.x, start.y, end.x, end.y, {
        style,
        width: VELOCITY_VECTOR_WIDTH,
        arrowhead: VELOCITY_VECTOR_ARROWHEAD,
        fill: false,
        ifShort: 'head'
      });
    }

    // Draw arrow for acceleration
    if (this.sim.getOption('display.acceleration')) {
      const vecScale = this.sim.getOption('display.accelerationScale');
      const accel = magnitude(acceleration);
      const arrowDirection = direction(acceleration);
      // Prevent negative numbers by adding 1
      // Make arrow lengths appear consistent regardless of display scale
      let arrowLength = Math.log10(accel + 1) * vecScale;
      if (!this.sim.getOption('display.zoomVectors')) {
        arrowLength /= this.sim.display.scale;
      }
      const offset = components(radius, arrowDirection)
      const start = add({x, y}, offset);
      const end = add(start, components(arrowLength, arrowDirection));
      const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
        `rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
      display.drawArrow(start.x, start.y, end.x, end.y, {
        style,
        width: ACCELERATION_VECTOR_WIDTH,
        arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
        fill: false,
        ifShort: 'tail'
      });
    }
  }

  drawObject(display) {
    const {
      color: {r, g, b},
      position: {x, y},
      radius,
    } = this;

    const {
      ctx,
      height: H,
      width: W,
      viewOrigin: {x: ox, y: oy},
    } = display;

    const cx = ox + W / 2;
    const cy = oy + H / 2;

    if (!this.alive) return;

    // If the object is outside the display area, draw an arrow at the edge of the display
    if (Math.abs(x - cx) - radius >= W / 2 ||
      Math.abs(y - cy) - radius >= H / 2) {
      // Find where a line from center of display to object intersects display edge
      let px, py;
      if (y <= cy) {
        // Line intersects y = 0:
        const y0px = cx + (H / 2) / (cy - y) * (x - cx);
        if (Math.abs(y0px - cx) <= W / 2) {
          px = y0px;
          py = oy;
        }
      } else {
        // Line intersects y = H
        const yHpx = cx + (H / 2) / (y - cy) * (x - cx);
        if (Math.abs(yHpx - cx) <= W / 2) {
          px = yHpx;
          py = oy + H;
        }
      }
      if (px === undefined) {
        if (x <= cx) {
          // Line intersects x = 0:
          px = ox;
          py = cy + (W / 2) / (cx - x) * (y - cy);
        } else {
          // Line intersects x = W:
          px = ox + W;
          py = cy + (W / 2) / (x - cx) * (y - cy);
        }
      }

      const arrowDirection = Math.atan2(py - cy, px - cx);
      // Length of arrow based on distance (logarithmic scale)
      const d = Math.sqrt((x - px) ** 2, (y - py) ** 2);
      const arrowLength = Math.log(d + 1) * OFFSCREEN_OBJECT_LINE_SCALE / display.scale;
      const startAx = px - arrowLength * Math.cos(arrowDirection);
      const startAy = py - arrowLength * Math.sin(arrowDirection);
      display.drawArrow(startAx, startAy, px, py, {
        style: `rgb(${r}, ${g}, ${b})`,
        width: OFFSCREEN_OBJECT_LINE_WIDTH,
        arrowheadLength: OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
        fill: false,
        ifShort: 'head',
      });

      return;
    }

    // Draw filled circle for the object
    ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
  }

  drawSelection(display) {
    const {
      position: {x, y},
      radius,
    } = this;
    const {ctx} = display;

    if (!this.alive) return;

    // If object is currently selected, draw a circle around it
    const isInSelectedGroup = this.sim.select.selectedGroup?.some(({id}) => id === this.id);
    const isSelectedSingle = this.sim.select.selectedSingle?.id === this.id;
    if (isSelectedSingle || isInSelectedGroup) {
      ctx.strokeStyle = isSelectedSingle ? SELECTED_SINGLE_COLOR : SELECTED_GROUP_COLOR;
      ctx.beginPath();
      ctx.arc(x, y, radius + SELECTED_MARGIN / display.scale, 0, 2 * Math.PI);
      ctx.stroke();
    }
  }
}

