import { world, system, Entity, ItemStack } from "@minecraft/server";
import { DataComponent } from "conduit/data_components.js";
import { ChunkUtil } from "conduit/chunk_util.js";
import { EventRegistry } from "conduit/events";
import conduit_data from "conduit/data.js";

const memberTagPrefix = "_er_entity__";
const uniqueIdPropertyId = "devkit:entity_region.unique_id";
let maxEntitiesLoadedPerTick = 4;
let targetDespawnTimeTicks = 200;
let activeDespawnTimeTicks = 200;

let players = [];
let overworld;
let currentTick = -1;
let countLoadedThisTick = 0;

let oldTagToRegionMap = null;
let shouldMigrateOldEntities = false;
let migrateVerbose = false;

export class EntityRegion {
    static memberTagPrefix = memberTagPrefix;

    static #initialized = false;
    static staticComponent;
    static regionList = [];
    static regionMap = {};

    id;
    dimension;
    bounds;

    states;

    memberTag;
    component;

    useActiveUnloading = false;
    optimizedChunkDistanceToPlayer;
    bypassThrottling = false;
    onLoadFunction;
    onUnloadFunction;

    chunkList;
    chunkMap; // only for active-unloading
    chunkComponentObjects;

    constructor(id) {
        this.id = id;
        this.#initializeComponent();
    }

    isLoaded() {
        return this.component.loaded;
    }

    isActive() {
        if(!this.isLoaded()) return false;
        for(let chunk of this.chunkList) {
            if(chunk.isActive()) return true;
        }
        return false;
    }

    getModCount() {
        return this.component.modCount;
    }

    hasStates() {
        return this.states != null;
    }

    getLoadedEntities(query) {
        if(query != null) {
            let combinedQuery = query != null ? {...query} : {};
            combinedQuery["tags"] ??= [];
            combinedQuery["tags"].push(this.memberTag);
            return this.dimension.getEntities(combinedQuery);
        } else {
            return this.dimension.getEntities({"tags":[this.memberTag]});
        }
    }

    getChunk(x, z) {
        if(z == null && typeof(x) == "string") {
            return this.chunkMap[x];
        }
        return this.getChunk(`(${x}, ${z})`);
    }

    getChunks() {
        return this.chunkList;
    }

    load() {
        if(this.isLoaded()) return;
        this.component.loaded = true;
        this.component.modCount = EntityRegion.claimModification();
        this.component.chunks = this.chunkComponentObjects;
        this.component.save();
        
        
        for(let chunk of this.chunkList) {
            chunk.reset();
        }
        this.#tick();
    }

    unload() {
        if(!this.isLoaded()) return;
        this.component.loaded = false;
        this.component.modCount = undefined;
        this.component.chunks = undefined;
        this.component.save();
        
        for(let chunk of this.chunkList) {
            if(this.useActiveUnloading) {
                chunk.unload();
            }
            chunk.reset();
        }
        let any = false;

        for(let entity of this.getLoadedEntities()) {
            if(this.bounds != null) {
                let entityLoc = entity.location;
                const padding = 0.5;
                if(
                    !(
                        this.bounds.origin[0]-padding <= entityLoc.x && entityLoc.x <= this.bounds.origin[0] + this.bounds.size[0]+padding
                        && this.bounds.origin[1]-padding <= entityLoc.y && entityLoc.y <= this.bounds.origin[1] + this.bounds.size[1]+padding
                        && this.bounds.origin[2]-padding <= entityLoc.z && entityLoc.z <= this.bounds.origin[2] + this.bounds.size[2]+padding
                    )
                ) {
                    // world.sendMessage("Entity " + entity.typeId + " is outside region " + region.id);
                    // world.sendMessage("Position: " + JSON.stringify(entityLoc));
                    continue;
                }
            }
            this.despawnEntity(entity);
            any = true;
        }
        // world.sendMessage("unloaded!");
    }

    notifyLoadedByRegion() {
        countLoadedThisTick++;
        if(activeDespawnTimeTicks >= 20) {
            activeDespawnTimeTicks--;
        }
    }

    notifyLoadedFromDisk(entity, memberComponent) {
        if(!this.#isEntityValid(entity, memberComponent)) {
            this.despawnEntity(entity);
            return;
        }

        if(this.hasStates()) {
            if(memberComponent.state_mod_count != this.states.getModCount()) {
                this.states.updateEntityState(entity);
            }
        }
    }

