import {
    pub,
    sub,
    SETTINGS_CHANGED,
    KEYBOARD_KEY_PRESSED,
    KEYBOARD_TOGGLE_FILTER_MODE
} from 'event';
import {gui} from 'gui';
import {log} from 'log';

/**
 * Stores app wide option names.
 *
 * Use the following format:
 * UPPERCASE_NAME: 'uppercase.name'
 *
 * @version 1
 */
export const opts = {
    _VERSION: '_version',
    LOG_LEVEL: 'log.level',
    INPUT_KEYBOARD_MAP: 'input.keyboard.map',
    MIRROR_SCREEN: 'mirror.screen',
    VOLUME: 'volume',
    FORCE_FULLSCREEN: 'force.fullscreen',
}


// internal structure version
const revision = 1.6;

// default settings
// keep them for revert to defaults option
const _defaults = Object.create(null);
_defaults[opts._VERSION] = revision;

/**
 * The main store with settings passed around by reference
 * (because of that we need a wrapper object)
 * don't do this at work (it's faster to write than immutable code).
 *
 * @type {{settings: {_version: number}}}
 */
let store = {
    settings: {
        ..._defaults
    }
};
let provider;

/**
 * Enum for settings types (the explicit type of a key-value pair).
 *
 * @readonly
 * @enum {number}
 */
const option = Object.freeze({undefined: 0, string: 1, number: 2, object: 3, list: 4});

const exportFileName = `cloud-game.settings.v${revision}.txt`;

const getStore = () => store.settings;

/**
 * The NullObject provider if everything else fails.
 */
const voidProvider = (store_ = {settings: {}}) => {
    const nil = () => ({})

    return {
        get: key => store_.settings[key],
        set: nil,
        remove: nil,
        save: nil,
        loadSettings: nil,
        reset: nil,
    }
}

/**
 * The LocalStorage backend for our settings (store).
 *
 * For simplicity it will rewrite all the settings on every store change.
 * If you want to roll your own, then use its "interface".
 */
const localStorageProvider = ((store_ = {settings: {}}) => {
    if (!_isSupported()) return;

    const root = 'settings';

    const _serialize = data => JSON.stringify(data, null, 2);

    const save = () => localStorage.setItem(root, _serialize(store_.settings));

    function _isSupported() {
        const testKey = '_test_42';
        try {
            // check if it's writable and isn't full
            localStorage.setItem(testKey, testKey);
            localStorage.removeItem(testKey);
            return true;
        } catch (e) {
            log.error(e);
            return false;
        }
    }

    const get = key => JSON.parse(localStorage.getItem(key));

    const set = () => save();

    const remove = () => save();

    const loadSettings = () => {
        if (!localStorage.getItem(root)) save();
        store_.settings = JSON.parse(localStorage.getItem(root));
    }

    const reset = () => {
        localStorage.removeItem(root);
        localStorage.setItem(root, _serialize(store_.settings));
    }

    return {
        get,
        clear: () => localStorage.removeItem(root),
        set,
        remove,
        save,
        loadSettings,
        reset,
    }
});

/**
 * Nuke existing settings with provided data.
 * @param text The text to extract data from.
 * @private
 */
const _import = text => {
    try {
        for (const property of Object.getOwnPropertyNames(store.settings)) delete store.settings[property];
        Object.assign(store.settings, JSON.parse(text).settings);
        provider.save();
        pub(SETTINGS_CHANGED);
    } catch (e) {
        log.error(`Your import file is broken!`);
    }

    _render();
}

const _export = () => {
    let el = document.createElement('a');
    el.setAttribute(
        'href',
        `data:text/plain;charset=utf-8,${encodeURIComponent(JSON.stringify(store, null, 2))}`
    );
    el.setAttribute('download', exportFileName);
    el.style.display = 'none';
    document.body.appendChild(el);
    el.click();
    document.body.removeChild(el);
}

const init = () => {
    // try to load settings from the localStorage with fallback to null-object
    provider = localStorageProvider(store) || voidProvider(store);
    provider.loadSettings();

    const lastRev = (store.settings || {_version: 0})._version

    if (revision > lastRev) {
        log.warn(`Your settings are in older format (v${lastRev}) and will be reset to (v${revision})!`);
        _reset();
    }
}

const get = () => store.settings;

const _isLoaded = key => store.settings.hasOwnProperty(key);

/**
 * Tries to load settings by some key.
 *
 * @param key A key to find values with.
 * @param default_ The default values to set if none exist.
 * @returns A slice of the settings with the given key or a copy of the value.
 */
const loadOr = (key, default_) => {
    // preserve defaults
    _defaults[key] = default_;

    if (!_isLoaded(key)) {
        store.settings[key] = {};
        set(key, default_);
    } else {
        // !to check if settings do have new properties from default & update
        // or it have ones that defaults doesn't
    }

    return store.settings[key];
}

