
use utils
use fs
use net

+ class ResponseWriter {
    -[ns+] output: utils:ByteBuffer (utils:ByteBuffer.new(1024)) 
    -[ns+] file_response: ?fs:FileStream 
    ~[ns+] responded: bool (false) 
    - output_pos: uint (0)

    - fn reset() {
        this.output.clear()
        this.output.reduce_size(8192)
        this.output_pos = 0

        let file = this.file_response
        if isset(file) {
            file.close()
            this.file_response = null
        }
    }

    - fn allow_new_response() {
        this.responded = false
    }

    fn send_response(resp: Response) {
        if this.responded : return

        let filepath = resp.filepath
        if isset(filepath) {
            this.send_file(filepath, resp.filename)
            return
        }

        this.respond(resp.status, resp.content_type, resp.body, resp.headers)
    }

    + fn send_status(status_code: uint) {
        this.respond(status_code, "text/plain", "")
    }

    + fn send_file(path: String, custom_filename: ?String (null)) {
        if this.responded : return

        let stream = fs:stream(path, true, false) ! {
            this.respond(404, "text/plain", "", null)
            return
        }
        this.send_file_stream(stream, custom_filename)
    }

    + fn send_file_stream(stream: fs:FileStream, filename: ?String (null)) {
        if this.responded : return
        this.responded = true

        let out = this.output
        let path = stream.path
        let fsize = fs:size(path)
        let ext = fs:ext(path)
        let mime = fs:mime(ext)

        this.file_response = stream

        out.append_str("HTTP/1.1 200 OK\r\nContent-Type: " + mime + "\r\nContent-Length: " + fsize + "\r\n")

        if isset(filename) {
            out.append_str("Content-Disposition: attachment filename=\"" + filename + "\"\r\n")
        }

        out.append_str("\r\n")
    }

    + fn respond(code: uint, content_type: String, body: String, headers: ?Map[String] (null)) {
        if this.responded : return
        this.responded = true

        let buf = this.output

        buf.append_str("HTTP/1.1 ")
        buf.append_uint(code)
        buf.append_str(" OK\r\n")

        buf.append_str("Content-Type: ")
        buf.append_str(content_type)
        buf.append_str("\r\n")

        buf.append_str("Content-Length: ")
        buf.append_uint(body.bytes)
        buf.append_str("\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)
    }

    - fn count_bytes_to_send() uint {
        return this.output.length - this.output_pos
    }

    - fn should_send() bool {
        return this.count_bytes_to_send() > (128 * 1024) || this.file_response != null
    }

    - fn send_bytes(connection: net:Connection) !write {

        let out = this.output
        let file = this.file_response

        while true {
            // Check file stream
            if isset(file) {
                if this.count_bytes_to_send() == 0 {
                    if file.reading {
                        file.read(65535, out) ! throw write
                    }
                    if !file.reading {
                        this.file_response = null
                        file = null
                    }
                }
            }

            let pos = this.output_pos
            let bytes = out.length - pos
            if bytes == 0 : break

            connection.send_bytes(out.data.offset(pos), bytes, true) ! {
                throw write
            }
            this.output_pos += bytes
        }
    }
}
