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

import { KeyHelper, MessageType, SessionBuilder, SessionCipher, SignalProtocolAddress } from '@privacyresearch/libsignal-protocol-typescript';
import Base64 from '../base64';
import { DBFactory } from '../db/DBFactory';
import { SignalProtocolStore } from './SignalProtocolStore';



export class SignalUtil {
    store: SignalProtocolStore | undefined;
    private _initPromise: Promise<void>;

    constructor(dbFactory?: DBFactory, ownUserId?: string) {
        this._initPromise = this.initSignal(dbFactory);
    }

    async initSignal(dbFactory?: DBFactory) {
        this.store = new SignalProtocolStore(dbFactory);
        if (!(await this.store.get('identityKey'))) {
            if (localStorage.getItem('initSignal')) {
                return;
            }
            localStorage.setItem('initSignal', 'true');
            console.log('initializing libsignal...');
            const registrationId = KeyHelper.generateRegistrationId();
            await this.store.put('registrationId', registrationId);

            const identityKeypair = await KeyHelper.generateIdentityKeyPair();
            const stringifiedIdentity = JSON.stringify({ pubKey: Base64.encode(identityKeypair.pubKey), privKey: Base64.encode(identityKeypair.privKey) });
            await this.store.put('identityKey', stringifiedIdentity);
            localStorage.removeItem('initSignal');
        }
    }


    async generatePreKeyBundle(preKeyId: number, signedPreKeyId: number) {
        await this._initPromise;
        const [identity, registrationId] = await Promise.all([
            this.store!.getIdentityKeyPair(),
            this.store!.getLocalRegistrationId()
        ]);
        const [preKey, signedPreKey] = await Promise.all([
            KeyHelper.generatePreKey(preKeyId),
            KeyHelper.generateSignedPreKey(identity, signedPreKeyId),
        ]);

        await this.store!.storePreKey(preKeyId, preKey.keyPair);
        await this.store!.storeSignedPreKey(signedPreKeyId, signedPreKey.keyPair);

        return {
            identityKey: identity.pubKey,
            registrationId: registrationId,
            preKey: {
                keyId: preKeyId,
                publicKey: preKey.keyPair.pubKey
            },
            signedPreKey: {
                keyId: signedPreKeyId,
                publicKey: signedPreKey.keyPair.pubKey,
                signature: signedPreKey.signature
            }
        };
    }

    serializePreKeyBundle(preKeyBundle: any): any {
        return {
            identityKey: Base64.encode(preKeyBundle.identityKey),
            registrationId: preKeyBundle.registrationId,
            preKey: {
                keyId: preKeyBundle.preKey.keyId,
                publicKey: Base64.encode(preKeyBundle.preKey.publicKey)
            },
            signedPreKey: {
                keyId: preKeyBundle.signedPreKey.keyId,
                publicKey: Base64.encode(preKeyBundle.signedPreKey.publicKey),
                signature: Base64.encode(preKeyBundle.signedPreKey.signature)
            }
        };
    }

    deserializePreKeyBundle(preKeyBundle: any) {
        return {
            identityKey: Base64.decode(preKeyBundle.identityKey),
            registrationId: preKeyBundle.registrationId,
            preKey: {
                keyId: preKeyBundle.preKey.keyId,
                publicKey: Base64.decode(preKeyBundle.preKey.publicKey)
            },
            signedPreKey: {
                keyId: preKeyBundle.signedPreKey.keyId,
                publicKey: Base64.decode(preKeyBundle.signedPreKey.publicKey),
                signature: Base64.decode(preKeyBundle.signedPreKey.signature)
            }
        }
    }

    async getPreKeyBundle(): Promise<string> {
        await this._initPromise;
        const keyId = await this.store!.getPreKeyId();
        const preKeyBundle = await this.generatePreKeyBundle(keyId, keyId);
        return this.serializePreKeyBundle(preKeyBundle);
    }

    async addContact(address: SignalProtocolAddress, preKeyBundle: string): Promise<void> {
        await this._initPromise;
        const builder = new SessionBuilder(this.store!, address);
        await builder.processPreKey(this.deserializePreKeyBundle(preKeyBundle));
    }

    async encryptTo(address: SignalProtocolAddress | string, message: string): Promise<string> {
        await this._initPromise;
        if (typeof address === 'string') {
            address = SignalProtocolAddress.fromString(address);
        }
        const sessionCipher = new SessionCipher(this.store!, address);

        const ciphertext = await sessionCipher.encrypt(new TextEncoder().encode(message).buffer);
        if (!ciphertext.body) {
            throw new Error('no ciphertext');
        }
        ciphertext.body = Base64.encode(new TextEncoder().encode(ciphertext.body));

        return JSON.stringify(ciphertext);
    }

    async decryptFrom(address: SignalProtocolAddress, ciphertext: string): Promise<string> {
        await this._initPromise;
        const sessionCipher = new SessionCipher(this.store!, address);
        const decoded = JSON.parse(ciphertext) as MessageType;
        decoded.body = new TextDecoder().decode(Base64.decode(decoded.body!));
        if (!decoded.body) {
            throw new Error('no ciphertext');
        }
        let plaintext: ArrayBuffer;
        if (decoded.type == 3) {
            plaintext = await sessionCipher.decryptPreKeyWhisperMessage(decoded.body, 'binary');
        } else {
            plaintext = await sessionCipher.decryptWhisperMessage(decoded.body, 'binary');
        }
        return new TextDecoder().decode(plaintext);
    }

    async getOwnUserId(): Promise<string> {
        await this._initPromise;
        return await this.store!.getOwnUserId();
    }
}