import { system, world } from "@minecraft/server";

class Vector {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }

  static add(a, b) {
    return new Vector(a.x + b.x, a.y + b.y, a.z + b.z);
  }

  static subtract(a, b) {
    return new Vector(a.x - b.x, a.y - b.y, a.z - b.z);
  }

  static multiplyScalar(a, scalar) {
    return new Vector(a.x * scalar, a.y * scalar, a.z * scalar);
  }

  normalized() {
    let len = Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2) || 1;
    return new Vector(this.x / len, this.y / len, this.z / len);
  }

  toString() {
    return `${this.x.toFixed(2)} ${this.y.toFixed(2)} ${this.z.toFixed(2)}`;
  }
}

const SPAWN_DISTANCE = 30;
const SPAWN_Y_OFFSET = 60;
const INITIAL_SPEED = 0.03;
const GRAVITY = 0.002;
const TICKS_PER_SPAWN = 20;
const ENTITY_SPAWN_DELAY = 10;
const MIN_DELAY = 20 * 60 * 20;
const MAX_DELAY = 40 * 60 * 20;
const COOLDOWN = 15 * 60 * 20;

const GLOBAL_TICK_AREA_RADIUS = 40;
const STATIC_THRESHOLD_TICKS = 20;

class MeteorShower {
  constructor() {
    this.isActive = false;
    this.shower = null;
    this.spawnedMeteors = [];
    this.eventTicksRemaining = 0;
    this.globalTickAreaName = null;
  }

  triggerOnPlayer(player) {
    if (this.isActive) {
      player.sendMessage("There’s already an active meteor shower!");
      return;
    }
    player.runCommandAsync("playsound block.bell.use @s ~ ~ ~ 5 1 1");

    const origin = new Vector(
      player.location.x,
      player.location.y,
      player.location.z
    );
    const totalMeteors =
      Math.floor(Math.random() * (12 - 9 + 1)) + 9;
    this.shower = {
      remaining: totalMeteors,
      origin: origin,
      dimension: player.dimension,
      spawnCooldown: 0,
    };
    this.eventTicksRemaining = TICKS_PER_SPAWN * totalMeteors;
    this.isActive = true;

    this.globalTickAreaName = `meteor_shower_${Date.now()}`;
    const dim = this.shower.dimension;
    const x1 = Math.floor(origin.x - GLOBAL_TICK_AREA_RADIUS);
    const y1 = Math.floor(origin.y - GLOBAL_TICK_AREA_RADIUS);
    const z1 = Math.floor(origin.z - GLOBAL_TICK_AREA_RADIUS);
    const x2 = Math.floor(origin.x + GLOBAL_TICK_AREA_RADIUS);
    const y2 = Math.floor(origin.y + GLOBAL_TICK_AREA_RADIUS);
    const z2 = Math.floor(origin.z + GLOBAL_TICK_AREA_RADIUS);

    try {
      dim.runCommandAsync(
        `tickingarea add ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${this.globalTickAreaName} true`
      );
    } catch (_) {}

    player.sendMessage("You’ve activated a meteor shower near you!");
    system.run(() => this.tick());
  }