    #isEntityValid(entity, memberComponent) {
        if(!this.isLoaded()) return false; // This occurs if the region gets unloaded while this entity is not loaded.
        if(this.getModCount() != memberComponent.region_mod_count) return false; // This occurs if the region gets unloaded and then reloaded while this entity is not loaded.
        if(this.useActiveUnloading) {
            let chunkKey = memberComponent.chunk_key;
            if(chunkKey == null) return false; // [Recompilation] This occurs if the entity gets loaded before active unloading is enabled on the region.
            let chunk = this.chunkMap[memberComponent.chunk_key];
            if(chunk == null) return false; // [Recompilation] This occurs for entities in chunks that get loaded before the chunk is removed from the region.
            if(chunk.getModCount() != memberComponent.chunk_mod_count) return false; // This occurs if the region chunk gets unloaded and then reloaded while this entity is not loaded.
        }

        return true;
    }

    linkEntity(entity) {
        entity.addTag(this.memberTag);
        let memberComponent = EntityRegion.getMemberComponent(entity);
        memberComponent.region_id = this.id;
        memberComponent.region_mod_count = this.getModCount();
        memberComponent.save();

        if(this.hasStates()) {
            this.states.updateEntityState(entity);
        }
        return memberComponent;
    }

    unlinkEntity(entity) {
        if(entity.removeTag(this.memberTag)) {
            let memberComponent = EntityRegion.getMemberComponent(entity);
            for(let key in memberComponent) {
                delete memberComponent[key];
            }
            memberComponent.save();
        }
    }

    despawnEntity(entity) {
        if(this.onUnloadFunction != null) {
            entity.runCommand(`function ${this.onUnloadFunction}`);
        }
        entity.remove();
    }

    #initializeComponent() {
        this.component = DataComponent.for(world, "conduit:entity_region." + this.id);
        this.component.loaded ??= false;
        this.component.save();
    }

    #createFromInput(input) {
        this.memberTag = memberTagPrefix + this.id;
        if(shouldMigrateOldEntities) {
            oldTagToRegionMap ??= {};
            oldTagToRegionMap[memberTagPrefix + this.id.replaceAll("/",".")] = this;
        }

        this.dimension = overworld;
        if(input.hasOwnProperty("dimension")) {
            this.dimension = world.getDimension(input["dimension"]);
        }

        if(input["states"]?.length > 0) {
            this.states = new EntityRegionStates(this, input["states"]);
            this.states.reloadOnStateChange = input["reload_on_state_change"] ?? false;
            this.states.componentObj = this.component.state ??= {};
            this.states.initialize();
        }

        if(input["use_active_unloading"]) this.useActiveUnloading = true;
        if(input["bypass_throttling"]) this.bypassThrottling = true;
        this.optimizedChunkDistanceToPlayer = input["optimized_chunk_distance_to_player"];
        this.bounds = input["bounds"];

        this.onLoadFunction = input["on_load"];
        this.onUnloadFunction = input["on_unload"];

        this.chunkList = [];
        this.chunkMap = {};
        this.chunkComponentObjects = {};

        for(let chunkInput of input.chunks) {
            let chunk = new EntityRegionChunk(this, chunkInput.chunk);

            if(this.isLoaded() && this.component.chunks != null) {
                chunk.componentObj = this.component.chunks[chunk.key] ?? {};
            } else {
                chunk.componentObj = {};
            }

            this.chunkList.push(chunk);
            this.chunkMap[chunk.key] = chunk;

            chunk.definedEntities = chunkInput.entities;
            chunk.initialize();
            
            if(chunk.isLoaded())
                this.chunkComponentObjects[chunk.key] = chunk.componentObj;
        }

        if(this.isLoaded()) {
            this.component.chunks = this.chunkComponentObjects;
            this.component.save();
        }

        //sort chunks by entity count
        this.chunkList.sort((a, b)=>b.getEntityCount() - a.getEntityCount());
    }

    #tick() {
        // assuming active

        for(let chunk of this.chunkList) {
            if(!this.bypassThrottling && countLoadedThisTick > maxEntitiesLoadedPerTick) break;
            if(chunk.isActive()) chunk.tick();
        }
    }

    static get(id) {
        return this.regionMap[id];
    }

    static getMemberComponent(entity) {
        return DataComponent.for(entity, "conduit:entity_region_member");
    }

    static getRegionForEntity(entity) {
        return this.get(this.getMemberComponent(entity)?.region_id);
    }

    static load(id) {
        let region = this.get(id);
        if(region == null) {
            logError(`No such entity region '${id}'`);
            return;
        }
        region.load();
    }

    static unload(id) {
        let region = this.get(id);
        if(region == null) {
            logError(`No such entity region '${id}'`);
            return;
        }
        region.unload();
    }

    static setState(id, stateName) {
        let region = this.get(id);
        if(region == null) {
            logError(`No such entity region '${id}'`);
            return;
        }
        if(!region.hasStates()) {
            logError(`Entity region '${id}' has no states!`);
            return;
        }
        region.states.setState(stateName);
    }

    static startSystem() {
        if(EntityRegion.#initialized) return;
        this.#initialized = true;

        overworld = world.getDimension("overworld");
        this.#initializeStaticComponent();
        shouldMigrateOldEntities = conduit_data.entity_region_settings?.auto_migrate_old_entities ?? false;

        this.reloadRegionData();
    }

    static reloadRegionData() {
        this.regionList = [];
        this.regionMap = {};
        for(let key in conduit_data.entity_regions ?? "") {
            let input = conduit_data.entity_regions[key];
            let region = new EntityRegion(key);
            region.#createFromInput(input);
            this.regionList.push(region);
            this.regionMap[region.id] = region;
        }

        // sort regions to keep bypassThrottling regions first
        this.regionList.sort((a, b)=>b.bypassThrottling - a.bypassThrottling);

        maxEntitiesLoadedPerTick = conduit_data.entity_region_settings?.throttle_spawn_rate ?? 4;
        activeDespawnTimeTicks = targetDespawnTimeTicks = 20*(conduit_data.entity_region_settings?.active_unloading_timer ?? 10);
    }

    static tickSystem() {
        players = world.getAllPlayers();
        for(let player of players) {
            player._er_chunk = EntityRegionUtil.getChunkObjForLocation(player.location);
        }

        currentTick = system.currentTick;

        // if(countLoadedThisTick > 0) {
        //     world.sendMessage(`loaded ${countLoadedThisTick} entities last tick`);
        // }
        countLoadedThisTick = 0;
        if(activeDespawnTimeTicks < targetDespawnTimeTicks) {
            activeDespawnTimeTicks++;
        }

        for(let region of this.regionList) {
            // this is a break; statement and not a continue, because regionList is sorted to put all the bypassThrottling regions first.
            if(!region.bypassThrottling && countLoadedThisTick > maxEntitiesLoadedPerTick) break;
            if(region.isActive()) {
                region.#tick();
            }
        }
    }

    static getUniqueIdForEntity(entity) {
        return entity.getDynamicProperty(uniqueIdPropertyId);
    }

    static #initializeStaticComponent() {
        this.staticComponent = DataComponent.for(world, "conduit:entity_regions");
        this.staticComponent.modCount ??= 0;

        this.staticComponent.save();
    }

    static claimModification() {
        this.staticComponent.modCount++;
        this.staticComponent.save();
        return this.staticComponent.modCount;
    }
}

