
use utils
use net
use fs
use url

+ class ClientRequest {
    ~ recv_buffer: utils:ByteBuffer
    - ctx: Context
    - con: net:Connection
    - payload: utils:ByteBuffer
    //
    - output_file: ?fs:FileStream
    // Progress
    - stage: uint (0)
    ~ bytes_to_send: uint (0)
    ~ bytes_sent: uint (0)
    ~ bytes_to_recv: uint (0)
    ~ bytes_received: uint (0)
    ~ sent_percent: uint (0)
    ~ recv_percent: uint (0)
    ~ request_sent: bool (false)
    ~ response_received: bool (false)
    // Final
    - resp: ?ClientResponse

    //////////////////////////////
    // Request initialization
    //////////////////////////////

    + static fn create(method: String, url: String, options: ?Options (null)) ClientRequest !invalid_url !connection_failed !ssl !invalid_output_path {

        // Validate URL
        let u = url:parse(url)
        let is_https = u.scheme == "https"

        let split = u.host.split(":")
        let host = u.host
        let port : u16 = is_https ? 443 : 80
        if split.length > 2 : throw invalid_url
        if split.length == 2 {
            host = split.get(0) !? ""
            let p = split.get(1) ! throw invalid_url
            port = (p.to_uint() ! throw invalid_url).@cast(u16)
        }

        // Connect
        let sock = net:Socket.new_tcp(host, port) ! throw connection_failed, "Failed to create socket"
        let con = sock.connect() ! throw connection_failed, "Failed to connect"

        if is_https {
            con.ssl_connect(host, net:ca_cert) ! {
                con.close()
                throw ssl, EMSG
            }
        }

        // Request
        let recv_buffer = utils:ByteBuffer.new(2048)
        let ctx = Context.new(recv_buffer)

        // Options for context
        if isset(options) {
            let out_path = options.output_to_file
            if isset(out_path) {
                let file = fs:stream(out_path, true, true, false, true) ! throw invalid_output_path, "Failed to open file: " + out_path
                ctx.save_to_file = file
            }
        }

        //
        let payload = SELF.create_payload(method, u, options)

        //
        return SELF {
            recv_buffer: recv_buffer
            ctx: ctx
            con: con
            payload: payload
            bytes_to_send: payload.length
        }
    }

    - static fn create_payload(method: String, url: url:Url, options: ?Options) utils:ByteBuffer {

        let body = ""
        let headers : ?Map[String] = null
        let has_custom_agent = false

        if isset(options) {
            body = options.body
            headers = options.headers
            has_custom_agent = isset(headers) && (headers.has("user-agent") || headers.has("User-Agent"))
            let qdata = options.query_data
            if isset(qdata) {
                let qstr = ""
                each qdata as v, k, i {
                    if i > 0 : qstr += "&"
                    qstr += "%{url:encode(k)}=%{url:encode(v)}"
                }
                url.query += url.query.length > 0 ? "&" + qstr : "?" + qstr 
            }
        }


        let len = body.bytes
        let buf = utils:ByteBuffer.new(len + 256)

        buf.append_str(method)
        buf.append_str(" ")
        buf.append_str(url.path)
        if !url.query.is_empty() {
            buf.append_str("?")
            buf.append_str(url.query)
        }
        buf.append_str(" HTTP/1.1\r\n")

        buf.append_str("Host: ")
        buf.append_str(url.host)
        buf.append_str("\r\n")

        buf.append_str("Content-Length: ")
        buf.append_str(body.bytes)
        buf.append_str("\r\n")

        if !has_custom_agent {
            buf.append_str("User-Agent: Valk http client\r\n")
        }

        if isset(headers) {
            each headers as v, k {
                buf.append_str(k)
                buf.append_str(": ")
                buf.append_str(v)
                buf.append_str("\r\n")
            }
        }

        buf.append_str("\r\n")
        buf.append_str(body)

        return buf
    }

    //////////////////////////////
    // Run / Progress
    //////////////////////////////

    - fn stop() {
        this.con.close()
        this.ctx.reset()
        this.request_sent = true
        this.response_received = true
        // Keep input for debugging
        // this.recv_buffer.clear()
        // this.recv_buffer.reduce_size(2048)
    }

    + fn progress() bool !disconnect !invalid_response {
        
        if this.response_received : return false

        let con = this.con

        if this.stage == 0 {
            // Send request
            let sent_count = con.send_buffer(this.payload, this.bytes_sent, false) ! {
                this.stop()
                throw disconnect, "Unexpected disconnect during request"
            }
            this.bytes_sent += sent_count
            this.sent_percent = this.bytes_sent * 100 / this.bytes_to_send
            if this.bytes_sent < this.bytes_to_send : return true
            // Done
            this.stage = 1
            this.sent_percent = 100
        }

        //
        this.request_sent = true

        // Receive response
        let context = this.ctx
        let recv_buf = this.recv_buffer
        if !context.done {
            let bytes = con.recv(recv_buf, 1024 * 32) ! {
                this.stop()
                throw invalid_response, EMSG
            }
            this.bytes_received += bytes
            if context.content_length > 0 {
                this.recv_percent = this.bytes_received * 100 / context.content_length
            }

            if bytes == 0 {
                this.stop()
                throw invalid_response, "Empty packet"
            }
            // Parse bytes
            parse_http(recv_buf, context, true) ! {
                // println(recv_buf)
                if error_is(E, incomplete) : return true
                this.stop()
                throw invalid_response, "Invalid HTTP syntax"
            }

            this.recv_percent = 100
        }

        // Create response
        let resp = ClientResponse {
            status: context.status
            headers: context.headers()
            body: context.body
        }

        this.stop()
        this.resp = resp

        return false
    }

    + fn response() ClientResponse !in_progress !invalid_response {
        if !this.response_received : throw in_progress
        return this.resp ?! throw invalid_response
    }
}
