import { world } from "@minecraft/server";
import { debugLog, debugWarn } from "./util/debug";
const CHUNK_SIZE = 30000;
/**
 * A modular database class for managing key-value pairs using dynamic properties.
 * Optimized for Minecraft Bedrock Edition scripting with support for large values via chunking.
 */
export class OptimizedDatabase {
    //private name: string;
    name;
    pointerKey;
    cachedPointers = null;
    /**
     * Constructs an instance of OptimizedDatabase.
     * @param name - The name of the database. Must be unique, non-empty, and follow specific constraints.
     * @throws Will throw an error if the name is empty, or contains invalid characters.
     *
     * @example
     * const db = new OptimizedDatabase('myDatabase');
     */
    constructor(name) {
        if (!name || name.length === 0) {
            throw new Error("Database name cannot be empty.");
        }
        if (name.includes('"') || name.includes("/")) {
            throw new Error('Database name cannot include the characters `"` or `/`.');
        }
        this.name = name;
        this.pointerKey = `${this.name}/pointers`;
        // Initialize the pointers array if it doesn't exist.
        if (!world.getDynamicProperty(this.pointerKey)) {
            world.setDynamicProperty(this.pointerKey, JSON.stringify([]));
        }
    }
    /**
     * Retrieves the list of pointers stored in the database (cached).
     * @returns An array of strings representing the dynamic keys in the database.
     */
    _getPointers() {
        if (this.cachedPointers !== null)
            return this.cachedPointers;
        const pointers = world.getDynamicProperty(this.pointerKey);
        this.cachedPointers = pointers ? JSON.parse(pointers) : [];
        return this.cachedPointers;
    }
    /**
     * Updates the list of pointers in the database.
     * @param pointers - An array of strings representing the dynamic keys to store.
     */
    _setPointers(pointers) {
        this.cachedPointers = pointers;
        world.setDynamicProperty(this.pointerKey, JSON.stringify(pointers));
    }
    /**
     * Stores a key-value pair in the database. Automatically chunks the value if it exceeds the safe limit.
     * @param key - The key to store the value under. Must be unique within the database.
     * @param value - The value to associate with the key. Must be serializable to JSON.
     *
     * @example
     * db.set('key1', { name: 'item', value: 100 });
     */
    set(key, value) {
        const json = JSON.stringify(value);
        const dynamicKeyBase = `${this.name}/${key}`;
        const pointers = this._getPointers();
        // Remove old chunks
        this._deleteChunks(dynamicKeyBase);
        // Chunk and store
        const chunks = [];
        for (let i = 0; i < json.length; i += CHUNK_SIZE) {
            const chunk = json.slice(i, i + CHUNK_SIZE);
            const chunkKey = `${dynamicKeyBase}/${i / CHUNK_SIZE}`;
            world.setDynamicProperty(chunkKey, chunk);
            chunks.push(chunkKey);
        }
        if (!pointers.includes(dynamicKeyBase)) {
            pointers.push(dynamicKeyBase);
            this._setPointers(pointers);
        }
    }
    /**
     * Retrieves a value associated with a given key. Automatically reassembles chunked data.
     * @param key - The key to retrieve the value for.
     * @returns The value associated with the key, or `undefined` if the key does not exist.
     *
     * @example
     * const value = db.get('key1');
     * debugLog(value); // { name: 'item', value: 100 }
     */
    get(key) {
        const dynamicKeyBase = `${this.name}/${key}`;
        const chunks = [];
        for (let i = 0;; i++) {
            const chunk = world.getDynamicProperty(`${dynamicKeyBase}/${i}`);
            if (chunk === null || chunk === undefined)
                break;
            chunks.push(chunk);
        }
        if (chunks.length === 0)
            return undefined;
        const data = chunks.join("");
        return JSON.parse(data);
    }
    /**
     * Deletes a key-value pair and all its chunks from the database.
     * @param key - The key to delete from the database.
     *
     * @example
     * db.delete('key1');
     */
    delete(key) {
        const dynamicKeyBase = `${this.name}/${key}`;
        const pointers = this._getPointers();
        if (pointers.includes(dynamicKeyBase)) {
            this._deleteChunks(dynamicKeyBase);
            this._setPointers(pointers.filter((p) => p !== dynamicKeyBase));
        }
    }
    /**
     * Clears all key-value pairs from the database, including chunked data.
     *
     * @example
     * db.clear(); // Clears all entries in the database
     */
    clear() {
        const pointers = this._getPointers();
        pointers.forEach((ptr) => this._deleteChunks(ptr));
        this._setPointers([]);
    }
    /**
     * Retrieves all entries (key-value pairs) in the database.
     * @returns An array of tuples where each tuple contains a key and its associated value.
     *
     * @example
     * const entries = db.entries();
     * debugLog(entries); // [['key1', { name: 'item', value: 100 }], ['key2', { name: 'another item', value: 50 }]]
     */
    entries() {
        return this._getPointers().map((ptr) => {
            const key = ptr.split("/").pop();
            const value = this.get(key);
            return [key, value];
        });
    }
    /**
     * Cleans invalid or empty entries from the database.
     *
     * By default, it removes entries where the value is:
     * - `undefined` or `null`
     * - an empty string `""`
     * - an empty array `[]`
     * - an empty object `{}`
     * - `NaN`
     * - a `function` or `symbol`
     *
     * You can also pass a custom validator function to define your own cleanup rules.
     *
     * @param validator - (Optional) A custom validation function `(key, value) => boolean`.
     *                    Return `true` to keep the entry, or `false` to delete it.
     *
     * @example
     * // Use default cleanup behavior:
     * db.clean();
     *
     * @example
     * // Use a custom validator to only keep entries where the value is a number > 10:
     * db.clean((key, value) => typeof value === 'number' && value > 10);
     *
     * @example
     * // Custom validator that removes entries where the key starts with "temp_":
     * db.clean((key, value) => !key.startsWith("temp_"));
     */
    clean(validator) {
        const entries = this.entries();
        let deletedCount = 0;
        const defaultValidator = (value) => {
            if (value === undefined || value === null)
                return false;
            if (typeof value === "string" && value.trim() === "")
                return false;
            if (Array.isArray(value) && value.length === 0)
                return false;
            if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
                return false;
            if (typeof value === "number" && isNaN(value))
                return false;
            if (typeof value === "function" || typeof value === "symbol")
                return false;
            return true;
        };
        for (const [key, value] of entries) {
            const isValid = validator ? validator(key, value) : defaultValidator(value);
            if (!isValid) {
                this.delete(key);
                debugWarn(`[${this.name}] Deleted invalid entry "${key}" with value:`, value);
                deletedCount++;
            }
        }
        debugLog(`[${this.name}] Cleanup complete. Total deleted entries: ${deletedCount}`);
    }
    /**
     * Internal helper to delete all chunks of a given base key.
     * @param baseKey - The base dynamic key (without chunk index).
     */
    _deleteChunks(baseKey) {
        for (let i = 0;; i++) {
            const chunkKey = `${baseKey}/${i}`;
            const existing = world.getDynamicProperty(chunkKey);
            if (existing === undefined || existing === null)
                break;
            world.setDynamicProperty(chunkKey, null);
        }
    }
    /**
     * Gets the size of a single entry in bytes and MB (approximate).
     * Assumes UTF-16 (2 bytes per character).
     */
    getEntrySizeInBytes(key) {
        const dynamicKeyBase = `${this.name}/${key}`;
        let size = 0;
        for (let i = 0;; i++) {
            const chunk = world.getDynamicProperty(`${dynamicKeyBase}/${i}`);
            if (chunk === null || chunk === undefined)
                break;
            size += chunk.length * 2; // 2 bytes per UTF-16 character
        }
        return size;
    }
    /**
     * Same as getEntrySizes but returns size in MB.
     */
    getEntrySizesMB() {
        return this._getPointers().map((ptr) => {
            const key = ptr.split("/").pop();
            const sizeBytes = this.getEntrySizeInBytes(key);
            const sizeMB = sizeBytes / (1024 * 1024);
            return [key, parseFloat(sizeMB.toFixed(3))]; // round to 3 decimals
        });
    }
    /**
     * Gets total size of the database in MB.
     */
    getTotalSizeMB() {
        const sizes = this.getEntrySizesMB();
        return parseFloat(sizes.reduce((sum, [, mb]) => sum + mb, 0).toFixed(3));
    }
    /**
     * Rebuilds the pointer list by scanning existing dynamic properties.
     * This is useful if the pointer list is lost or out of sync.
     */
    rebuildPointers() {
        const basePrefix = `${this.name}/`;
        const rawKeys = world.getDynamicPropertyIds(); // Get all keys
        const pointerSet = new Set();
        for (const key of rawKeys) {
            if (typeof key !== "string")
                continue;
            if (!key.startsWith(basePrefix))
                continue;
            // Remove chunk index if present
            const parts = key.split("/");
            if (parts.length >= 3 && !isNaN(Number(parts[parts.length - 1]))) {
                parts.pop(); // Remove chunk number
            }
            const baseKey = parts.join("/");
            pointerSet.add(baseKey);
        }
        const rebuiltPointers = [...pointerSet];
        this._setPointers(rebuiltPointers);
        debugLog(`[${this.name}] Rebuilt pointers. Total entries: ${rebuiltPointers.length}`);
    }
    // Add this method to get entry keys (user-visible keys)
    getEntryKeys() {
        return this._getPointers().map((ptr) => ptr.split("/").pop());
    }
    // Alias for `get()`, clearer for data viewing
    getData(key) {
        return this.get(key);
    }
}