export class EntityRegionChunk {
    region;
    key;
    cx;
    cz;
    location;
    componentObj;

    definedEntities;
    loadedEntities;
    lastLoadedTick;

    constructor(region, chunkObj) {
        this.region = region;
        this.key = chunkObj.key;
        this.cx = chunkObj.x;
        this.cz = chunkObj.z;
        this.location = {
            x: this.cx*16,
            y: 0,
            z: this.cz*16
        };
    }

    isLoaded() {
        return this.componentObj.loaded ?? false;
    }

    isActive() {
        return this.region.isLoaded() && (this.region.useActiveUnloading || !this.isLoaded());
    }

    getModCount() {
        return this.componentObj.modCount;
    }

    isSimulated() {
        return ChunkUtil.isLocationSimulated(this.region.dimension, this.location);
    }

    getEntityCount() {
        return this.definedEntities.length;
    }

    initialize() {
        this.loadedEntities = this.componentObj.loaded_entities;
    }

    tick() {
        // assuming active

        let shouldBeLoaded;
        if(this.region.useActiveUnloading || this.region.optimizedChunkDistanceToPlayer >= 0) {
            // world.sendMessage(`coords: ${this.cx}, ${this.cz}`);
            shouldBeLoaded = players.length == 0
                ? this.isLoaded() //If no one is online, keep whatever loaded state this chunk is in
                : sqrDistanceToNearestPlayer(this.cx, this.cz) <= Math.pow(this.region.optimizedChunkDistanceToPlayer ?? 4, 2)
                    && this.isSimulated()
                ;
        } else {
            shouldBeLoaded = this.isSimulated();
        }

        if(this.region.useActiveUnloading) {
            if(shouldBeLoaded) {
                this.lastLoadedTick = currentTick;
            } else if(this.lastLoadedTick != null && currentTick < this.lastLoadedTick + activeDespawnTimeTicks) {
                shouldBeLoaded = true;
            }
        }

        if(shouldBeLoaded != this.isLoaded()) {
            if(shouldBeLoaded) {
                if(this.attemptLoad()) {
                    this.markLoaded();
                }
            } else if(this.region.useActiveUnloading) {
                this.unload();
            }
        }
    }

