import MutablePromise from "mutable-promise";

export interface IStorage<T> {
    storageType: string;
    setItem(key: string, value: T): void;
    getItem(key: string): T | null;
    removeItem(key: string): void;
    itemExists(key: string): boolean;
    keys(): IterableIterator<string>; 
    reload(key:string):Promise<T|null>;
    waitForCommit():Promise<void>;
}
export type SyncIDBStorageOptions={
  // 0 wait for loadall on mount
  // 1 start loadall but not wait on mount
  // 2 postpone loadall until first access
  lazy?:0|1|2,
};
const storeName="kvStore";
export class SyncIDBStorage<T> implements IStorage<T> {
    storageType="idb";
    //private db: IDBDatabase | null = null;
    memoryCache: Record<string, T> = {}; // メモリキャッシュ
    //uncommitedCounter=new UncommitCounter();
    loadedAll=false;
    loadingPromise?:Promise<void>;
    passiveLoadingPromise=new MutablePromise();
    getLoadingPromise(passive=false){
      if(this.loadedAll)return Promise.resolve();
      if(passive)return this.passiveLoadingPromise;
      this.loadingPromise=this.loadingPromise||
        this.asyncStorage.initDB(this).then(
          ()=>{
            this.loadedAll=true;
            this.passiveLoadingPromise.resolve(void 0);
          }
        );
      return this.loadingPromise;
    }
    
    static async create<T>(dbName:string, 
      initialData:Record<string,T>,
      opt={} as SyncIDBStorageOptions): Promise<SyncIDBStorage<T>> {
      const a=new AsyncIDBStorage<T>(dbName, initialData);
      const s=new SyncIDBStorage<T>(a,dbName);
      opt.lazy=opt.lazy||0;
      if(opt.lazy<2)s.getLoadingPromise();
      if(!opt.lazy)await s.getLoadingPromise();
      return s;
    }
    ensureLoaded(){
      if(this.loadedAll)return ;
      throw Object.assign(
        new Error(`${this.channelName}: Now loading. Try again later.`),
        {retryPromise:this.getLoadingPromise(),}
      );
    }
    
    constructor(
        public asyncStorage:AsyncIDBStorage<T>,
        public channelName:string,
    ) {}
    getItem(key: string): T | null {
        return this.memoryCache[key] ?? null;
    }
    setItem(key: string, value: T): void {
        this.ensureLoaded();
        this.memoryCache[key] = value;
        //this._saveToIndexedDB(key, value);
        this.asyncStorage.setItem(key,value);
    }
    removeItem(key: string): void {
        this.ensureLoaded();
        delete this.memoryCache[key];
        //this._deleteFromIndexedDB(key);
        this.asyncStorage.removeItem(key);
    }
    itemExists(key: string): boolean {
        this.ensureLoaded();
        return key in this.memoryCache;
    }
    keys(): IterableIterator<string> {
        this.ensureLoaded();
        return Object.keys(this.memoryCache)[Symbol.iterator]();
    }
    async reload(key: string): Promise<T|null> {
        await this.getLoadingPromise();
        //const value=await this._getFromIndexedDB(key);
        const value=await this.asyncStorage.getItem(key);
        if (value){
            if (value!==this.memoryCache[key]){
                this.memoryCache[key]=value;
            }
        } else {
            if (key in this.memoryCache) {
                delete this.memoryCache[key];    
            }
        }
        return value;
    }
    async waitForCommit(){
        return await this.asyncStorage.uncommitedCounter.wait();
    }
}
export function idbReqPromise<T>(request:IDBRequest<T>){
  return new Promise<T>((resolve,reject)=>{
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
export class AsyncIDBStorage<T> {
    private db: IDBDatabase | null = null;
    uncommitedCounter=new UncommitCounter();
    constructor(
        public dbName = "SyncStorageDB", 
        public initialData:Record<string,T>,
    ) {}
    async initDB(s:SyncIDBStorage<T>): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            const request = indexedDB.open(this.dbName, 1);
            request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
                const db = (event.target as IDBOpenDBRequest).result;
                if (!db.objectStoreNames.contains(storeName)) {
                    db.createObjectStore(storeName);
                }
            };
            request.onsuccess = (event: Event) => {
                this.db = (event.target as IDBOpenDBRequest).result;
                this.loadAllData(s).then(resolve).catch(reject);
            };
            request.onerror = (event: Event) => reject((event.target as IDBOpenDBRequest).error);
        });
    }
    async loadAllData(s: SyncIDBStorage<T>): Promise<void> {
        const transaction = this.db!.transaction(storeName, "readonly");
        const store = transaction.objectStore(storeName);
        // Get all keys and values in the same transaction
        const [keys,values]= await Promise.all([
          idbReqPromise(store.getAllKeys()) as Promise<string[]>,
          idbReqPromise(store.getAll())
        ]);
        // Both arrays have the same order
        keys.forEach((key, i) => {
            if (!(key in s.memoryCache)) {
                s.memoryCache[key] = values[i] ?? "";
            }
        });
        for (let key in this.initialData) {
            if (!(key in s.memoryCache)){
                s.memoryCache[key] = this.initialData[key];
            }
        }
    }
    async getItem(key: string): Promise<T | null> {
        return new Promise((resolve, reject) => {
            if (!this.db) return resolve(null);
            const transaction = this.db.transaction(storeName, "readonly");
            const store = transaction.objectStore(storeName);
            const request = store.get(key);
            request.onsuccess = () => resolve(request.result ?? null);
            request.onerror = () => reject(request.error);
        });
    }
    async setItem(key: string, value: T): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            this.uncommitedCounter.inc();
            if (!this.db) return resolve();
            const transaction = this.db.transaction(storeName, "readwrite");
            const store = transaction.objectStore(storeName);
            const request = store.put(value, key);
            request.onsuccess = () => resolve();
            request.onerror = (event) => reject((event.target as IDBRequest).error);
        }).finally(()=>{this.uncommitedCounter.dec();});
    }
    async removeItem(key: string): Promise<void> {
        return new Promise<void>((resolve, reject) => {
            if (!this.db) return resolve();
            this.uncommitedCounter.inc();
            const transaction = this.db.transaction(storeName, "readwrite");
            const store = transaction.objectStore(storeName);
            const request = store.delete(key);
            request.onsuccess = () => resolve();
            request.onerror = (event) => reject((event.target as IDBRequest).error);
        }).finally(()=>{this.uncommitedCounter.dec();});
    }
    async waitForCommit(){
        return await this.uncommitedCounter.wait();
    }
}
class UncommitCounter {
    private value=0;
    private promise: MutablePromise<void>|undefined;
    inc() {
        this.value++;
        if (!this.promise) this.promise=new MutablePromise<void>();
    }
    dec() {
        this.value--;
        if (this.value<0) throw new Error("UncommitCounter: Invalid counter state.");
        if (this.value==0) {
            if (!this.promise) throw new Error("UncommitCounter: Invalid promise state.");
            this.promise.resolve();
            delete this.promise;
        }
    }
    async wait(){
        if (!this.promise) return;
        await this.promise;
    }
}