//based on https://github.com/signalapp/libsignal-protocol-javascript/blob/master/test/InMemorySignalProtocolStore.js

import Base64 from '../base64';
import * as libsignal from '@privacyresearch/libsignal-protocol-typescript';
import { getRandomUUID } from '../crypto';
import { SignalProtocolAddress } from '@privacyresearch/libsignal-protocol-typescript';
import { SignalProtocolDto } from '../libDto/SignalProtocolDto';
import { ObjectStore } from '../db/DBAccess';
import { DBFactory } from '../db/DBFactory';

interface SignalObjectStore {
    get(key: string): Promise<SignalProtocolDto | undefined>;
    update(object: SignalProtocolDto): Promise<SignalProtocolDto>;
    delete(obj: SignalProtocolDto): Promise<SignalProtocolDto>;
    getAll(): Promise<SignalProtocolDto[]>;
}

class EphemeralSignalObjectStore implements SignalObjectStore {
    storeMap: Map<string, SignalProtocolDto> = new Map();

    get(key: string): Promise<SignalProtocolDto | undefined> {
        return Promise.resolve(this.storeMap.get(key));
    }

    getAll(): Promise<SignalProtocolDto[]> {
        return Promise.resolve(Array.from(this.storeMap.values()));
    }

    update(object: SignalProtocolDto): Promise<SignalProtocolDto> {
        this.storeMap.set(object.id, object);
        return Promise.resolve(object);
    }

    delete(obj: SignalProtocolDto): Promise<SignalProtocolDto> {
        this.storeMap.delete(obj.id);
        return Promise.resolve(obj);
    }

}

export class SignalProtocolStore {
    private _initPromise: Promise<void>;
    private _signalStore?: SignalObjectStore;

    constructor(private dbFactory?: DBFactory) {
        this.dbFactory = dbFactory;
        this._initPromise = this._init();
    }

    async _init() {
        if (this.dbFactory) {
            this._signalStore = await this.dbFactory.getDao<ObjectStore<SignalProtocolDto>>(SignalProtocolDto);
        } else {
            this._signalStore = new EphemeralSignalObjectStore();
        }
    }

    Direction = {
        SENDING: 1,
        RECEIVING: 2,
    };

    async getIdentityKeyPair() {
        const value = JSON.parse(await this.get('identityKey'));
        return { pubKey: Base64.decode(value.pubKey), privKey: Base64.decode(value.privKey) };
    }
    getLocalRegistrationId() {
        return Promise.resolve(this.get('registrationId'));
    }
    async put(key: string, value: any) {
        await this._initPromise;
        if (key === undefined || value === undefined || key === null || value === null)
            throw new Error('Tried to store undefined/null');
        await this._signalStore!.update(new SignalProtocolDto(key, value));
    }
    async get(key: string, defaultValue?: any) {
        await this._initPromise;
        if (key === null || key === undefined)
            throw new Error('Tried to get value for undefined/null key');
        const signalData = await this._signalStore!.get(key);
        if (signalData) {
            return signalData.value;
        } else {
            return defaultValue;
        }
    }
    async remove(key: string) {
        await this._initPromise;
        if (key === null || key === undefined)
            throw new Error('Tried to remove value for undefined/null key');
        await this._signalStore!.delete(new SignalProtocolDto(key, ''));
    }

    async isTrustedIdentity(identifier: string, identityKey: ArrayBuffer) {
        if (identifier === null || identifier === undefined) {
            throw new Error('tried to check identity key for undefined/null key');
        }
        if (!(identityKey instanceof ArrayBuffer)) {
            throw new Error('Expected identityKey to be an ArrayBuffer');
        }
        const trusted = await this.get('identityKey' + identifier);
        if (trusted === undefined) {
            return true;
        }
        return Base64.encode(identityKey) === trusted;
    }
    async loadIdentityKey(identifier: string) {
        if (identifier === null || identifier === undefined)
            throw new Error('Tried to get identity key for undefined/null key');
        return Base64.decode(await this.get('identityKey' + identifier));
    }
    async saveIdentity(identifier: string, identityKey: ArrayBuffer) {
        await this._initPromise;
        if (identifier === null || identifier === undefined)
            throw new Error('Tried to put identity key for undefined/null key');

        const address = libsignal.SignalProtocolAddress.fromString(identifier);

        const existing = await this.get('identityKey' + address.getName());
        await this.put('identityKey' + address.getName(), Base64.encode(identityKey));

        if (existing && Base64.encode(identityKey) !== Base64.encode(existing)) {
            return Promise.resolve(true);
        } else {
            return Promise.resolve(false);
        }

    }

    /* Returns a prekeypair object or undefined */
    async loadPreKey(keyId: string | number) {
        await this._initPromise;
        let res = await this.get('25519KeypreKey' + keyId);
        if (res !== undefined) {
            res = JSON.parse(res);
            res = { pubKey: Base64.decode(res.pubKey), privKey: Base64.decode(res.privKey) };
        }
        return res;
    }
    async storePreKey(keyId: string | number, keyPair: any) {
        const stringified = JSON.stringify({ pubKey: Base64.encode(keyPair.pubKey), privKey: Base64.encode(keyPair.privKey) });
        return await this.put('25519KeypreKey' + keyId, stringified);
    }
    async removePreKey(keyId: string | number) {
        return await this.remove('25519KeypreKey' + keyId);
    }

    /* Returns a signed keypair object or undefined */
    async loadSignedPreKey(keyId: string | number): Promise<any | undefined> {
        let res = await this.get('25519KeysignedKey' + keyId);
        if (res !== undefined) {
            res = JSON.parse(res);
            res = { pubKey: Base64.decode(res.pubKey), privKey: Base64.decode(res.privKey) };
        }
        return res;
    }
    async storeSignedPreKey(keyId: string | number, keyPair: any) {
        const stringified = JSON.stringify({ pubKey: Base64.encode(keyPair.pubKey), privKey: Base64.encode(keyPair.privKey) });
        return await this.put('25519KeysignedKey' + keyId, stringified);
    }
    async removeSignedPreKey(keyId: string | number) {
        return await this.remove('25519KeysignedKey' + keyId);
    }

    async loadSession(identifier: string) {
        return await this.get('session' + identifier);
    }
    async storeSession(identifier: string, record: any) {
        return await this.put('session' + identifier, record);
    }
    async removeSession(identifier: string) {
        return await this.remove('session' + identifier);
    }
    async removeAllSessions(identifier: string) {
        await this._initPromise;
        const sessions = await this._signalStore!.getAll();
        for (const session of sessions) {
            if (session.id.startsWith('session' + identifier)) {
                await this.remove(session.id);
            }
        }
    }
    async getPreKeyId() {
        const currkey = parseInt(await this.get('preKeyId'), 10) || 0;
        await this.put('preKeyId', (currkey + 1) + '');
        return currkey;
    }

    async setOwnUserId(userId: string) {
        const currentUserId = await this.get('userId');
        if (currentUserId) {
            throw new Error('Own userId already set');
        }
        userId = new SignalProtocolAddress(getRandomUUID(), 1).toString();
        await this.put('userId', userId);
        console.info('Generated new own userId: ' + userId);
    }

    async getOwnUserId() {
        let userId = await this.get('userId');
        if (!userId) {
            userId = new SignalProtocolAddress(getRandomUUID(), 1).toString();
            await this.put('userId', userId);
            console.info('Generated new own userId: ' + userId);
        }
        return userId;
    }
}