    attemptLoad() {
        // world.sendMessage(`Attempt load chunk ${this.key}`);
        if(this.region.useActiveUnloading) {
            this.componentObj.modCount ??= EntityRegion.claimModification();
            this.loadedEntities ??= [];
        } else {
            this.loadedEntities = undefined;
        }
        var successCount = 0;
        var failureCount = 0;
        for(let entityData of this.definedEntities) {
            let entity;
            try {
                entity = EntityRegionSpawner.spawnEntity(entityData, this.region);
                
                successCount++;
                if(entity != null) {
                    this.region.notifyLoadedByRegion(entity);
                }
            } catch(ex) {
                // logError(ex);
                failureCount++;
                continue;
            }
            
            if(entity != null) {
                this.linkEntity(entity);
            
                if(this.region.onLoadFunction != null) {
                    entity.runCommand(`function ${this.region.onLoadFunction}`);
                }
            }
        }

        // if(failureCount > 0) {
            // world.sendMessage(`Chunk failed to load: only [${successCount}/${successCount + failureCount}]`);
        // }
        return successCount > 0 || failureCount == 0;
    }

    unload() {
        if(!this.region.useActiveUnloading) throw `Cannot unload an individual chunk of region ${this.region.id}: The region's use_active_unloading must be set to true!`;

        if(this.loadedEntities == null) {
            this.loadedEntities = this.componentObj.loaded_entities;
        }

        for(let entry of this.loadedEntities ?? "") {
            if(entry instanceof Entity) {
                if(entry.isValid()) this.region.despawnEntity(entry);
            } else {
                if(typeof(entry) != "string") {
                    logWarn("Non-entity and non-id in region chunk's loaded entities: " + entry);
                    continue;
                }
                // this is an ID, get the entity
                let entity = world.getEntity(entry);
                if(entity != null) this.region.despawnEntity(entity);
            }
        }
        this.reset();
    }

    linkEntity(entity, resave) {
        resave ??= false;
        // assume loaded
        if(entity == null) return;
        let memberComponent = this.region.linkEntity(entity);
        if(this.region.useActiveUnloading) {
            memberComponent.chunk_key = this.key;
            memberComponent.chunk_mod_count = this.getModCount();
        } else {
            memberComponent.chunk_key = undefined;
            memberComponent.chunk_mod_count = undefined;
        }
        
        if(this.region.useActiveUnloading) {
            this.loadedEntities ??= [];
            this.loadedEntities.push(entity);
            if(resave)
                this.componentObj.loaded_entities = this.loadedEntities.map(e => e instanceof Entity ? e.id : e);
        }
        memberComponent.save();
    }
    
    markLoaded() {
        this.componentObj.loaded = true;
        if(this.region.useActiveUnloading) {
            this.componentObj.modCount ??= EntityRegion.claimModification();
            this.loadedEntities ??= [];
        }
        if(this.loadedEntities != null) {
            this.componentObj.loaded_entities = this.loadedEntities.map(e => e instanceof Entity ? e.id : e);
        }
        this.region.chunkComponentObjects[this.key] = this.componentObj;
        this.region.component.save();
    }

