/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 * 
 * Originally from:
 * https://github.com/zhukov/webogram
 * Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
 * https://github.com/zhukov/webogram/blob/master/LICENSE
 */

import { Database } from "../config/databases";
import { MOUNT_CLASS_TO } from "../config/debug";
//import DATABASE_SESSION from "../config/databases/session";
import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromise";
import throttle from "../helpers/schedulers/throttle";
//import { WorkerTaskTemplate } from "../types";
import IDBStorage from "./idb";

function noop() {}

/* export interface LocalStorageProxySetTask extends WorkerTaskTemplate {
  type: 'localStorageProxy',
  payload: {
    type: 'set',
    keys: string[],
    values: any[]
  }
};

export interface LocalStorageProxyDeleteTask extends WorkerTaskTemplate {
  type: 'localStorageProxy',
  payload: {
    type: 'delete',
    keys: string[]
  }
}; */

const THROTTLE_TIME = 16;

/* Storage extends {[name: string]: any} *//* Storage extends Record<string, any> */
export default class AppStorage<
  Storage extends Record<string, any>, 
  T extends Database<any>
> {
  private static STORAGES: AppStorage<any, Database<any>>[] = [];
  private storage: IDBStorage<T>;//new CacheStorageController('session');

  //private cache: Partial<{[key: string]: Storage[typeof key]}> = {};
  private cache: Partial<Storage> = {};
  private useStorage: boolean;
  private savingFreezed: boolean;

  private getPromises: Map<keyof Storage, CancellablePromise<Storage[keyof Storage]>> = new Map();
  private getThrottled: () => void;

  private keysToSet: Set<keyof Storage> = new Set();
  private saveThrottled: () => void;
  private saveDeferred = deferredPromise<void>();

  private keysToDelete: Set<keyof Storage> = new Set();
  private deleteThrottled: () => void;
  private deleteDeferred = deferredPromise<void>();

  constructor(private db: T, private storeName: typeof db['stores'][number]['name']) {
    this.storage = new IDBStorage<T>(db, storeName);

    if(AppStorage.STORAGES.length) {
      this.useStorage = AppStorage.STORAGES[0].useStorage;
    } else {
      this.useStorage = true;
    }

    this.savingFreezed = false;

    AppStorage.STORAGES.push(this);

    this.saveThrottled = throttle(async() => {
      const deferred = this.saveDeferred;
      this.saveDeferred = deferredPromise();

      const set = this.keysToSet;
      if(set.size) {
        const keys = Array.from(set.values()) as string[];
        set.clear();

        const values = keys.map((key) => this.cache[key]);
        try {
          //console.log('setItem: will set', key/* , value */);
          //await this.cacheStorage.delete(key); // * try to prevent memory leak in Chrome leading to 'Unexpected internal error.'
          //await this.storage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));

          /* if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage
            self.postMessage({
              type: 'localStorageProxy', 
              payload: {
                type: 'set',
                keys,
                values
              }
            } as LocalStorageProxySetTask);
          } */

          await this.storage.save(keys, values);
          //console.log('setItem: have set', key/* , value */);
        } catch(e) {
          //this.useCS = false;
          console.error('[AS]: set error:', e, keys, values);
        }
      }

      deferred.resolve();

      if(set.size) {
        this.saveThrottled();
      }
    }, THROTTLE_TIME, false);

    this.deleteThrottled = throttle(async() => {
      const deferred = this.deleteDeferred;
      this.deleteDeferred = deferredPromise();

      const set = this.keysToDelete;
      if(set.size) {
        const keys = Array.from(set.values()) as string[];
        set.clear();

        try {
          /* if(db === DATABASE_SESSION && !('localStorage' in self)) { // * support legacy Webogram's localStorage
            self.postMessage({
              type: 'localStorageProxy', 
              payload: {
                type: 'delete',
                keys
              }
            } as LocalStorageProxyDeleteTask);
          } */

          await this.storage.delete(keys);
        } catch(e) {
          console.error('[AS]: delete error:', e, keys);
        }
      }

      deferred.resolve();

      if(set.size) {
        this.deleteThrottled();
      }
    }, THROTTLE_TIME, false);

    this.getThrottled = throttle(async() => {
      const keys = Array.from(this.getPromises.keys());

      // const perf = performance.now();
      this.storage.get(keys as string[]).then((values) => {
        for(let i = 0, length = keys.length; i < length; ++i) {
          const key = keys[i];
          const deferred = this.getPromises.get(key);
          if(deferred) {
            // @ts-ignore
            deferred.resolve(this.cache[key] = values[i]);
            this.getPromises.delete(key);
          }
        }

        // console.log('[AS]: get time', keys, performance.now() - perf);
      }, (error) => {
        if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(error)) {
          this.useStorage = false;
          console.error('[AS]: get error:', error, keys, storeName);
        }

        for(let i = 0, length = keys.length; i < length; ++i) {
          const key = keys[i];
          const deferred = this.getPromises.get(key);
          if(deferred) {
            //deferred.reject(error);
            deferred.resolve(undefined);
            this.getPromises.delete(key);
          }
        }
      }).finally(() => {
        if(this.getPromises.size) {
          this.getThrottled();
        }
      });
    }, THROTTLE_TIME, false);
  }

  public isAvailable() {
    return this.useStorage;
  }

  public getCache() {
    return this.cache;
  }

  public getFromCache<T extends keyof Storage>(key: T) {
    return this.cache[key];
  }

  public setToCache(key: keyof Storage, value: Storage[typeof key]) {
    return this.cache[key] = value;
  }

  public async get<T extends keyof Storage>(key: T, useCache = true): Promise<Storage[T]> {
    if(this.cache.hasOwnProperty(key) && useCache) {
      return this.getFromCache(key);
    } else if(this.useStorage) {
      const r = this.getPromises.get(key);
      if(r) return r as any;

      const p = deferredPromise<Storage[T]>();
      this.getPromises.set(key, p as any);

      this.getThrottled();

      return p;
    }/*  else {
      throw 'something went wrong';
    } */
  }

  public getAll() {
    return this.storage.getAll().catch(() => []);
  }

  public set(obj: Partial<Storage>, onlyLocal = false) {
    //console.log('storageSetValue', obj, callback, arguments);

    const canUseStorage = this.useStorage && !onlyLocal && !this.savingFreezed;
    for(const key in obj) {
      if(obj.hasOwnProperty(key)) {
        const value = obj[key];
        this.setToCache(key, value);

        // let perf = /* DEBUG */false ? performance.now() : 0;
        // value = JSON.stringify(value);

        // if(perf) {
        //   let elapsedTime = performance.now() - perf;
        //   if(elapsedTime > 10) {
        //     console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key);
        //   }
        // }
        
        /* perf = performance.now();
        value = stringify(value);
        console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */

        if(canUseStorage) {
          this.keysToSet.add(key);
          this.keysToDelete.delete(key);
          this.saveThrottled();
        }
      }
    }

    return canUseStorage ? this.saveDeferred : Promise.resolve();
  }

  public delete(key: keyof Storage, saveLocal = false) {
    /* if(!this.cache.hasOwnProperty(key)) {
      return;
    } */

    // ! it is needed here
    key = '' + key;

    if(!saveLocal) {
      delete this.cache[key];
    }
    
    if(this.useStorage) {
      this.keysToSet.delete(key);
      this.keysToDelete.add(key);
      this.deleteThrottled();
    }

    return this.useStorage ? this.deleteDeferred : Promise.resolve();
  }

  public clear(saveLocal = false) {
    if(!saveLocal) {
      for(const i in this.cache) {
        delete this.cache[i];
      }
    }

    return this.storage.clear().catch(noop);
  }

  public static toggleStorage(enabled: boolean) {
    return Promise.all(this.STORAGES.map((storage) => {
      storage.useStorage = enabled;
      
      if(!enabled) {
        storage.keysToSet.clear();
        storage.keysToDelete.clear();
        storage.getPromises.forEach((deferred) => deferred.resolve(undefined));
        storage.getPromises.clear();
        return storage.clear(true);
      } else {
        return storage.set(storage.cache);
      }
    })).catch(noop);
  }

  public static freezeSaving<T extends Database<any>>(callback: () => any, names: T['stores'][number]['name'][]) {
    this.STORAGES.forEach((storage) => storage.savingFreezed = true);
    try {
      callback();
    } catch(err) {
      console.error('freezeSaving callback error:', err);
    }
    this.STORAGES.forEach((storage) => storage.savingFreezed = false);
  }

  /* public deleteDatabase() {
    return IDBStorage.deleteDatabase().catch(noop);
  } */
}

MOUNT_CLASS_TO && (MOUNT_CLASS_TO.AppStorage = AppStorage);
