import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";

const { systemManager } = sdk;

function parseValue(value: string, setting: StorageSetting, readDefaultValue: () => any, rawDevice?: boolean) {
    const type = setting.multiple ? 'array' : setting.type;

    if (type === 'boolean') {
        if (value === 'true')
            return true;
        if (value === 'false')
            return false;
        return readDefaultValue() || false;
    }
    if (type === 'number') {
        return parseFloat(value) || readDefaultValue() || 0;
    }
    if (type === 'integer') {
        return parseInt(value) || readDefaultValue() || 0;
    }
    if (type === 'array') {
        try {
            return JSON.parse(value);
        }
        catch (e) {
            return readDefaultValue() || [];
        }
    }
    if (type === 'device') {
        if (rawDevice)
            return value;
        return systemManager.getDeviceById(value) || systemManager.getDeviceById(readDefaultValue());
    }

    // string type, so check if it is json.
    if (value && setting.json) {
        try {
            return JSON.parse(value)
        }
        catch (e) {
            return readDefaultValue();
        }
    }

    return value || readDefaultValue();
}

export type HideFunction = (device: any) => boolean;

export interface StorageSetting extends Setting {
    defaultValue?: any;
    persistedDefaultValue?: any;
    onPut?: (oldValue: any, newValue: any) => void;
    onGet?: () => Promise<StorageSetting>;
    mapPut?: (oldValue: any, newValue: any) => any;
    mapGet?: (value: any) => any;
    json?: boolean;
    hide?: boolean;
    noStore?: boolean;
}

export type StorageSettingsDict<T extends string> = { [key in T]: StorageSetting };

export interface StorageSettingsDevice {
    storage: Storage;
    onDeviceEvent(eventInterface: string, eventData: any): Promise<void>;
}

export class StorageSettings<T extends string> implements Settings {
    public values: { [key in T]: any } = {} as any;
    public hasValue: { [key in T]: boolean } = {} as any;
    public options?: {
        hide?: {
            [key in T]?: () => Promise<boolean>;
        },
        onGet?: () => Promise<Partial<StorageSettingsDict<T>>>,
    };

    constructor(public device: StorageSettingsDevice, public settings: StorageSettingsDict<T>) {
        for (const key of Object.keys(settings)) {
            const setting = settings[key as T];
            const rawGet = () => this.getItem(key as T);
            let get: () => any;
            if (setting.type !== 'clippath') {
                get = rawGet;
            }
            else {
                // maybe need a mapPut. clippath is the only complex type at the moment.
                get = () => {
                    try {
                        return JSON.parse(rawGet());
                    }
                    catch (e) {
                    }
                };
            }
            Object.defineProperty(this.values, key, {
                get,
                set: value => this.putSetting(key, value),
                enumerable: true,
            });
            Object.defineProperty(this.hasValue, key, {
                get: () => this.device.storage.getItem(key) != null,
                enumerable: true,
            });
        }
    }

    get keys(): { [key in T]: string } {
        const ret: any = {};
        for (const key of Object.keys(this.settings)) {
            ret[key] = key;
        }
        return ret;
    }

    async getSettings(): Promise<Setting[]> {
        const onGet = await this.options?.onGet?.();

        const ret = [];
        for (const [key, setting] of Object.entries(this.settings)) {
            let s: StorageSetting = Object.assign({}, setting);
            if (onGet?.[key as T])
                s = Object.assign(s, onGet[key as T]);
            if (s.onGet)
                s = Object.assign(s, await s.onGet());
            if (s.hide || await this.options?.hide?.[key as T]?.())
                continue;
            s.key = key;
            s.value = this.getItemInternal(key as T, s, true);
            ret.push(s);
            delete s.onPut;
            delete s.onGet;
            delete s.mapPut;
            delete s.mapGet;
        }
        return ret;
    }

    async putSetting(key: string, value: SettingValue): Promise<void> {
        const setting: StorageSetting = this.settings[key as T];
        let oldValue: any;
        if (setting)
            oldValue = this.getItemInternal(key as T, setting);
        return this.putSettingInternal(setting, oldValue, key, value);
    }

    putSettingInternal(setting: StorageSetting, oldValue: any, key: string, value: SettingValue) {
        if (!setting?.noStore) {
            if (setting?.mapPut)
                value = setting.mapPut(oldValue, value);
            // nullish values should be removed, since Storage can't persist them correctly.
            if (typeof value === 'object')
                this.device.storage.setItem(key, JSON.stringify(value));
            else if (value == null)
                this.device.storage.removeItem(key);
            else
                this.device.storage.setItem(key, value?.toString());
        }
        setting?.onPut?.(oldValue, value);
        this.device.onDeviceEvent(ScryptedInterface.Settings, undefined);
    }

    private getItemInternal(key: T, setting: StorageSetting, rawDevice?: boolean): any {
        if (!setting)
            return this.device.storage.getItem(key);
        const readDefaultValue = () => {
            if (setting.persistedDefaultValue) {
                this.putSettingInternal(setting, undefined, key, setting.persistedDefaultValue);
                return setting.persistedDefaultValue;
            }
            return setting.defaultValue;
        };
        const ret = parseValue(this.device.storage.getItem(key), setting, readDefaultValue, rawDevice);
        return setting.mapGet ? setting.mapGet(ret) : ret;
    }

    getItem(key: T): any {
        return this.getItemInternal(key, this.settings[key]);
    }
}
