/// <reference path="leaf.ts" />
namespace Leaf {
    export type RPCId = number
    export const RPCErrors = Leaf.ErrorDoc.create()
        .define("APINotExists")
        .define("Timeout")
        .define("Conflict")
        .define("BrokenConnection")
        .generate() as ErrorsOf<{
            APINotExists
            Timeout
            Conflict
            BrokenConnection
        }>
    export interface RPCCallback<TResult> {
        (error: Error, result: TResult): void
    }
    export interface RPCInvoke<TOption, TResult> {
        (option?: TOption, callback?: RPCCallback<TResult>): void
        RPC?: RPCDeclare<TOption, TResult>
    }
    export interface RPCOption {
        name: string
        timeout?: number
    }
    export interface RPCMethodHandler {
        (data: any, callback: { (err?: Error, data?: any): void }): void
    }
    export class RPCComposer {
        public declares: any = {}
        public requestOffset: number = 10000
        public methods: {
            [index: string]: RPCMethodHandler
        } = {}
        public remoteEvents: EventEmitter<string> = new EventEmitter()
        private connection: RPCConnection
        private responseHandlers: RPCResponseHandler[] = []
        public customNormalize: { (data: any): string }
        public customDenormalize: { (data: string): any }
        private allocateId() { return this.requestOffset++ }
        constructor(public option: {
            defaultTimeout?: number
        } = {}) {
            if (!(typeof this.option.defaultTimeout === "number")) {
                this.option.defaultTimeout = 0
            }
        }
        public setConnection(con: RPCConnection) {
            this.connection = con
            this.connection.events.listenBy(this, "message", (message: string) => {
                this.handleMessage(message)
            })
            this.connection.events.listenBy(this, "close", () => {
                this.unsetConnection()
            })
        }
        public unsetConnection() {
            this.connection.events.stopListenBy(this)
            this.connection = null
        }
        public registerRPCMethod(name: string, handler: RPCMethodHandler) {
            if (this.methods[name]) { throw new RPCErrors.Conflict(`RPC Method "${name}" already exists`) }
            this.methods[name] = handler
        }
        public createRPCDeclare<TOption, TResult>(option: RPCOption): RPCDeclare<TOption, TResult> {
            let api = new RPCDeclare<TOption, TResult>(this, option)
            this.declares[option.name] = api
            return api
        }
        public createRPCInvokeInterface<TOption, TResult>(option: RPCOption): RPCInvoke<TOption, TResult> {
            let api = this.createRPCDeclare<TOption, TResult>(option)
            this[option.name] = api
            return api.getFunction()
        }
        public emitRemoteEvent(name: string, data?: any) {
            let blob: RPCProtocolEventBlob = {
                name: name
                , data: data
                , type: RPCProtocolBlobType.EVENT
            }
            this.send(this.normalize(blob))
        }
        public invoke(name: string, data: any, callback?: Function, option: {
            timeout?: number
        } = {}) {
            if (!callback) { callback = () => { } }

            if (!this.connection) {
                callback(new RPCErrors.BrokenConnection("Connection not exists"))
                return
            }
            let handler: RPCResponseHandler = {
                id: this.allocateId(),
                callback: callback,
                finished: false,
                timer: null
            }
            let timeout = option.timeout || this.option.defaultTimeout
            if (timeout > 0) {
                handler.timer = setTimeout(() => {
                    if (!handler.finished) {
                        this.finishHandler(handler.id, new RPCErrors.Timeout(`RPC timeout after ${this.option.defaultTimeout}`))
                    }
                }, timeout)
            }
            this.responseHandlers.push(handler)
            let blob: RPCProtocolInvokeBlob = {
                id: handler.id,
                method: name,
                data: data,
                type: RPCProtocolBlobType.INVOKE
            }
            this.send(this.normalize(blob))
        }
        private send(data: any) {
            if (typeof data !== "string") {
                data = JSON.stringify(data)
            }
            this.connection.send(data)
        }
        private handleMessage(rawString: string) {
            let data: any = null
            try {
                let raw = JSON.parse(rawString)
                data = this.denormalize(raw)
            } catch (e) {
                return
            }
            if (data.type === RPCProtocolBlobType.INVOKE) {
                this.handleInvoke(data)
            } else if (data.type === RPCProtocolBlobType.RESPONSE) {
                this.handleResponse(data)
            } else if (data.type === RPCProtocolBlobType.EVENT) {
                this.handleEvent(data)
            }
        }
        private handleInvoke(req: RPCProtocolInvokeBlob) {
            let response: RPCProtocolResponseBlob = {
                id: req.id,
                type: RPCProtocolBlobType.RESPONSE
            }
            if (!this.methods[req.method]) {
                response.error = new RPCErrors.APINotExists(`Requesting API ${req.method} not exists`)
                this.send(this.normalize(response))
                return
            }
            let call = this.methods[req.method]
            call(req.data, (err, data) => {
                response.error = err
                response.data = data
                this.send(this.normalize(response))
            })

        }
        private handleResponse(res: RPCProtocolResponseBlob) {
            this.finishHandler(res.id, res.error, res.data)
        }
        private handleEvent(event: RPCProtocolEventBlob) {
            this.remoteEvents.emit(event.name, event.data)
        }
        private transformObject(data: any, handler: Function): any {
            if (typeof data !== "object") {
                return data
            }
            if (data instanceof Array) {
                return data.map((item) => { return this.transformObject(item, handler) })
            }
            if (!data) return data
            let norm = data
            if (handler) {
                norm = handler(norm)
            }
            // changed by custom norm
            if (norm && norm !== data) {
                return norm
            }
            for (let prop in data) {
                data[prop] = this.transformObject(data[prop], handler)
            }
            return data
        }
        private normalize(data: any): any {
            return this.transformObject(data, this.customNormalize)
        }
        private denormalize(data: any): any {
            return this.transformObject(data, this.customDenormalize)
        }
        private finishHandler(id: RPCId, err?: Error, value?: any) {
            this.responseHandlers = this.responseHandlers.filter((handler) => {
                if (handler.id === id) {
                    handler.finished = true
                    if (handler.timer) {
                        clearTimeout(handler.timer)
                        handler.timer = null
                    }
                    setTimeout(() => {
                        handler.callback(err, value)
                    }, 0)
                    return false
                }
                return true
            })
        }
    }