  tick() {
    if (this.eventTicksRemaining > 0) {
      if (
        this.shower.remaining > 0 &&
        this.shower.spawnCooldown <= 0
      ) {
        let spawnCount =
          Math.floor(Math.random() * 2) + 1;
        if (spawnCount > this.shower.remaining)
          spawnCount = this.shower.remaining;
        for (let i = 0; i < spawnCount; i++) {
          let offsetX =
            (Math.random() * 2 - 1) * SPAWN_DISTANCE;
          let offsetZ =
            (Math.random() * 2 - 1) * SPAWN_DISTANCE;
          if (Math.random() < 0.1) {
            offsetX = Math.random() * 4 - 2;
            offsetZ = Math.random() * 4 - 2;
          }
          const spawnX = this.shower.origin.x + offsetX;
          const spawnZ = this.shower.origin.z + offsetZ;
          const spawnY =
            this.shower.origin.y + SPAWN_Y_OFFSET;
          const spawnPos = new Vector(spawnX, spawnY, spawnZ);
          const baseDir = new Vector(0, -1, 0);
          const extraVX = (Math.random() - 0.5) * 0.03;
          const extraVZ = (Math.random() - 0.5) * 0.03;
          let initialVelocity = Vector.add(
            baseDir,
            new Vector(extraVX, 0, extraVZ)
          );
          initialVelocity = Vector.multiplyScalar(
            initialVelocity,
            INITIAL_SPEED
          );

          const meteorType =
            Math.random() < 0.5
              ? "sapphire_meteors:meteor_entity"
              : "sapphire_meteors:meteorite_entity";
          let meteorEntity;
          try {
            meteorEntity = this.shower.dimension.spawnEntity(
              meteorType,
              {
                x: spawnPos.x,
                y: spawnPos.y,
                z: spawnPos.z,
              }
            );
          } catch (e) {
            continue;
          }

          this.spawnedMeteors.push({
            entity: meteorEntity,
            pos: spawnPos,
            vel: initialVelocity,
            dimension: this.shower.dimension,
            slowFall: Math.random() < 0.3,
            ticksStatic: 0,
            prevPos: spawnPos,
          });
          this.shower.remaining--;
          this.shower.spawnCooldown =
            TICKS_PER_SPAWN;
        }
      }
      if (this.shower.spawnCooldown > 0) {
        this.shower.spawnCooldown--;
      }
      this.eventTicksRemaining--;
    }

    for (let i = this.spawnedMeteors.length - 1; i >= 0; i--) {
      let meteor = this.spawnedMeteors[i];
      const newPos = Vector.add(meteor.pos, meteor.vel);

      const dx = newPos.x - meteor.prevPos.x;
      const dy = newPos.y - meteor.prevPos.y;
      const dz = newPos.z - meteor.prevPos.z;
      const distanceMoved = Math.sqrt(dx * dx + dy * dy + dz * dz);

      if (distanceMoved < 0.01) {
        meteor.ticksStatic++;
      } else {
        meteor.ticksStatic = 0;
      }

      meteor.prevPos = newPos;
      meteor.pos = newPos;
      meteor.vel.y -=
        meteor.slowFall ? GRAVITY / 2 : GRAVITY;

      if (meteor.entity.isValid()) {
        meteor.entity.teleport({
          x: meteor.pos.x,
          y: meteor.pos.y,
          z: meteor.pos.z,
        });
      }

      try {
        if (
          !meteor.dimension
            .getBlock({
              x: meteor.pos.x,
              y: meteor.pos.y - 1,
              z: meteor.pos.z,
            })
            ?.isAir &&
          meteor.entity.isValid()
        ) {
          meteor.dimension.spawnParticle(
            "sapphire_meteors:meteor_splash",
            meteor.pos
          );
          meteor.entity.runCommand(
            "camerashake add @a[r=30] 1 3"
          );
          meteor.entity.runCommandAsync(
            `/event entity @s sapphire_meteors:despawn`
          );
          system.runTimeout(() => {
            const availableMobs = [
              "sapphire_meteors:cosmic_creeper",
              "sapphire_meteors:meteorite_golem",
            ];
            let mobCount = Math.floor(Math.random() * 3);
            if (mobCount === 1) {
              const mobType =
                availableMobs[
                  Math.floor(Math.random() * availableMobs.length)
                ];
              const offsetX =
                (Math.random() - 0.5) * 6;
              const offsetZ =
                (Math.random() - 0.5) * 6;
              meteor.dimension.spawnEntity(mobType, {
                x: meteor.pos.x + offsetX,
                y:
                  meteor
                    .dimension
                    .getTopmostBlock(
                      {
                        x: meteor.pos.x,
                        z: meteor.pos.z,
                      },
                      meteor.pos.y
                    ).location.y + 2,
                z: meteor.pos.z + offsetZ,
              });
            } else if (mobCount === 2) {
              for (let mobType of availableMobs) {
                const offsetX =
                  (Math.random() - 0.5) * 6;
                const offsetZ =
                  (Math.random() - 0.5) * 6;
                meteor.dimension.spawnEntity(mobType, {
                  x: meteor.pos.x + offsetX,
                  y:
                    meteor
                      .dimension
                      .getTopmostBlock(
                        {
                          x: meteor.pos.x,
                          z: meteor.pos.z,
                        },
                        meteor.pos.y
                      ).location.y + 2,
                  z: meteor.pos.z + offsetZ,
                });
              }
            }
            meteor.dimension.placeFeature(
              "sapphire_meteors:scatter_gravity_ore_feature",
              {
                x: meteor.pos.x,
                y: meteor.pos.y,
                z: meteor.pos.z,
              }
            );
            meteor.dimension.placeFeature(
              "sapphire_meteors:scatter_meteor_explosion_patch_feature",
              {
                x: meteor.pos.x,
                y: meteor.pos.y,
                z: meteor.pos.z,
              }
            );
          }, ENTITY_SPAWN_DELAY);
          this.spawnedMeteors.splice(i, 1);
          continue;
        }
      } catch {}

      if (meteor.ticksStatic >= STATIC_THRESHOLD_TICKS) {
        meteor.dimension.spawnParticle(
          "sapphire_meteors:meteor_splash",
          meteor.pos
        );
        if (meteor.entity.isValid()) {
          meteor.entity.runCommand(
            "camerashake add @a[r=30] 1 3"
          );
          meteor.entity.runCommandAsync(
            `/event entity @s sapphire_meteors:despawn`
          );
        }
        system.runTimeout(() => {
          meteor.dimension.placeFeature(
            "sapphire_meteors:scatter_gravity_ore_feature",
            {
              x: meteor.pos.x,
              y: meteor.pos.y,
              z: meteor.pos.z,
            }
          );
          meteor.dimension.placeFeature(
            "sapphire_meteors:scatter_meteor_explosion_patch_feature",
            {
              x: meteor.pos.x,
              y: meteor.pos.y,
              z: meteor.pos.z,
            }
          );
        }, ENTITY_SPAWN_DELAY);
        this.spawnedMeteors.splice(i, 1);
      }
    }

    if (
      this.eventTicksRemaining > 0 ||
      this.spawnedMeteors.length > 0
    ) {
      system.run(() => this.tick());
    } else {
      this.isActive = false;
      try {
        this.shower.dimension.runCommandAsync(
          `tickingarea remove ${this.globalTickAreaName}`
        );
      } catch (_) {}
    }
  }
}

