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

const MamiSettings = function(storageOrPrefix, eventTarget) {
    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 createUpdateEvent = (name, value, initial, silent, local) => eventTarget.create(name, {
        name: name,
        value: value,
        initial: !!initial,
        silent: !!silent,
        local: !!local,
    });
    const dispatchUpdate = (name, value, silent, local) => eventTarget.dispatch(createUpdateEvent(name, value, false, silent, local));

    const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`);
    const broadcastUpdate = (name, value, silent) => {
        setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value, silent: !!silent }), 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, silent) => {
        if(setting.immutable)
            return;

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

    const setValue = (setting, value, silent) => {
        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;

                                if(setting.min !== undefined && value < setting.min)
                                    value = setting.min;
                                else if(setting.max !== undefined && value > setting.max)
                                    value = setting.max;
                            } 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, silent, true);
        broadcastUpdate(setting.name, value, silent);
    };

    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, ev.data.silent, false);
            return;
        }
    };

    const settingBlueprint = function(name) {
        if(typeof name !== 'string')
            throw 'setting name must be a string';

        const checkDefined = () => {
            if(settings.has(name))
                throw `setting ${name} has already been defined`;
        };

        checkDefined();

        let created = false;
        let virtual = false;
        const setting = {
            name: name,
            type: undefined,
            fallback: null,
            immutable: false,
            critical: false,
            min: undefined,
            max: undefined,
        };

        const checkCreated = () => {
            if(created)
                throw 'setting has already been created';
        };

        const pub = {
            type: value => {
                if(typeof value !== 'string' && !Array.isArray(value))
                    throw 'type must be a javascript type or array of valid string values';

                checkCreated();

                setting.type = value;
                return pub;
            },
            default: value => {
                checkCreated();
                setting.fallback = value === undefined ? null : value;

                if(setting.type === undefined)
                    setting.type = typeof setting.fallback;

                return pub;
            },
            immutable: value => {
                checkCreated();
                setting.immutable = value === undefined || value === true;
                return pub;
            },
            critical: value => {
                checkCreated();
                setting.critical = value === undefined || value === true;
                return pub;
            },
            min: value => {
                checkCreated();
                if(typeof value !== 'number')
                    throw 'value must be a number';

                setting.min = value;
                return pub;
            },
            max: value => {
                checkCreated();
                if(typeof value !== 'number')
                    throw 'value must be a number';

                setting.max = value;
                return pub;
            },
            virtual: value => {
                checkCreated();
                virtual = value === undefined || value === true;
                return pub;
            },
            create: () => {
                checkCreated();
                checkDefined();

                settings.set(name, Object.freeze(setting));

                if(virtual)
                    storage.virtualise(name);
            },
        };

        return pub;
    };

    const pub = {
        define: name => new settingBlueprint(name),
        defined: name => settings.has(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, silent) => setValue(getSetting(name), value, silent),
        delete: (name, silent) => deleteValue(getSetting(name), silent),
        toggle: (name, silent) => {
            const setting = getSetting(name);
            if(!setting.immutable)
                setValue(setting, !getValue(setting), silent);
        },
        touch: (name, silent) => {
            const setting = getSetting(name);
            dispatchUpdate(setting.name, getValue(setting), silent, true);
        },
        clear: (criticalOnly, prefix) => {
            for(const setting of settings.values())
                if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical))
                    deleteValue(setting);
        },
        watch: (name, handler, silent) => {
            const setting = getSetting(name);
            eventTarget.watch(setting.name, handler);
            handler(createUpdateEvent(setting.name, getValue(setting), true, silent, true));
        },
        unwatch: (name, handler) => {
            eventTarget.unwatch(name, handler);
        },
        virtualise: name => storage.virtualise(getSetting(name).name),
        scope: name => new MamiSettingsScoped(pub, name),
    };

    return pub;
};
