interface Window {
    BroadcastChannel: BroadcastChannel;
}

namespace bipc {
    export interface BroadcastMessage {
        timestamp: number;
        receiver: string;
        sender: string;

        type: string;
        data: any;
    }

    function uuidv4() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    interface ProcessQuery {
        timestamp: number
        query_id: string;
    }

    export interface ProcessQueryResponse {
        request_timestamp: number
        request_query_id: string;

        device_id: string;
        protocol: number;
    }

    export interface CertificateAcceptCallback {
        request_id: string;
    }
    export interface CertificateAcceptSucceeded {

    }

    export abstract class BasicIPCHandler {
        protected static readonly BROADCAST_UNIQUE_ID = "00000000-0000-4000-0000-000000000000";
        protected static readonly PROTOCOL_VERSION = 1;

        protected unique_id;

        protected constructor() { }

        setup() {
            this.unique_id = uuidv4(); /* lets get an unique identifier */
        }

        abstract send_message(type: string, data: any, target?: string);

        protected handle_message(message: BroadcastMessage) {
            console.log("Received: %o", message);

            if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
                if(message.type == "process-query") {
                    log.debug(LogCategory.IPC, tr("Received a device query from %s."), message.sender);
                    this.send_message("process-query-response", {
                        request_query_id: (<ProcessQuery>message.data).query_id,
                        request_timestamp: (<ProcessQuery>message.data).timestamp,

                        device_id: this.unique_id,
                        protocol: BasicIPCHandler.PROTOCOL_VERSION
                    } as ProcessQueryResponse, message.sender);
                    return;
                }
            } else if(message.receiver === this.unique_id) {
                if(message.type == "process-query-response") {
                    const response: ProcessQueryResponse = message.data;
                    if(this._query_results[response.request_query_id])
                        this._query_results[response.request_query_id].push(response);
                    else {
                        log.warn(LogCategory.IPC, tr("Received a query response for an unknown request."));
                    }
                    return;
                } else if(message.type == "certificate-accept-callback") {
                    const data: CertificateAcceptCallback = message.data;
                    if(!this._cert_accept_callbacks[data.request_id]) {
                        log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID."));
                        return;
                    }
                    this._cert_accept_callbacks[data.request_id]();
                    delete this._cert_accept_callbacks[data.request_id];

                    this.send_message("certificate-accept-succeeded", {

                    } as CertificateAcceptSucceeded, message.sender);
                    return;
                } else if(message.type == "certificate-accept-succeeded") {
                    if(!this._cert_accept_succeeded[message.sender]) {
                        log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback."));
                        return;
                    }
                    this._cert_accept_succeeded[message.sender]();
                    return;
                }
            }
        }

        private _query_results: {[key: string]:ProcessQueryResponse[]} = {};
        async query_processes(timeout?: number) : Promise<ProcessQueryResponse[]> {
            const query_id = uuidv4();
            this._query_results[query_id] = [];

            this.send_message("process-query", {
                query_id: query_id,
                timestamp: Date.now()
            } as ProcessQuery);

            await new Promise(resolve => setTimeout(resolve, timeout || 250));
            const result = this._query_results[query_id];
            delete this._query_results[query_id];
            return result;
        }

        private _cert_accept_callbacks: {[key: string]:(() => any)} = {};
        register_certificate_accept_callback(callback: () => any) : string {
            const id = uuidv4();
            this._cert_accept_callbacks[id] = callback;
            return this.unique_id + ":" + id;
        }

        private _cert_accept_succeeded: {[sender: string]:(() => any)} = {};
        post_certificate_accpected(id: string, timeout?: number) : Promise<void> {
            return new Promise((resolve, reject) => {
                const data = id.split(":");
                const timeout_id = setTimeout(() => {
                    delete this._cert_accept_succeeded[data[0]];
                    clearTimeout(timeout_id);
                    reject("timeout");
                }, timeout || 250);
                this._cert_accept_succeeded[data[0]] = () => {
                    delete this._cert_accept_succeeded[data[0]];
                    clearTimeout(timeout_id);
                    resolve();
                };
                this.send_message("certificate-accept-callback", {
                    request_id: data[1]
                } as CertificateAcceptCallback, data[0]);
            })
        }
    }

    class BroadcastChannelIPC extends BasicIPCHandler {
        private static readonly CHANNEL_NAME = "TeaSpeak-Web";

        private channel: BroadcastChannel;

        constructor() {
            super();
        }

        setup() {
            super.setup();

            this.channel = new BroadcastChannel(BroadcastChannelIPC.CHANNEL_NAME);
            this.channel.onmessage = this.on_message.bind(this);
            this.channel.onmessageerror = this.on_error.bind(this);
        }

        private on_message(event: MessageEvent) {
            if(typeof(event.data) !== "string") {
                log.warn(LogCategory.IPC, tr("Received message with an invalid type (%s): %o"), typeof(event.data), event.data);
                return;
            }

            let message: BroadcastMessage;
            try {
                message = JSON.parse(event.data);
            } catch(error) {
                log.error(LogCategory.IPC, tr("Received an invalid encoded message: %o"), event.data);
                return;
            }
            super.handle_message(message);
        }

        private on_error(event: MessageEvent) {
            log.warn(LogCategory.IPC, tr("Received error: %o"), event);
        }

        send_message(type: string, data: any, target?: string) {
            const message: BroadcastMessage = {} as any;

            message.sender = this.unique_id;
            message.receiver = target ? target : BasicIPCHandler.BROADCAST_UNIQUE_ID;
            message.timestamp = Date.now();
            message.type = type;
            message.data = data;

            this.channel.postMessage(JSON.stringify(message));
        }
    }

    let handler: BasicIPCHandler;
    export function setup() {
        if(!supported())
            return;
        /* TODO: test for support */
        handler = new BroadcastChannelIPC();
        handler.setup();
    }

    export function get_handler() {
        return handler;
    }

    export function supported() {
        /* ios does not support this */
        return typeof(window.BroadcastChannel) !== "undefined";
    }
}