#include eventtarget.js
#include settings/scoped.js
#include settings/virtual.js
#include settings/webstorage.js

const MamiSettings = function(storageOrPrefix) {
    if(typeof storageOrPrefix === 'string')
        storageOrPrefix = new MamiSettingsWebStorage(window.localStorage, storageOrPrefix);
    else if(typeof storageOrPrefix !== 'object')
        throw 'storageOrPrefix must be a prefix string or an object';

    if(typeof storageOrPrefix.get !== 'function'
        || typeof storageOrPrefix.set !== 'function'
        || typeof storageOrPrefix.delete !== 'function')
        throw 'required methods do not exist in storageOrPrefix object';

    const storage = new MamiSettingsVirtualStorage(storageOrPrefix);
    const settings = new Map;

    const eventTarget = new MamiEventTarget('mami:setting');
    const createUpdateEvent = (name, value, initial) => eventTarget.create(name, {
        name: name,
        value: value,
        initial: !!initial,
    });
    const dispatchUpdate = (name, value) => eventTarget.dispatchEvent(createUpdateEvent(name, value));

    const broadcast = new BroadcastChannel(`${MAMI_JS}:settings:${storage.name()}`);
    const broadcastUpdate = (name, value) => {
        setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value }), 0);
    };

    const getSetting = name => {
        const setting = settings.get(name);
        if(setting === undefined)
            throw `setting ${name} is undefined`;
        return setting;
    };

    const getValue = setting => {
        if(setting.immutable)
            return setting.fallback;

        const value = storage.get(setting.name);
        return value === null ? setting.fallback : value;
    };

    const deleteValue = setting => {
        if(setting.immutable)
            return;

        storage.delete(setting.name);
        dispatchUpdate(setting.name, setting.fallback);
        broadcastUpdate(setting.name, setting.fallback);
    };

    const setValue = (setting, value) => {
        if(value !== null) {
            if(value === undefined)
                value = null;
            else if('type' in setting) {
                if(Array.isArray(setting.type)) {
                    if(!setting.type.includes(value))
                        throw `setting ${setting.name} must match an enum value`;
                } else {
                    const type = typeof value;
                    let resolved = false;

                    if(type !== setting.type) {
                        if(type === 'string') {
                            if(setting.type === 'number') {
                                value = parseFloat(value);
                                resolved = true;
                            } else if(setting.type === 'boolean') {
                                value = !!value;
                                resolved = true;
                            }
                        } else if(setting.type === 'string') {
                            value = value.toString();
                            resolved = true;
                        }
                    } else resolved = true;

                    if(!resolved)
                        throw `setting ${setting.name} must be of type ${setting.type}`;
                }
            }
        }

        if(setting.immutable)
            return;

        if(value === null || value === setting.fallback) {
            value = setting.fallback;
            storage.delete(setting.name);
        } else
            storage.set(setting.name, value);

        dispatchUpdate(setting.name, value);
        broadcastUpdate(setting.name, value);
    };

    broadcast.onmessage = ev => {
        if(typeof ev.data !== 'object' || typeof ev.data.act !== 'string')
            return;

        if(ev.data.act === 'update' && typeof ev.data.name === 'string') {
            dispatchUpdate(ev.data.name, ev.data.value);
            return;
        }
    };

    const pub = {
        define: (name, type, fallback, immutable, critical, virtual) => {
            if(typeof name !== 'string')
                throw 'setting name must be a string';
            if(typeof type !== 'string' && !Array.isArray(type))
                throw 'type must be a javascript type or array of valid string values.';
            if(settings.has(name))
                throw `setting ${name} has already been defined`;

            settings.set(name, Object.freeze({
                name: name,
                type: type === null ? undefined : type,
                fallback: fallback === undefined ? null : fallback,
                immutable: immutable === true,
                critical: critical === true,
            }));

            if(virtual === true)
                storage.virtualise(name);
        },
        info: name => getSetting(name),
        names: () => Array.from(settings.keys()),
        has: name => {
            const setting = settings.get(name);
            return setting !== undefined
                && !setting.immutable
                && storage.get(setting.name) !== null;
        },
        get: name => getValue(getSetting(name)),
        set: (name, value) => setValue(getSetting(name), value),
        delete: name => deleteValue(getSetting(name)),
        toggle: name => {
            const setting = getSetting(name);
            if(!setting.immutable)
                setValue(setting, !getValue(setting));
        },
        touch: name => {
            const setting = getSetting(name);
            dispatchUpdate(setting.name, getValue(setting));
        },
        clear: (criticalOnly, prefix) => {
            for(const setting of settings.values())
                if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical))
                    deleteValue(setting);
        },
        watch: (name, handler) => {
            const setting = getSetting(name);
            eventTarget.watch(setting.name, handler);
            handler(createUpdateEvent(setting.name, getValue(setting), true));
        },
        unwatch: (name, handler) => {
            eventTarget.unwatch(name, handler);
        },
        virtualise: name => storage.virtualise(getSetting(name).name),
        scope: name => new MamiSettingsScoped(pub, name),
    };

    return pub;
};