function getRandomDelay() {
  return Math.floor(
    Math.random() * (MAX_DELAY - MIN_DELAY)
  ) + MIN_DELAY;
}

function scheduleMeteorEvent() {
  system.runTimeout(() => {
    const players = world.getPlayers();
    if (players.length > 0) {
      const randomPlayer =
        players[
          Math.floor(Math.random() * players.length)
        ];
      const meteorEvent = new MeteorShower();
      meteorEvent.triggerOnPlayer(randomPlayer);
    }
    system.runTimeout(scheduleMeteorEvent, COOLDOWN);
  }, getRandomDelay());
}

world.afterEvents.itemUse.subscribe((event) => {
  const item = event.itemStack;
  const player = event.source;
  if (
    item.typeId === "sapphire_meteors:meteor_summoner"
  ) {
    const meteorEvent = new MeteorShower();
    meteorEvent.triggerOnPlayer(player);
  }
});

function cleanupLeftoverMeteors() {
  const dimensions = ["overworld", "nether", "the_end"];
  dimensions.forEach((dimId) => {
    try {
      const dimension = world.getDimension(dimId);
      dimension.runCommand(
        "kill @e[type=sapphire_meteors:meteor_entity]"
      );
      dimension.runCommand(
        "kill @e[type=sapphire_meteors:meteorite_entity]"
      );
    } catch (error) {}
  });
}

cleanupLeftoverMeteors();

world.afterEvents.playerJoin.subscribe((event) => {
  for (const delay of [
    100, 120, 140, 160, 180, 200, 220, 240, 260,
  ]) {
    system.runTimeout(() => {
      cleanupLeftoverMeteors();
    }, delay);
  }
});

scheduleMeteorEvent();