    reset() {
        this.componentObj.loaded = undefined;

        this.loadedEntities = undefined;
        this.componentObj.loaded_entities = undefined;

        this.componentObj.modCount = undefined;

        delete this.region.chunkComponentObjects[this.key];
        this.region.component.save();
    }
}

class EntityRegionStates {
    region;
    stateList;

    defaultState;

    componentObj;
    entityStateData;
    reloadOnStateChange;

    constructor(region, stateList) {
        this.region = region;
        this.stateList = stateList;
        this.defaultState = this.stateList[0];
    }

    initialize() {
        this.componentObj.modCount ??= EntityRegion.claimModification();
        this.componentObj.current_state ??= this.getDefaultState();
        this.region.component.save();

        this.entityStateData = {};
        for(let entry of EntityRegionDataInterface.getAllEntityDataForRegion(this.region.id)) {
            if(entry.states == null) continue;
            this.entityStateData[""+entry.unique_id] = entry.states;
        }
    }

    getDefaultState() {
        return this.defaultState;
    }

    getCurrentState() {
        return this.componentObj.current_state ?? this.getDefaultState();
    }

    getModCount() {
        return this.componentObj.modCount;
    }

    setState(stateName) {
        if(!this.stateList.includes(stateName)) {
            logError(`Cannot set state '${stateName}' for entity region ${this.region.id}: No such state.`);
            return;
        }

        this.componentObj.current_state = stateName;
        this.componentObj.modCount = EntityRegion.claimModification();
        this.region.component.save();
        
        if(this.region.isLoaded()) {
            if(this.reloadOnStateChange) {
                this.region.unload();
                this.region.load();
            } else {
                for(let entity of this.region.getLoadedEntities()) {
                    this.updateEntityState(entity);
                }
            }
        }
    }
    
    updateEntityState(entity) {
        let currentState = this.getCurrentState();
        let memberComponent = EntityRegion.getMemberComponent(entity);
        memberComponent.state = currentState;
        memberComponent.state_mod_count = this.getModCount();
        memberComponent.save();

        let uniqueId = entity.getDynamicProperty(uniqueIdPropertyId);
        let entityStateData = this.entityStateData[""+uniqueId];
        if(entityStateData != null) entityStateData = entityStateData[currentState];
        
        if(entityStateData != null) {
            EntityRegionSpawner.deserializeEntity(entity, entityStateData, this.region, false, true);
        }
    }
}

class EntityRegionSpawner {
    static spawnEntity(entityData, region) {
        EventRegistry.invoke("entity_regions.before_entity_deserialized", null, {
            "region": region,
            "entityData": entityData
        });

        let dimension = region.dimension;
        let id = entityData.identifier;
        let location = EntityRegionUtil.arrayToLocation(entityData.position);

        let ignored = entityData.ignored ?? false;
        if(region.hasStates() && entityData.states != null) {
            let currentStateData = entityData.states[region.states.getCurrentState()];
            if(currentStateData != null) {
                ignored = currentStateData.ignored ?? ignored;
                id = currentStateData.identifier ?? id;
            }
        }
        if(ignored) return;
    
        let entity;
        if(id == "minecraft:item") {
            let itemStack = this.newItemStack(entityData.item) ?? new ItemStack("minecraft:stone");
            entity = dimension.spawnItem(itemStack, location);
        } else {
            //gotta summon that entity via a command for rotation to work
            let summonCommand = `summon ${id} ${location.x.toFixed(8)} ${location.y.toFixed(8)} ${location.z.toFixed(8)} ${entityData.yaw ?? 0} 0 ${JSON.stringify(entityData.events?.length > 0 ? entityData.events[0] : "minecraft:entity_spawned")}`;
            try {
                dimension.runCommand(summonCommand);
            } catch(err) {
                // logError(`Error summoning entity in region ${region.id}.\n` + err);
                // assume there was an error with the entity type identifier
                return;
            }
            let candidates;
            for(let checkDistance = 1; entity == null && checkDistance <= 16; checkDistance *= 2) {
                candidates = dimension.getEntities({
                    "type": id,
                    "location": location,
                    "maxDistance": checkDistance
                });
                for(let candidate of candidates) {
                    if(candidate.typeId == id && candidate.getTags().length == 0) {
                        entity = candidate;
                        break;
                    }
                }
            }
    
            if(entity == null) {
                //overworld.runCommand(`scriptevent conduit:log ${JSON.stringify({"message":`[EntityRegions] Failed to find summoned entity ${id} in region ${region.id}! ${candidates.length} candidates`})}`);
                throw `Failed to find summoned entity ${id} in region ${region.id}! ${candidates.length} candidates`;
            }
        }
    
        try {
            this.deserializeEntity(entity, entityData, region, true, false);
        } catch(err) {
            logError("Error deserializing entity in region " + region.id + ".\n" + err);
        }
    
        return entity;
    }

