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

export class DataComponent {
    static #saveQueue;

    key;
    owner;
    data;
    
    #scheduledForSave = false;
    #proxy;

    constructor(owner, key) {
        this.key = key;
        this.owner = owner;

        this.data = this.#load();
    }

    #load() {
        let raw;
        try {
            raw = this.owner.getDynamicProperty(this.key);
            if(raw == undefined) {
                return {};
            }
        } catch(err) {
            console.error(`Failed to load data component '${key}': ${err}`);
            return {};
        }
        try {
            return JSON.parse(raw);
        } catch(err) {
            console.error(`Failed to parse loaded data component '${key}': ${err}`);
            return {};
        }
    }

    #performSave() {
        this.#scheduledForSave = false;
        try {
            if(this.owner instanceof Entity && !this.owner.isValid()) return;
        } catch(err) {
            return;
        }

        let serialized;
        try {
            serialized = JSON.stringify(this.data);
        } catch(err) {
            console.error(`Failed to serialize data component '${this.key}': ${err}`);
            return;
        }
        try {
            this.owner.setDynamicProperty(this.key, serialized);
        } catch(err) {
            console.error(`Failed to save data component '${this.key}': ${err}`);
            return;
        }
    }

    save() {
        if(this.#scheduledForSave) return;
        if(DataComponent.#saveQueue == null) {
            DataComponent.#saveQueue = [];
            system.run(DataComponent.#flushQueue);
        }
        DataComponent.#saveQueue.push(this);
        this.#scheduledForSave = true;
    }

    getProxy() {
        let dataComponent = this;
        return this.#proxy ??= new Proxy(this.data, {
            get(data, property, receiver) {
                if(property == "save") return () => dataComponent.save();
                return data[property];
            }
        });
    }

    static for(owner, key) {
        if(owner instanceof Entity && !owner.isValid()) return;
        owner.scriptData ??= {};
        owner.scriptData["conduit:data_components"] ??= {};
        return (owner.scriptData["conduit:data_components"][key] ??= new DataComponent(owner, key)).getProxy();
    }

    static #flushQueue() {
        for(let component of DataComponent.#saveQueue) {
            component.#performSave();
        }
        DataComponent.#saveQueue = null;
    }
}

// Script Events

system.afterEvents.scriptEventReceive.subscribe((e) => {
    let entity = e.sourceEntity;
    if(entity == null || !entity.isValid()) return;
    let requestedKey = (e.message?.length ?? 0) > 0 ? e.message : null;
    switch(e.id) {
        case "conduit:data_components.list.entity": {
            let owner = e.sourceEntity;
            if(owner != null && owner.isValid())
                dumpInfo(owner, requestedKey);
            break;
        }
        case "conduit:data_components.list.world": {
            dumpInfo(world, requestedKey);
            break;
        }
        case "conduit:data_components.delete.entity": {
            let owner = e.sourceEntity;
            if(owner != null && owner.isValid())
                deleteProperty(owner, requestedKey);
            break;
        }
        case "conduit:data_components.delete.world": {
            deleteProperty(world, requestedKey);
            break;
        }
    }
}, {"namespaces": ["conduit"]});

function getOwnerName(owner) {
    if(owner == world) return "world";
    else if(owner instanceof Entity) return `entity ${(owner.nameTag ?? "") != "" ? owner.nameTag : owner.typeId}@${owner.id}`;
    return "unknown type";
}

function dumpInfo(owner, requestedKey) {
    world.sendMessage(`§s[!] Dynamic Properties for ${getOwnerName(owner)}${requestedKey != null ? " (§bfiltered§s)" : ""}:`);
    if(requestedKey == null) world.sendMessage(`§7  Total size: ${(owner.getDynamicPropertyTotalByteCount() / 1024).toFixed(3)} KB`);
    world.sendMessage(`§7  (Loaded Data Components are §eyellow§7)`);

    let dynamicPropertyIds = owner.getDynamicPropertyIds();
    let components = owner.scriptData != null ? owner.scriptData["conduit:data_components"] : null;

    let any = false;
    for(let key of dynamicPropertyIds) {
        if(requestedKey != null && (requestedKey.startsWith("-") ? key.includes(requestedKey.substring(1)) : !key.includes(requestedKey))) continue;
        if(components?.hasOwnProperty(key)) {
            world.sendMessage(`§7  * §e[${key}]`);
            world.sendMessage(`    ${JSON.stringify(components[key].data)}`);
        } else {
            world.sendMessage(`§7  * §8[${key}]`);
            world.sendMessage(`    §7${JSON.stringify(owner.getDynamicProperty(key))}`);
        }
        any = true;
    }
    let anyUnsaved = false;
    for(let key in components) {
        if(requestedKey != null && (requestedKey.startsWith("-") ? key.includes(requestedKey.substring(1)) : !key.includes(requestedKey))) continue;
        if(dynamicPropertyIds.includes(key)) continue;
        if(!anyUnsaved) {
            world.sendMessage(`§7  Data components not yet saved:`);
            anyUnsaved = true;
            any = true;
        }
        world.sendMessage(`§7  * §c[${key}]`);
        world.sendMessage(`    ${JSON.stringify(components[key].data)}`);
    }

    if(!any) {
        world.sendMessage(`§7    <none>`);
    }
}

function deleteProperty(owner, key) {
    if(key == null) return;
    let components = owner.scriptData != null ? owner.scriptData["conduit:data_components"] : null;
    
    let wasDataComponent = components?.hasOwnProperty(key) ?? false;
    owner.setDynamicProperty(key, undefined)
    if(wasDataComponent) {
        world.sendMessage(`§eDeleted data component '${key}' for ${getOwnerName(owner)}`);
        let data = components[key].data;
        for(let subkey in data) {
            delete data[subkey];
        }
    } else {
        world.sendMessage(`§eDeleted dynamic property '${key}' for ${getOwnerName(owner)}`);
    }
}