    export class RPCDeclare<TOption, TResult> {
        constructor(private center: RPCComposer, private option: RPCOption) {
        }
        public getFunction(): RPCInvoke<TOption, TResult> {
            let fn: RPCInvoke<TOption, TResult> = this.invoke.bind(this)
            fn.RPC = this
            return fn
        }
        public invoke(data: TOption = {} as any, callback?: RPCCallback<TResult>) {
            this.center.invoke(this.option.name, data, callback, {
                timeout: this.option.timeout || 0
            })
        }
    }
    export type RPCBaseConnectionMessage = string
    export interface RPCConnection {
        events: EventEmitter<"message" | "close">
        send: { (data: string): void }
    }
    export abstract class RPCConnectionBase implements RPCConnection {
        events: EventEmitter<string> = new EventEmitter()
        public abstract send(data: string): void;
    }
    export enum RPCProtocolBlobType {
        INVOKE = 1
        , RESPONSE
        , EVENT
    }
    export interface RPCResponseHandler {
        id: RPCId
        callback: Function
        timer: any // timer for timeout the info
        finished: boolean
    }
    export interface RPCProtocolBlob {
        type: RPCProtocolBlobType
    }
    export interface RPCProtocolResponseBlob extends RPCProtocolBlob {
        id: RPCId
        data?: any
        error?: any
        type: RPCProtocolBlobType
    }
    export interface RPCProtocolInvokeBlob extends RPCProtocolBlob {
        id: RPCId
        method: string
        data: any
        type: RPCProtocolBlobType
    }
    export interface RPCProtocolEventBlob extends RPCProtocolBlob {
        name: string
        data: any
        type: RPCProtocolBlobType
    }
    export class RPCBrowserWebSocketConnection extends RPCConnectionBase implements RPCConnection {
        constructor(public connection: WebSocket) {
            super()
            this.connection.addEventListener("message", (msg) => {
                this.events.emit("message", msg.data)
            })
            this.connection.addEventListener("close", () => {
                this.events.emit("close")
            })
        }
        send(content: string) {
            this.connection.send(content)
        }
    }
}