    static deserializeEntity(entity, entityData, region, skipFirstEvent, updatePosition) {
        if(entityData.unique_id != null) {
            entity.setDynamicProperty(uniqueIdPropertyId, entityData.unique_id);
        }
    
        if(updatePosition ?? false) {
            if(entityData.position != null) {
                entity.teleport(EntityRegionUtil.arrayToLocation(entityData.location));
            }
            if(entityData.yaw != null || entityData.pitch != null) {
                let rotation = entity.getRotation();
                if(entityData.yaw != null) rotation.y = entityData.yaw;
                if(entityData.pitch != null) rotation.y = entityData.pitch;
                entity.setRotation(rotation);
            }
        }
    
        if(entityData.events != null) {
            for(let event of entityData.events) {
                if(skipFirstEvent ?? false) {
                    skipFirstEvent = false;
                    continue;
                }
                try {
                    entity.triggerEvent(event);
                } catch(ignore) {}
            }
        }
        if(entityData.name != null) {
            entity.nameTag = entityData.name;
        }
        if(entityData.tags != null) {
            for(let tag of entityData.tags) {
                if(tag.startsWith("!")) {
                    tag = tag.substring(1);
                    entity.removeTag(tag);
                } else {
                    entity.addTag(tag);
                }
            }
        }
        if(entityData.scores != null) {
            for(let key in entityData.scores) {
                let objective = world.scoreboard.getObjective(key);
                if(objective != null) {
                    objective.setScore(entity, entityData.scores[key])
                }
            }
        }
        if(entityData.properties != null) {
            for(let key in entityData.properties) {
                try {
                    entity.setProperty(key, entityData.properties[key]);
                } catch(ignore) {}
            }
        }
        if(entityData.dynamic_properties != null) {
            for(let key in entityData.dynamic_properties) {
                try {
                    entity.setDynamicProperty(key, entityData.dynamic_properties[key]);
                } catch(ignore) {}
            }
        }
        if(entityData.mark_variant != null) {
            let markVariantComponent = entity.getComponent("minecraft:mark_variant");
            if(markVariantComponent != null)
                markVariantComponent.value = entityData.mark_variant;
        }
        if(entityData.skin_id != null) {
            let skinIdComponent = entity.getComponent("minecraft:skin_id");
            if(skinIdComponent != null)
                skinIdComponent.value = entityData.skin_id;
        }
        if(entityData.active_effects != null) {
            for(let entry of entityData.active_effects) {
                entity.addEffect(entry.id, Math.min(entry.duration, 20000000), {
                    amplifier: entry.amplifier ?? 0,
                    showParticles: entry.show_particles ?? true
                });
            }
        }
        if(entityData.mainhand != null) {
            this.handleEquipment(entity, "Mainhand", entityData.mainhand);
        }
        if(entityData.offhand != null) {
            this.handleEquipment(entity, "Offhand", entityData.offhand);
        }
        if(entityData["armor.head"] != null) {
            this.handleEquipment(entity, "Head", entityData["armor.head"]);
        }
        if(entityData["armor.chest"] != null) {
            this.handleEquipment(entity, "Chest", entityData["armor.chest"]);
        }
        if(entityData["armor.legs"] != null) {
            this.handleEquipment(entity, "Legs", entityData["armor.legs"]);
        }
        if(entityData["armor.feet"] != null) {
            this.handleEquipment(entity, "Feet", entityData["armor.feet"]);
        }
        if(entityData.items != null) {
            let inventory = entity.getComponent("minecraft:inventory");
            if(inventory != null) {
                let container = inventory.container;
                for(let itemData of entityData.items) {
                    let slotIndex = itemData.slot;
                    let itemStack = this.newItemStack(itemData);
                    container.setItem(slotIndex, itemStack);
                }
            }
        }
    
        for(let command of entityData.on_load ?? "") {
            entity.runCommand(command);
        }
        for(let command of entityData.on_load_async ?? "") {
            entity.runCommandAsync(command);
        }
    
        EventRegistry.invoke("entity_regions.entity_deserialized", entity, {
            "region": region,
            "entityData": entityData
        });
    }
    
