import {Constructable, Mixin} from "../util/Common.js";
import {CanLog} from "../util/role/CanLog.js";
import {Channel, Envelop, EnvelopId, Message} from "./Types.js";

// globally unique, to up to repeated usages of this module in different JS context
let ENVELOP_COUNTER         : EnvelopId = 1

export const ChannelEndPoint = <T extends Constructable<CanLog>>(base : T) =>

class ChannelEndPoint extends base {
    connectionTimeout       : number
    connectionInterval      : number = 1000

    awaitingResponses       : Map<EnvelopId, [ Function, Function, Envelop, any ]> = new Map()


    doConnect (channel : Channel) : Promise<any> {
        throw "Abstract method `doConnect`"
    }

    doDisconnect () {
        throw "Abstract method `doDisconnect`"
    }

    sendMessage (message : Message) {
        throw "Abstract method `sendMessage`"
    }

    messageToEnvelop (message : Message) : Envelop | undefined {
        throw "Abstract method `messageToEnvelop`"
    }

    envelopToMessage (envelop : Envelop) : Message {
        throw "Abstract method `envelopToMessage`"
    }

    doProcessRawChannelMessage (message : Message, envelop : Envelop) {
        throw "Abstract method `doProcessRawChannelMessage`"
    }

    doDispatchEnvelop (envelop : Envelop) {
        throw "Abstract method `doDispatchEnvelop`"
    }


    connect(channel? : Channel) : Promise<any> {
        let start   = new Date().getTime()

        return new Promise((resolve, reject) => {

            const connectionAttempt = () => {
                this.doConnect(channel).then(
                    resolve,
                    reason => {
                        if (new Date().getTime() - start > (this.connectionTimeout || 3000))
                            reject(reason)
                        else
                            setTimeout(connectionAttempt, this.connectionInterval)
                    }
                )
            }

            connectionAttempt()
        })
    }


    onRawChannelMessage (message : Message) {
        this.info("Received raw message: " + JSON.stringify(message))

        const envelop       = this.messageToEnvelop(message)

        if (envelop !== undefined && envelop.id != null) {
            if (envelop.inResponseOf != null) {
                let id          = envelop.inResponseOf

                const handlers  = this.awaitingResponses.get(id)

                if (!handlers) {
                    this.warn("Response for unknown envelop, timeout occurred?:\n" + JSON.stringify(envelop))
                } else {
                    this.awaitingResponses.delete(id)

                    handlers[ envelop.isRejection ? 1 : 0 ](envelop.payload)
                }
            } else
                this.doDispatchEnvelop(envelop)
        } else {
            this.doProcessRawChannelMessage(message, envelop)
        }
    }


    disconnect () {
        this.awaitingResponses.forEach((value, id) => {
            value[ 1 ]("Disconnecting")
        })

        this.awaitingResponses.clear()

        return this.doDisconnect()
    }


    getFreshEnvelopId () : EnvelopId {
        return ENVELOP_COUNTER++
    }


    replyWith (envelop : Envelop, result : any, isRejection : boolean = false) {
        this.sendMessage(this.envelopToMessage({
            id              : this.getFreshEnvelopId(),
            inResponseOf    : envelop.id,
            isRejection     : isRejection,
            payload         : result
        }))
    }


    sendRpcCall (payload : object, timeout? : number) : Promise<any> {
        this.debug("Sending command: " + payload.toString())

        const id        = this.getFreshEnvelopId()

        const envelop : Envelop  = { id : id, payload : payload }

        return new Promise((resolve, reject) => {
            let timeoutHandler

            if (timeout != null) {
                let timeoutHandler = setTimeout(() => {
                    this.debug("Timeout occurred for: " + JSON.stringify(envelop))

                    if (this.awaitingResponses.has(id)) {
                        this.awaitingResponses.delete(id)

                        reject("Timeout while waiting for command result: " + JSON.stringify(payload))
                    }
                }, timeout)
            }

            this.awaitingResponses.set(id, [ resolve, reject, envelop, timeoutHandler ])

            this.sendMessage(this.envelopToMessage(envelop))
        })
    }
}

export type ChannelEndPoint = Mixin<typeof ChannelEndPoint>
