/// <reference path="leaf.ts" />
namespace Leaf {
    export interface APICallback<TResult> {
        (error: Error, result: TResult): void
    }
    export interface APIFunction<TOption, TResult> {
        (option: TOption, callback: APICallback<TResult>): void
        API?: APIDeclare<TOption, TResult>
    }
    export interface APIOption {
        method: string | "GET" | "POST" | "PUT" | "DELETE"
        path: string
        timeout?: number
        headers?: any
        data?: any
        withCredentials?: boolean
        bodyType?: "json" | "query"
    }
    export class APIFactory {
        constructor(public option: {
            root?: string
            prefix?: string
            suffix?: string
            defaultTimeout?: number
            withCredentials?: boolean
            bodyType?: "json" | "query"
            errorMapper?: (err: Error) => Error
        }) {
            if (!(typeof this.option.defaultTimeout === "number")) {
                this.option.defaultTimeout = 0
            }
            if (!(typeof this.option.prefix === "string")) {
                this.option.prefix = ""
            }
            if (!(typeof this.option.suffix === "string")) {
                this.option.suffix = ""
            }
            if (!this.option.root) {
                this.option.root = ""
            }
            if (option.errorMapper) {
                this.errorMapper = option.errorMapper
            }
        }
        createAPI<TOption, TResult>(option: APIOption): APIDeclare<TOption, TResult> {
            let api = new APIDeclare<TOption, TResult>(this, option)
            return api
        }
        createAPIFunction<TOption, TResult>(option: APIOption): APIFunction<TOption, TResult> {
            let api = this.createAPI<TOption, TResult>(option)
            return api.getFunction()
        }
        public errorMapper: (err: Error) => Error = (err: Error) => {
            return new Error(err.message)
        }
        private solveResponse(err, response, callback) {
            callback(err, response)
        }
        public request<TResult>(option: {
            method?: string
            url: string
            query?: any
            withCredentials?: boolean
            headers?: any
            timeout?: number
            bodyType?: "json" | "query"
        }, callback: { (err, result: TResult): void }) {
            let method = option.method || "GET"
            let url = option.url
            let sendData = null
            let headers = option.headers || {}
            let bodyType = option.bodyType || this.option.bodyType || "query"
            let queryString = this.buildQueryString(option.query)
            if (method.toUpperCase() === "GET") {
                if (url.indexOf("? < 0")) {
                    url += "?"
                } else {
                    url += "&"
                }
                url += queryString
                sendData = null
            } else {
                if (bodyType === "query") {
                    sendData = queryString
                } else {
                    sendData = JSON.stringify(option.query)
                }
            }
            let xhr = new XMLHttpRequest()
            xhr.open(method, url, true)
            if (option.withCredentials) {
                xhr.withCredentials = true
            }
            xhr.setRequestHeader("Accept", "application/json")
            if (bodyType === "json") {
                xhr.setRequestHeader("Content-Type", "application/json")
            } else {
                xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
            }
            for (let name in headers) {
                xhr.setRequestHeader(name, headers[name])
            }
            let done = false
            let _callback = callback
            let timer = null
            callback = (err, response: TResult) => {
                clearTimeout(timer)
                // disable multiple callback
                callback = () => { }
                this.solveResponse(err, response, _callback)
                return
            }
            if (option.timeout) {
                timer = setTimeout(() => {
                    callback = () => { }
                    _callback(new Errors.Timeout(`Request timeout after ${option.timeout}`), null)
                    xhr.abort()
                }, option.timeout * 1000)
            }
            xhr.onreadystatechange = () => {
                if (xhr.readyState === 0 && !done) {
                    callback(new Errors.NetworkError(), null)
                    return
                }
                if (xhr.readyState === 4) {
                    done = true
                    let data: {
                        state: boolean
                        error?: Error
                        data: TResult
                    }
                    if (xhr.responseText) {
                        try {
                            data = JSON.parse(xhr.responseText)
                        } catch (e) {
                            callback(new Errors.NetworkError("Broken Response"), null)
                            return
                        }
                        if (!data.state) {
                            if (data.error) {
                                callback(this.errorMapper(data.error), null)
                            } else {
                                callback(new Error("APIError"), null)
                            }
                            return
                        } else {
                            callback(null, data.data)
                        }
                    } else {
                        callback(new Errors.NetworkError("Empty response"), null)
                    }
                }
            }
            if (method.toLowerCase() != "get") {
                xhr.send(sendData)
            } else {
                xhr.send()
            }
            return xhr
        }
        public buildQueryString(query) {
            if (!query) {
                return ""
            }
            let kvs = []
            for (let prop in query) {
                let value = query[prop] || ""
                kvs.push([encodeURIComponent(prop), encodeURIComponent(value)].join("="))
            }
            return kvs.join("&")
        }
    }

    export class APIDeclare<TOption, TResult> {
        constructor(private factory: APIFactory, private option: APIOption) {
        }
        public getFunction(): APIFunction<TOption, TResult> {
            let fn: APIFunction<TOption, TResult> = this.invoke.bind(this)
            fn.API = this
            return fn
        }
        public invoke(data: TOption = {} as any, callback: APICallback<TResult>) {
            let method = this.option.method
            let url = this.factory.option.root + this.factory.option.prefix + this.option.path + this.factory.option.suffix
            let routeParams = (url.match(/:[a-z_][a-z0-9_]*/ig) || []).map((item) => { return item.substring(1) })
            if (this.option.data) {
                // default params
                for (let prop in this.option.data) {
                    if (typeof data[prop] == "undefined") {
                        data[prop] = this.option.data[prop]
                    }
                }
            }
            for (let prop in data) {
                if (routeParams.indexOf(prop) >= 0) {
                    url = url.replace(new RegExp(":" + prop, "g"), encodeURIComponent(data[prop] as any as string))
                    delete data[prop]
                }
            }
            let reqOption = {
                url, method,
                query: data,
                timeout: this.option.timeout,
                headers: this.option.headers,
                withCredentials: this.option.withCredentials,
                bodyType: this.option.bodyType
            }
            this.factory.request(reqOption, callback)
        }
    }
}