    static newItemStack(data) {
        if(data == null) 
            return null;

        let id = data.identifier;
        if(id == null || id == "") return null;
        let amount = data.amount ?? 1;
        let stack = new ItemStack(id, amount);
        if(data.name_tag != null)
            stack.nameTag = data.name_tag;
        if(data.lore != null)
            stack.setLore(data.lore);
        if(data.can_destroy != null)
            stack.setCanDestroy(data.can_destroy);
        if(data.can_place_on != null)
            stack.setCanPlaceOn(data.can_place_on);
        if(data.keep_on_death != null)
            stack.keepOnDeath = data.keep_on_death;
        if(data.item_lock != null) {
            stack.lockMode = data.item_lock;
        }
        return stack;
    }
    static handleEquipment(entity, slot, data) {
        let equippable = entity.getComponent("minecraft:equippable");
        if(equippable != null) {
            //yay this works
            equippable.setEquipment(slot, this.newItemStack(data));
        } else {
            //boo, having to use runCommand
            slot = slot.toLowerCase();
            if(slot.endsWith("hand")) {
                slot = "slot.weapon." + slot;
            } else {
                slot = "slot.armor." + slot;
            }
            let identifier = data.identifier || "air";
            if(identifier.startsWith("minecraft:")) {
                identifier = identifier.substring("minecraft:".length);
            }
            let command = `replaceitem entity @s ${slot} 0 ${identifier} ${data.amount ?? 1} ${data.damage ?? 0}`;
            entity.runCommand(command);
        }
    }
}

system.run(()=>EntityRegion.startSystem());
system.runInterval(()=>EntityRegion.tickSystem());


export class EntityRegionDataInterface {

    // Getters
    static getRegionData(regionId) {
        return conduit_data.entity_regions[regionId];
    }
    static getAllEntityDataForRegion(regionId) {
        let regionData = this.getRegionData(regionId);
        return regionData.chunks.reduce((list, chunk) => {
            if(chunk.entities != null)
                list.push(...chunk.entities);
            return list;
        }, []);
    }
    static getEntityDataForEntity(entity, regionId) {
        let regionData = this.getRegionData(regionId);
        let uniqueId = entity.getDynamicProperty(uniqueIdPropertyId);
        if(uniqueId == null) return null;
        for(let chunk of regionData.chunks) {
            for(let entityData of chunk.entities) {
                if(entityData.unique_id == uniqueId) return entityData;
            }
        }
        return null;
    }

    // Procedures
    static reloadRegionData() {
        EntityRegion.reloadRegionData();
    }
}

export class EntityRegionUtil {
    static arrayToLocation(arr) {
        return {
            x: arr[0],
            y: arr[1],
            z: arr[2]
        };
    }
    static getChunkObjForLocation(loc) {
        var x = Math.floor(loc.x / 16);
        var z = Math.floor(loc.z / 16);
        return this.createChunkObj(x, z);
    }
    static createChunkObj(cx, cz) {
        return {
            x: cx,
            z: cz,
            key: `(${cx}, ${cz})`
        };
    }
}

system.afterEvents.scriptEventReceive.subscribe((e) => {
    if(e.id == "conduit:entity_region.load") {
        EntityRegion.load(e.message);
    } else if(e.id == "conduit:entity_region.unload") {
        EntityRegion.unload(e.message);
    } else if(e.id == "conduit:entity_region.set_state") {
        let args = JSON.parse(e.message);
        EntityRegion.setState(args.region, args.state);
    } else if(e.id == "conduit:entity_region.dump_info") {
        dumpInfo(e.sourceEntity, e.message);
    } else if(e.id == "conduit:entity_region.migrate_old_entities") {
        shouldMigrateOldEntities = true;
        migrateVerbose = true;
        world.sendMessage("Now migrating existing entities to the V2 entity region format");
        oldTagToRegionMap ??= {};
        EntityRegionDataInterface.reloadRegionData();
        for(let entity of overworld.getEntities({})) {
            if(EntityRegion.getRegionForEntity(entity) == null)
                migrateOldEntity(entity);
        }
    }
}, {"namespaces": ["conduit"]});