const set = (key, value, updateProvider = true) => {
    const type = _getType(value);

    // mutate settings w/o changing the reference
    switch (type) {
        case option.list:
            store.settings[key].splice(0, Infinity, ...value);
            break;
        case option.object:
            for (let option of Object.keys(value)) {
                log.debug(`Change key [${option}] from ${store.settings[key][option]} to ${value[option]}`);
                store.settings[key][option] = value[option];
            }
            break;
        case option.string:
        case option.number:
        case option.undefined:
        default:
            store.settings[key] = value;
    }

    if (updateProvider) {
        provider.set(key, value);
        pub(SETTINGS_CHANGED);
    }
}

const changed = (key, obj, key2) => {
    if (!store.settings.hasOwnProperty(key)) return
    const newValue = store.settings[key]
    const changed = newValue !== obj[key2]
    changed && (obj[key2] = newValue)
    return changed
}

const _reset = () => {
    for (let _option of Object.keys(_defaults)) {
        const value = _defaults[_option];

        // delete all sub-options not in defaults
        if (_getType(value) === option.object) {
            for (let opt of Object.keys(store.settings[_option])) {
                const prev = store.settings[_option][opt];
                const isDeleted = delete store.settings[_option][opt];
                log.debug(`User option [${opt}=${prev}] has been deleted (${isDeleted}) from the [${_option}]`);
            }
        }

        set(_option, value, false);
    }

    provider.reset();
    pub(SETTINGS_CHANGED);
}

const remove = (key, subKey) => {
    const isRemoved = subKey !== undefined ? delete store.settings[key][subKey] : delete store.settings[key];
    if (!isRemoved) log.warn(`The key: ${key + (subKey ? '.' + subKey : '')} wasn't deleted!`);
    provider.remove(key, subKey);
}

const _render = () => {
    renderer.data = panel.contentEl;
    renderer.render()
}

const panel = gui.panel(document.getElementById('settings'), '> OPTIONS', 'settings', null, [
        {caption: 'Export', handler: () => _export(), title: 'Save',},
        {caption: 'Import', handler: () => _fileReader.read(onFileLoad), title: 'Load',},
        {
            caption: 'Reset',
            handler: () => {
                if (window.confirm("Are you sure want to reset your settings?")) {
                    _reset();
                    pub(SETTINGS_CHANGED);
                }
            },
            title: 'Reset',
        },
        {}
    ],
    (show) => {
        if (show) {
            _render();
            return;
        }

        // to make sure it's disabled, but it's a tad verbose
        pub(KEYBOARD_TOGGLE_FILTER_MODE, {mode: true});
    })

function _getType(value) {
    if (value === undefined) return option.undefined
    else if (Array.isArray(value)) return option.list
    else if (typeof value === 'object' && value !== null) return option.object
    else if (typeof value === 'string') return option.string
    else if (typeof value === 'number') return option.number
    else return option.undefined;
}

const _fileReader = (() => {
    let callback_ = () => ({})

    const el = document.createElement('input');
    const reader = new FileReader();

    el.type = 'file';
    el.accept = '.txt';
    el.onchange = event => event.target.files.length && reader.readAsBinaryString(event.target.files[0]);
    reader.onload = event => callback_(event.target.result);

    return {
        read: callback => {
            callback_ = callback;
            el.click();
        },
    }
})();

const onFileLoad = text => {
    try {
        _import(text);
    } catch (e) {
        log.error(`Couldn't read your settings!`, e);
    }
}

sub(SETTINGS_CHANGED, _render);

/**
 * App settings module.
 *
 * So the basic idea is to let app modules request their settings
 * from an abstract store first, and if the store doesn't contain such settings yet,
 * then let the store to take default values from the module to save them before that.
 * The return value with the settings is gonna be a slice of in-memory structure
 * backed by a data provider (localStorage).
 * Doing it this way allows us to considerably simplify the code and make sure that
 * exposed settings will have the latest values without additional update/get calls.
 */
export const settings = {
    init,
    loadOr,
    getStore,
    get,
    set,
    changed,
    remove,
    import: _import,
    export: _export,
    ui: {
        set onToggle(fn) {
            panel.onToggle(fn);
        },
        toggle: () => panel.toggle(),
    },
}

// don't show these options (i.e. ignored = {'_version': 1})
const ignored = {'_version': 1};

// the main display data holder element
let data = null;

const scrollState = ((sx = 0, sy = 0, el) => ({
    track(_el) {
        el = _el
        el.addEventListener("scroll", () => ({scrollTop: sx, scrollLeft: sy} = el), {passive: true})
    },
    restore() {
        el.scrollTop = sx
        el.scrollLeft = sy
    }
}))()

// a fast way to clear data holder.
const clearData = () => {
    while (data.firstChild) data.removeChild(data.firstChild)
};

