
use fs
use url
use json

trait ContextParser {

    -[ns+] header_map: ?Map[String]
    -[ns+] params_map: ?Map[String]
    -[ns+] params_grp: ?Map[Array[String]]
    -[ns+] data_map: ?Map[String]
    -[ns+] files_map: ?Map[fs:InMemoryFile]

    fn reset_cache() {
        this.header_map = null
        this.params_map = null
        this.params_map = null
        this.data_map = null
        this.files_map = null
    }

    + fn headers() Map[String] {
        let headers = this.header_map
        if isset(headers) : return headers
        let new = Map[String].new()
        this.parse_headers(this.header_data.data, this.header_data.length, new) _
        this.header_map = new
        return new
    }

    + fn params() Map[String] {
        let params = this.params_map
        if isset(params) : return params
        let new = Map[String].new()
        let data : String = this.query_string
        if data.bytes > 0 {
            let parts = data.split("&")
            each parts as part {
                let k, v = part.split_on_first_occurance_of_byte('=') ! continue
                new.set(k, url:decode(v))
            }
        }
        this.params_map = new
        return new
    }

    + fn params_grouped() Map[Array[String]] {
        let params = this.params_grp
        if isset(params) : return params
        let new = Map[Array[String]].new()
        let data : String = this.query_string
        if data.bytes > 0 {
            let parts = data.split("&")
            each parts as part {
                let k, v = part.split_on_first_occurance_of_byte('=') ! continue
                let arr = new.get(k) ! {
                    let g = Array[String].new()
                    g.append(url:decode(v))
                    new.set(k, g)
                    // new.set(k, Array[String]{ url:decode(v) })
                    continue
                }
                arr.append(url:decode(v))
            }
        }
        this.params_grp = new
        return new
    }

    + fn data() Map[String] {
        let data = this.data_map
        if isset(data) : return data
        let new = Map[String].new()
        let files = Map[fs:InMemoryFile]{}
        let headers = this.headers()
        let type = headers.get("content-type") !? ""

        if type == "application/x-www-form-urlencoded" {
            this.data_urlencoded(new)
        } else if type.starts_with("multipart/form-data") {
            this.data_multipart(new, files, type.ltrim("multipart/form-data boundary="))
        } else if type == "application/json" {
            this.data_json(new)
        }
        this.data_map = new
        this.files_map = files
        return new
    }

    + fn files() Map[fs:InMemoryFile] {
        let files = this.files_map
        if isset(files) : return files
        this.data()
        return this.files()
    }

    - fn data_urlencoded(result: Map[String]) {
        let parts = this.body.split("&")
        each parts as part {
            let k, v = part.split_on_first_occurance_of_byte('=') ! continue
            result.set(k, url:decode(v))
        }
    }
    - fn data_multipart(result: Map[String], files: Map[fs:InMemoryFile], boundary: String) {
        // Content-Type: multipart/form-data boundary=boundary
        // Body example:
        // --boundary
        // Content-Disposition: form-data name="name"
            
        // John
        // --boundary
        // Content-Disposition: form-data name="avatar" filename="avatar.jpg"
        // Content-Type: image/jpeg
            
        // <some binary data>
        // --boundary--
        let headers = Map[String].new()
        let dispos = Map[String].new()
        let body = this.body
        let parts = body.split("--" + boundary)
        each parts as part, i {
            if i == 0 : continue
            let ch = part.get(0)
            if !ch.is_newline() : break
            ch = part.get(1)
            if !ch.is_newline() : break
            // Parse headers
            headers.clear()
            dispos.clear()
            let part_data = part.data
            let header_bytes = this.parse_headers(part_data, part.bytes, headers) ! continue
            // Content disposition
            let dispo = headers.get("content-disposition") ! continue
            this.parse_content_disposition(dispo, dispos)
            let name = dispos.get("name") ! continue
            let fn : ?String = dispos.get("filename") !? null
            // 
            let body_offset = header_bytes + 2
            if body_offset > part.bytes - 2 : continue
            let body_bytes = part.bytes - body_offset - 2

            if isset(fn) {
                let file = fs:InMemoryFile.create_from_ptr(part_data + body_offset, body_bytes)
                file.filename = fn
                files.set(name, file)
                file.mime_type = headers.get("content-type") ! continue
            } else {
                let v = part.part(body_offset, body_bytes)
                result.set(name, v)
            }
        }
    }

    - fn parse_content_disposition(value: String, result: Map[String]) {
        // form-data name="avatar" filename="avatar.jpg"
        let parts = value.split("")
        each parts as part {
            let k, v = part.split_on_first_occurance_of_byte('=') ! continue
            v = v.trim(" ").trim("\"").unescape()
            result.set(k.trim(" "), v)
        }
    }

    - fn data_json(result: Map[String]) {
        let data = json:decode(this.body) ! return
        each data.map() as v, k {
            let str = v.encode()
            result.set(k, str.part(1, str.length - 2))
        }
    }

    - fn parse_headers(data: ptr, length: uint, result: Map[String]) uint !invalid {

        let pos : uint = 0

        while true {
            let index = data.offset(pos).index_of_byte('\r', length - pos) ! throw invalid
            index += pos
            if index + 1 >= length : throw invalid
            if @ptrv(data, u8, index + 1) != '\n' : throw invalid

            // End of headers
            if index == pos : break

            let split = data.offset(pos).index_of_byte(':', index - pos) ! throw invalid
            split += pos

            let value_pos = split + 1
            while @ptrv(data, u8, value_pos) == ' ' : value_pos++

            let key = data.offset(pos).create_string(split - pos)
            let value = data.offset(value_pos).create_string(index - value_pos)
            result.set(key.lower(), value)

            pos = index + 2
        }

        return pos
    }
}