world.afterEvents.entityLoad.subscribe((e) => {
    let entity = e.entity;
    if(!entity.isValid()) return;
    let memberComponent = EntityRegion.getMemberComponent(entity);
    if(oldTagToRegionMap != null) {
        if(migrateOldEntity(entity)) {
            if(!entity.isValid()) return;
        }
    }

    if(memberComponent.region_id != null) {
        let region = EntityRegion.get(memberComponent.region_id);
        if(region != null) {
            //entity belongs to a region
            region.notifyLoadedFromDisk(entity, memberComponent)
        } else {
            //from invalid region, likely deleted
            entity.remove();
        }
    }
});

function migrateOldEntity(entity) {
    let any = false;
    for(let tag of entity.getTags()) {
        if(oldTagToRegionMap.hasOwnProperty(tag)) {
            // this is an entity loaded in V1, replace tag
            entity.removeTag(tag);
            let region = oldTagToRegionMap[tag];
            if(migrateVerbose)
                world.sendMessage(`§nEntity updated to new region format: §6${entity.typeId}§n; region: §6${region.id}`);
            if(region.isLoaded()) {
                region.linkEntity(entity);
            } else {
                entity.remove();
            }
            any = true;
        }
    }
    return any;
}

function dumpInfo(entity, message) {
    let region;
    if(message != null && message.length > 0) {
        message = message.trim();
        region = EntityRegion.get(message);
        if(region == null) {
            world.sendMessage(`§cNo such entity region '${message}'`);
        }
    } else if(entity != null && entity.isValid()) {
        world.sendMessage(`§s[!] Entity info (${entity.name || entity.typeId}@${entity.id}):`);
        let regionForEntity = EntityRegion.getRegionForEntity(entity);
        region ??= regionForEntity;
        if(regionForEntity != null) {
            let memberComponent = EntityRegion.getMemberComponent(entity);
            for(let key in memberComponent) {
                world.sendMessage(`§7    ${key}: §f${memberComponent[key]}`);
            }
        } else {
            world.sendMessage(`§7    Not part of a region`);
        }
        let uniqueId = entity.getDynamicProperty(uniqueIdPropertyId);
        if(uniqueId != null) {
            world.sendMessage(`§7    Unique ID: §f${uniqueId}`);
        }
    }

    if(region != null) {
        world.sendMessage(`§q[!] Region info (${region.id}):`);
        world.sendMessage(`§7    Loaded: ${region.isLoaded() ? "§atrue" : "§cfalse"}`);
        world.sendMessage(`§7    modCount: §f${region.getModCount()}`);
        world.sendMessage(`§7    §f${region.getChunks().reduce((count, chunk) => count + chunk.getEntityCount(), 0)}§7 entities across §f${region.getChunks().length}§7 chunks`);
        if(region.hasStates()) {
            world.sendMessage(`§7    States: [§f${region.states.stateList.map(n => n == region.states.getCurrentState() ? `§a${n}§r` : n).join(", ")}§7]`);
            world.sendMessage(`§7        modCount: §f${region.states.getModCount()}`);
        }
    }
}

function logWarn(err) {
    console.warn("§6[Entity Regions] " + err);
}

function logError(err) {
    console.error("§c[Entity Regions] " + err);
}

function sqrDistanceToNearestPlayer(cx, cz) {
    if(players.length == 0) return Infinity;
    let minSqrDistance = null;

    for(let player of players) {
        // world.sendMessage(`player chunk: ${player._er_chunk.x}, ${player._er_chunk.z}`);
        let sqrDistanceToPlayer = (player._er_chunk.x-cx)*(player._er_chunk.x-cx) + (player._er_chunk.z-cz)*(player._er_chunk.z-cz);
        if(sqrDistanceToPlayer == 0) return 0;
        
        if(minSqrDistance == null || sqrDistanceToPlayer < minSqrDistance) minSqrDistance = sqrDistanceToPlayer;
    }

    return minSqrDistance;
}