const objCache = new Map();

/**
 * An abstract class for objects that will be collected and potentially stored in non-volitile storage when they are no longer immediately needed.
 */
class TransientObject {
    /**
     * Constructor for TransientObject
     *
     * @param  {Object} opts Options for the new TransientObject
     * @param  {any} opts.id An optional globally unique identifier for the TransientObject.
     * @returns {TransientObject}      The newly created Transient object wrapped in a Proxy object. The Proxy redirects any get / set access to the `data` property of hte object.
     */
    constructor(opts = {}) {
        if (opts.id && objCache.has(opts.id)) {
            return objCache.get(opts.id);
        }

        this.id = opts.id;
        this.opts = opts;
        this.isDirty = false;
        this.isLoaded = false;
        this.data = Object.create(null);

        let obj = new Proxy(this, {
            get: function(target, prop) {
                if (prop in target) {
                    // console.log("hasOwnProperty:", prop);
                    return target[prop];
                }

                // console.log("getting data:", prop);
                if (!target.isLoaded) {
                    throw new Error(`can't get value '${prop}' before loading TransientObject`);
                }

                return target.data[prop];
            },
            set: function(target, prop, value) {
                if (prop in target) {
                    // console.log("hasOwnProperty:", prop);
                    return target[prop] = value;
                }

                if (!target.isLoaded) {
                    throw new Error(`can't set value '${prop}' before loading TransientObject`);
                }

                this.isDirty = true;
                // console.log("setting data:", prop);
                // TODO deep properties
                return target.data[prop] = value;
            },
        });

        if (this.id) {
            objCache.set(this.id, obj);
        }

        return obj;
    }

    /**
     * Abstract loader for the TransientObject. Classes that derive from this class will overload this function.
     */
    async load() {
        this.isLoaded = true;
    }

    /**
     * Abstract function for storing for the TransientObject. Classes that derive from this class will overload this function.
     */
    async store() {
        this.isDirty = false;
    }

    /**
     * Abstract function for deleting for the TransientObject. Classes that derive from this class will overload this function.
     */
    async delete() {
        if (this.isDirty) {
            await this.store();
        }

        if (this.id) {
            objCache.delete(this.id);
        }
    }

    /**
     * Converts the transient object to a string.
     *
     * @returns {string} A string representation of the TransientObject
     */
    toString() {
        return JSON.stringify(this.data);
    }

    /**
     * Synonym for toString()
     */
    toJson() {
        return this.toString();
    }

    static cache = objCache;

    /**
     * Method for deleting an object with the matching `id`
     *
     * @param  {any} id The ID to match
     * @returns {boolean}    Returns true on success, false on failure
     */
    static async deleteId(id) {
        let obj = objCache.get(id);
        if (!obj) {
            return false;
        }

        await obj.delete();

        return true;
    }
}

module.exports = {
    TransientObject,
};