const _option = (holderEl) => {
    const wrapperEl = document.createElement('div');
    wrapperEl.classList.add('settings__option');

    const titleEl = document.createElement('div');
    titleEl.classList.add('settings__option-title');
    wrapperEl.append(titleEl);

    const nameEl = document.createElement('div');

    const valueEl = document.createElement('div');
    valueEl.classList.add('settings__option-value');
    wrapperEl.append(valueEl);

    return {
        withName: function (name = '') {
            if (name === '') return this;
            nameEl.classList.add('settings__option-name');
            nameEl.textContent = name;
            titleEl.append(nameEl);
            return this;
        },
        withClass: function (name = '') {
            wrapperEl.classList.add(name);
            return this;
        },
        withDescription(text = '') {
            if (text === '') return this;
            const descEl = document.createElement('div');
            descEl.classList.add('settings__option-desc');
            descEl.textContent = text;
            titleEl.append(descEl);
            return this;
        },
        restartNeeded: function () {
            nameEl.classList.add('restart-needed-asterisk');
            return this;
        },
        add: function (...elements) {
            if (elements.length) for (let _el of elements.flat()) valueEl.append(_el);
            return this;
        },
        build: () => holderEl.append(wrapperEl),
    };
}

const onKeyChange = (key, oldValue, newValue, handler) => {

    if (newValue !== 'Escape') {
        const _settings = settings.get()[opts.INPUT_KEYBOARD_MAP];

        if (_settings[newValue] !== undefined) {
            log.warn(`There are old settings for key: ${_settings[newValue]}, won't change!`);
        } else {
            settings.remove(opts.INPUT_KEYBOARD_MAP, oldValue);
            settings.set(opts.INPUT_KEYBOARD_MAP, {[newValue]: key});
        }
    }

    handler?.unsub();

    pub(KEYBOARD_TOGGLE_FILTER_MODE);
    pub(SETTINGS_CHANGED);
}

const _keyChangeOverlay = (keyName, oldValue) => {
    const wrapperEl = document.createElement('div');
    wrapperEl.classList.add('settings__key-wait');
    wrapperEl.textContent = `Let's choose a ${keyName} key...`;

    let handler = sub(KEYBOARD_KEY_PRESSED, button => onKeyChange(keyName, oldValue, button.key, handler));

    return wrapperEl;
}

/**
 * Handles a normal option change.
 *
 * @param key The name (id) of an option.
 * @param newValue A new value to set.
 */
const onChange = (key, newValue) => {
    settings.set(key, newValue);
    scrollState.restore(data);
}

const onKeyBindingChange = (key, oldValue) => {
    clearData();
    data.append(_keyChangeOverlay(key, oldValue));
    pub(KEYBOARD_TOGGLE_FILTER_MODE);
}

const render = function () {
    const _settings = settings.getStore();

    clearData();
    for (let k of Object.keys(_settings).sort()) {
        if (ignored[k]) continue;

        const value = _settings[k];
        switch (k) {
            case opts._VERSION:
                _option(data).withName('Options format version').add(value).build();
                break;
            case opts.LOG_LEVEL:
                _option(data).withName('Log level')
                    .add(gui.select(k, onChange, {
                        labels: ['trace', 'debug', 'warning', 'info'],
                        values: [log.TRACE, log.DEBUG, log.WARN, log.INFO].map(String)
                    }, value))
                    .build();
                break;
            case opts.INPUT_KEYBOARD_MAP:
                _option(data).withName('Keyboard bindings')
                    .withClass('keyboard-bindings')
                    .withDescription(
                        'Bindings for RetroPad. There is an alternate ESC key [Shift+`] (tilde) for cores with keyboard+mouse controls (DosBox)')
                    .add(Object.keys(value).map(k => gui.binding(value[k], k, onKeyBindingChange)))
                    .build();
                break;
            case opts.MIRROR_SCREEN:
                _option(data).withName('Video mirroring')
                    .add(gui.select(k, onChange, {values: ['mirror'], labels: []}, value))
                    .withDescription('Disables video image smoothing by rendering the video on a canvas (much more demanding for browser)')
                    .build();
                break;
            case opts.VOLUME:
                _option(data).withName('Volume (%)')
                    .add(gui.inputN(k, onChange, value))
                    .build()
                break;
            case opts.FORCE_FULLSCREEN:
                _option(data).withName('Force fullscreen')
                    .withDescription(
                        'Whether games should open in full-screen mode after starting up (excluding mobile devices)'
                    )
                    .add(gui.checkbox(k, onChange, value, 'Enabled', 'settings__option-checkbox'))
                    .build()
                break;
            default:
                _option(data).withName(k).add(value).build();
        }
    }

    data.append(
        gui.create('br'),
        gui.create('div', (el) => {
            el.classList.add('settings__info', 'restart-needed-asterisk-b');
            el.innerText = ' -- applied after page reload'
        }),
        gui.create('div', (el) => {
            el.classList.add('settings__info');
            el.innerText = `Options format version: ${_settings?._version}`;
        })
    );
}

const renderer = {
    render,
    set data(el) {
        data = el;
        scrollState.track(el)
    }
}
