var bsock = require('bsock')

import { ExtendedRpcInfo, UnhookFunction, CallbackFunction, AsyncFunction } from "./Types";
import { Socket } from './interfaces/Socket'

//fix args with defaults like "force = true" -> "force"
function stripAfterEquals(str:string){
    return str.split("=")[0]
}

export class Client implements Socket{
    private socket: Socket
    constructor(public port:number, private server: string, private tls: boolean = false){
    }

    hook(name, args){
        return this.socket.hook(name, args)
    }

    unhook(name){
        return this.socket.unhook(name)
    }

    on(type: "error" | "close", f: (e?: any) => void){
        return this.socket.on(type, name)
    }

    destroy(){
        return this.socket.destroy()
    }

    close(){
        return this.socket.close()
    }

    async call (rpcname: string, ...args: any[]) : Promise<any>{
        return await this.socket.call.apply(this.socket, [rpcname, ...args])
    }

    async fire(rpcname: string, ...args: any[]) : Promise<any>{
        return await this.socket.fire.apply(this.socket, [rpcname, ...args])
    }
    
    async connect(){
        this.socket = await bsock.connect(this.port, this.server, this.tls)

        const info:ExtendedRpcInfo[] = await this.info()
        info.forEach(i => {
            let f: any
            switch (i.type) {
                case 'Call':
                    f = this.callGenerator(i.uniqueName, i.argNames)    
                    break                
                case 'Hook':
                    f = this.hookGenerator(i.uniqueName, i.argNames)                    
                    break
                case 'Unhook':
                    f = this.unhookGenerator(i.uniqueName, i.argNames)                    
                    break
            }
            if(this[i.owner] == null)
                this[i.owner] = {}
            this[i.owner][i.name] = f
            this[i.owner][i.name].bind(this)
        })
    }

    async info(){
        return await this.socket.call('info')
    }

    private callGenerator(fnName, fnArgs:string[]): AsyncFunction{
        const headerArgs = fnArgs.join(",")
        const argParams = fnArgs.map(stripAfterEquals).join(",")
        return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' )
    }

    private hookGenerator(fnName, fnArgs:string[]): CallbackFunction{
        const headerArgs = fnArgs.join(",")
        const argParams = fnArgs.map(stripAfterEquals).join(",")
        return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => {
                            const r = await this.socket.call("`+fnName+`", `+argParams+`)
                            if(r.uid != null){
                                this.socket.hook(res.uid, callback)
                            }
                            return res
                        } )()` )
    }

    private unhookGenerator(fnName, fnArgs:string[]): UnhookFunction{
        const headerArgs = fnArgs.join(",")
        const argParams = fnArgs.map(stripAfterEquals).join(",")
        if(fnArgs.length != 1)
            console.error("UnhookFunction", fnName, "specified more than one argument: ("+headerArgs+")")

        return eval( `( () => async (`+headerArgs+`) => {
                            const r = await this.socket.call("`+fnName+`", `+argParams+`)
                            this.socket.unhook(`+argParams+`)
                            return res
                        } )()` )
    }

}