import { system, world, Dimension, Player } from "@minecraft/server";
import * as Numbers from '../utils/Numbers.js';
import { spawnRules } from "../consts/spawnRules.js";
import BiomeObserver from "./BiomeObserver.js";

const overworld = world.getDimension('overworld')

/**
 * A class made to handle the spawning of mobs in custom biomes
 */
class MobSpawner {
    /** @type {SpawnRule[]} */
    #SpawnRules = spawnRules;

    /** @type {Map<string, string[]>} */
    #populationGroups = new Map();

    constructor() {
        this.#setupPopulationGroups();
        this.#run();
    }

    #setupPopulationGroups() {
        this.#SpawnRules.forEach(rule => {
            if (!this.#populationGroups.has(rule.population)) {
                this.#populationGroups.set(rule.population, [rule.mob]);
            } else {
                if (!this.#populationGroups.get(rule.population).includes(rule.mob))
                    this.#populationGroups.get(rule.population).push(rule.mob);
            }
        });
    }

    /**
     * Picks a random player and attempts to execute spawn rules at their location
     * @returns {void}
     */
    #trySpawnMobs() {
        //console.warn(world.getTimeOfDay())
        const players = world.getAllPlayers();
        const player = players[Math.floor(Math.random() * players.length)];
        if (!player || !player.isValid() || player.location.y > 300) return;

        if (player.dimension !== overworld) return; // spawn rules only apply to the overworld

        const biomeIdentifier = BiomeObserver.getPlayerBiome(player);
        if (biomeIdentifier === undefined) return;

        const validRules = this.#SpawnRules.filter(rule => rule.biome === biomeIdentifier);
        if (!validRules || validRules.length == 0) return;
        const spawnRule = this.#selectSpawnRule(validRules);

        this.#executeSpawnRule(spawnRule, player);
    }

    /**
     * Uses a weighted random selection to pick a spawn rule
     * @param {SpawnRule[]} rules 
     */
    #selectSpawnRule(rules) {
        // converting to weighted pool format
        const weightedNumbers = rules.map(rule => ({ weight: rule.weight, value: rule }));
        const item = Numbers.getItemFromWeightedPool(weightedNumbers);
        return item[0].value;
    }

    /**
     * 
     * @param {SpawnRule} rule 
     * @param {Player} player 
     */
    #executeSpawnRule(rule, player) {
        const nearbyMobs = overworld.getEntities({ location: player.location, maxDistance: 128 });
        const populationCount = nearbyMobs.filter(mob => this.#populationGroups.get(rule.population).includes(mob.typeId)).length;
        const mobCount = nearbyMobs.filter(mob => mob.typeId === rule.mob).length;

        if (rule.timeRange !== undefined) {
            const time = world.getTimeOfDay();
            if (time < rule.timeRange.min || time > rule.timeRange.max) return;
        }

        let herdSize = 1;
        if (rule.herd !== undefined)
            herdSize = Math.floor(Math.random() * (rule.herd.max - rule.herd.min + 1)) + rule.herd.min;

        if ((populationCount + herdSize) > 10 || (mobCount + herdSize) > rule.max) return;

        const location = this.#findValidSpawnLocation(player.location);
        if (!location) return;
        if (rule.canSpawnOn !== undefined) {
            const block = overworld.getBlock({ ...location, y: location.y - 1 });
            if (!rule.canSpawnOn.includes(block.typeId)) return;
        }

        for (let i = 0; i < herdSize; i++) {
            const e = overworld.spawnEntity(rule.mob, player.location);
            e.teleport(location);
        }
    }

    #findValidSpawnLocation(location) {
        const rngAdd = Math.random() * (64 - 12) + 7;
        const sign = () => Math.random() < 0.5 ? -1 : 1;

        location = {
            x: location.x + sign() * rngAdd,
            y: location.y,
            z: location.z + sign() * rngAdd
        };
        let block = overworld.getBlock(location);
        if (!block || !block.isValid()) return undefined;

        if (block.isAir) {
            while (block.isAir) {
                location.y--;
                block = overworld.getBlock(location);
            }
            location.y++;
        } else {
            while (!block.isAir) {
                location.y++;
                block = overworld.getBlock(location);
            }
        }
        return location;
    }

    #run() {
        system.runInterval(this.#trySpawnMobs.bind(this), 5);
    }
}



export default new MobSpawner();


/**
 * @typedef {Object} SpawnRule
 * @property {string} mob - the identifier of the mob 
 * @property {BiomeIdentifier} biome - the identifier of the biome
 * @property {Herd} [herd] - the number of mobs that spawn in a group
 * @property {number} max - the max number of mobs that can spawn in an area
 * @property {number} weight - the weight of the spawn rule
 * @property {string} population - the population identifier of the mob, there is a max number of mobs spawning at one area per population
 * @property {TimeRange} [timeRange] - the range of time in which the mob can spawn
 * @property {string[]} [canSpawnOn] - the blocks the mob can spawn on
 */

/**
 * @typedef {Object} TimeRange
 * @property {number} min - 0-23999 time range in ticks
 * @property {number} max - 0-23999 time range in ticks
 */

/**
 * @typedef {Object} Herd
 * @property {number} min - the minimum number of mobs in a herd
 * @property {number} max - the maximum number of mobs in a herd
 */

/**
 * @typedef {"veldtlands"} BiomeIdentifier 
*/