import { deleteDB, IDBPDatabase, openDB } from 'idb';
import { cursorTo } from 'readline';
import { timeout } from '../helper';
import { BaseDto, DtoClass } from './DBAccess';
import sem from 'semaphore';



export interface TransactionEntry<T> {
    onExecute: () => Promise<T | T[] | undefined>
    onGetPostTransactionState?: () => Promise<T | undefined>
}

export class Transaction {
    entries: TransactionEntry<BaseDto>[] = [];
    entityNames: Set<string> = new Set();
    completeEntries: TransactionEntry<BaseDto>[] = [];

    constructor() {
        this.entityNames.add('Transactions');
    }

    addEntry<T extends BaseDto>(entry: TransactionEntry<T>) {
        this.entries.push(entry);
    }

    hasNextEntry() {
        return this.entries.length > 0;
    }

    getNextEntry(): TransactionEntry<BaseDto> {
        if (this.entries.length === 0) {
            throw new Error('no more entries');
        }
        const currentEntry = this.entries.splice(0, 1)[0];
        this.completeEntries.push(currentEntry);
        return currentEntry;
    }

    addEntity(name: string) {
        this.entityNames.add(name);
    }

}

export class TransactionManager {
    db: IDBPDatabase | undefined;
    initPromise: Promise<void> | undefined;

    private stores: string[] = [];

    constructor() {
        this.stores.push('Transactions');
    }

    addObjectStore(objectStoreName: string) {
        this.stores.push(objectStoreName);
    }

    init(dbName: string): Promise<void> {
        return this.initPromise = (async () => {
            // eslint-disable-next-line @typescript-eslint/no-this-alias
            const tmThis = this;
            this.db = await openDB(dbName, new Date().getTime(), {
                upgrade(db) {
                    const existingStores = Array.from(db.objectStoreNames);
                    // Create a store of objects
                    for (const oldName of existingStores) {
                        if (!tmThis.stores.includes(oldName)) {
                            db.deleteObjectStore(oldName);
                        }
                    }
                    for (const storeName of tmThis.stores) {
                        if (!existingStores.includes(storeName)) {
                            db.createObjectStore(storeName, {
                                keyPath: 'id',
                                autoIncrement: false
                            }).createIndex('id', 'id', { unique: true });
                        }
                    }
                },
            });
        })();
    }

    deleteDB(dbName: string): Promise<void> {
        this.initPromise = undefined;
        return deleteDB(dbName);
    }

    private dataChangeListeners: ({ listener: (data: BaseDto) => void, type: DtoClass })[] = [];

    private transactionSemaphore = sem(1);

    async lock() {
        return await new Promise<void>(r => this.transactionSemaphore.take(() => { r() }));
    }

    unlock() {
        this.transactionSemaphore.leave();
    }

    async runTransaction(callback: (dbtransaction: Transaction, wait: CallableFunction) => Promise<Promise<any> | void>): Promise<void> {
        await this.lock();
        try {
            if (!this.initPromise) {
                throw new Error('TransactionManager not initialized');
            }
            await this.initPromise;
            if (!this.db) {
                return;
            }
            const transaction = new Transaction();
            const wait = async () => {
                while (transaction.hasNextEntry()) {
                    await transaction.getNextEntry().onExecute();
                }
            };
            await callback(transaction, wait);
            const tx = this.db.transaction(Array.from(transaction.entityNames), 'readwrite');
            try {
                while (transaction.hasNextEntry()) {
                    await transaction.getNextEntry().onExecute();
                }
                await tx.done;
            } catch (e) {
                console.error(e);
                await tx.abort();
                throw e;
            }

            const postTransactionStateList = (await Promise.all(transaction.completeEntries.filter(entry => entry.onGetPostTransactionState).map(entry => entry.onGetPostTransactionState?.()).filter(entry => entry !== undefined) as Promise<BaseDto>[]))
            for (const { listener, type } of this.dataChangeListeners) {
                for (const entry of postTransactionStateList) {
                    if (entry.id?.startsWith(type.dbName + ':')) {
                        listener(entry);
                    }
                }
            }
        }
        finally {
            this.unlock();
        }
    }

    async addDataChangeListener(listener: (data: BaseDto) => void, type: DtoClass) {
        this.dataChangeListeners.push({ listener, type });
    }
}