import { Entity, EntityNames } from ".";
import {
  BoundingBox,
  Component,
  ComponentNames,
  Life,
  Renderable,
} from "../components";
import { Coord2D, Dimension2D, DrawArgs } from "../interfaces";
import { colors } from "../utils";
import { normalRandom } from "../utils/random";

export interface ParticleSpawnOptions {
  spawnerDimensions: Dimension2D;
  center: Coord2D;
  spawnerShape: "circle" | "rectangle";
  particleShape: "circle" | "rectangle";
  particleCount: number;
  particleMeanLife: number;
  particleLifeVariance: number;
  particleMeanSize: number;
  particleSizeVariance: number;
  particleMeanSpeed: number;
  particleSpeedVariance: number;
  particleColors: Array<string>;
}

const DEFAULT_PARTICLE_SPAWN_OPTIONS: ParticleSpawnOptions = {
  spawnerDimensions: { width: 0, height: 0 },
  center: { x: 0, y: 0 },
  spawnerShape: "circle",
  particleShape: "circle",
  particleCount: 50,
  particleMeanLife: 200,
  particleLifeVariance: 50,
  particleMeanSize: 12,
  particleSizeVariance: 1,
  particleMeanSpeed: 2,
  particleSpeedVariance: 1,
  particleColors: [colors.gray, colors.aqua, colors.lightAqua],
};

interface Particle {
  position: Coord2D;
  velocity: Coord2D;
  dimension: Dimension2D;
  color: string;
  life: number;
  shape: "circle" | "rectangle";
}

class ParticleRenderer extends Component implements Renderable {
  private particles: Array<Particle>;
  private onDeath?: () => void;

  constructor(particles: Array<Particle> = [], onDeath?: () => void) {
    super(ComponentNames.Sprite);

    this.particles = particles;
    this.onDeath = onDeath;
  }

  public update(dt: number) {
    this.particles = this.particles.filter((particle) => {
      particle.position.x += particle.velocity.x * dt;
      particle.position.y += particle.velocity.y * dt;
      particle.life -= dt;
      return particle.life > 0;
    });

    if (this.particles.length === 0 && this.onDeath) {
      this.onDeath();
    }
  }

  public draw(ctx: CanvasRenderingContext2D, _drawArgs: DrawArgs) {
    for (const particle of this.particles) {
      ctx.fillStyle = particle.color;
      if (particle.shape === "circle") {
        ctx.beginPath();

        ctx.ellipse(
          particle.position.x,
          particle.position.y,
          particle.dimension.width / 2,
          particle.dimension.height / 2,
          0,
          0,
          Math.PI * 2
        );
        ctx.fill();
      } else {
        ctx.fillRect(
          particle.position.x - particle.dimension.width / 2,
          particle.position.y - particle.dimension.height / 2,
          particle.dimension.width,
          particle.dimension.height
        );
      }
    }
  }
}

export class Particles extends Entity {
  constructor(options: Partial<ParticleSpawnOptions>) {
    super(EntityNames.Particles);

    const spawnOptions = {
      ...DEFAULT_PARTICLE_SPAWN_OPTIONS,
      ...options,
    };
    const particles = Array(options.particleCount)
      .fill(0)
      .map(() => Particles.spawnParticle(spawnOptions));

    this.addComponent(new Life(true));
    this.addComponent(
      new ParticleRenderer(particles, () => {
        const life = this.getComponent<Life>(ComponentNames.Life);
        life.alive = false;
        this.addComponent(life);
      })
    );

    this.addComponent(
      new BoundingBox(
        {
          x: 0,
          y: 0,
        },
        {
          width: spawnOptions.spawnerDimensions.width,
          height: spawnOptions.spawnerDimensions.height,
        },
        0
      )
    );
  }

  static spawnParticle(options: ParticleSpawnOptions) {
    const angle = Math.random() * Math.PI * 2;
    const speed = normalRandom(
      options.particleMeanSpeed,
      options.particleSpeedVariance
    );
    const life = normalRandom(
      options.particleMeanLife,
      options.particleLifeVariance
    );
    const size = normalRandom(
      options.particleMeanSize,
      options.particleSizeVariance
    );
    const color =
      options.particleColors[
        Math.floor(Math.random() * options.particleColors.length)
      ];
    const position = {
      x:
        options.center.x +
        (Math.cos(angle) * options.spawnerDimensions.width) / 2,
      y:
        options.center.y +
        (Math.sin(angle) * options.spawnerDimensions.height) / 2,
    };
    if (options.spawnerShape === "rectangle") {
      // determine a random position on the edge of the spawner based on the angle
      const halfWidth = options.spawnerDimensions.width / 2;
      const halfHeight = options.spawnerDimensions.height / 2;

      if (angle < Math.PI / 4 || angle > (Math.PI * 7) / 4) {
        position.x += halfWidth;
        position.y += Math.tan(angle) * halfWidth;
      } else if (angle < (Math.PI * 3) / 4) {
        position.y += halfHeight;
        position.x += halfHeight / Math.tan(angle);
      } else if (angle < (Math.PI * 5) / 4) {
        position.x -= halfWidth;
        position.y -= Math.tan(angle) * halfWidth;
      } else {
        position.y -= halfHeight;
        position.x -= halfHeight / Math.tan(angle);
      }
    }

    return {
      position,
      velocity: {
        x: Math.cos(angle) * speed,
        y: Math.sin(angle) * speed,
      },
      color,
      life,
      dimension: { width: size, height: size },
      shape: options.particleShape,
    };
  }
}
