
use utils
use mem

+ fn parse_http(input: utils:ByteBuffer, context: Context, is_response: bool) !invalid !http413 !incomplete !missing_host_header !file_error {

    if context.done : return

    let pos = context.parsed_index
    if pos >= input.length : throw incomplete

    let stage = context.stage
    let data = input.data
    let length = input.length

    // Header
    if stage == 0 {
        let index = input.index_of('\r', pos) ! {
            if length - pos > 4096 : throw http413
            throw incomplete
        }

        // let version = ""

        if is_response {
            // TODO: optimize, dont allocate memory
            let line = input.part(pos, index - pos)
            let split = line.split(" ")
            if split.length < 2 : throw invalid
            // version = split.get(0) ? ""
            let status = split.get(1) !? ""
            context.status = status.to_uint() ! throw invalid

        } else {
            if index + 1 == length : throw incomplete
            if @ptrv(data, u8, index + 1) != '\n' { throw invalid }

            let space1 = input.index_of(' ', pos) ! { throw invalid }
            let space2 = input.index_of(' ', space1 + 1) ! { throw invalid }

            if space1 > index { throw invalid }
            if space2 > index { throw invalid }

            // Request "GET / HTTP/1.1"
            let method = context.method
            method.offset = pos
            method.length = space1 - pos

            // Path
            let path_offset = space1 + 1
            let path_len = space2 - space1 - 1

            let path = context.path
            path.offset = path_offset
            path.length = path_len

            // Query string
            let qmark = path.data.index_of_byte('?', path_len) !? 999999
            if qmark != 999999 {
                let query_len = path_len - qmark
                if query_len > 1 {
                    let qs = context.query_string
                    qs.offset = path_offset + qmark + 1
                    qs.length = query_len - 1
                }
                path.length -= query_len
            }
            // version = input.part(space2 + 1, index - space2 - 1)
        }

        pos = index + 2
        context.parsed_index = pos

        stage = 1
        context.stage = stage
    }

    let hdata = context.header_data
    if hdata.offset == 0 {
        hdata.offset = pos
        hdata.length = 0
    }
    let bdata = context.body_data

    // Headers
    while stage == 1 {
        let index = input.index_of('\r', pos) ! {
            if input.length - pos > 4096 : throw http413
            throw incomplete
        }
        if index + 1 == length : throw incomplete
        if @ptrv(data, u8, index + 1) != '\n' : throw invalid

        if index == pos {
            // End of headers
            pos = index + 2
            context.parsed_index = pos

            stage = 2
            context.stage = stage

            // Host header = required
            if !is_response && !context.has_host : throw missing_host_header

            hdata.length += 2
            bdata.offset = pos
            break
        }

        let key_adr = input.data.offset(pos)
        let header_len = index - pos
        if header_len > 4095 : throw http413

        let headers_length = hdata.length + header_len + 2
        if headers_length > 8192 : throw http413
        hdata.length = headers_length

        // Check certain headers
        let h1 = "content-length"
        let h2 = "transfer-encoding"
        let h3 = "host"
        if header_len > h1.bytes && @ptrv(key_adr, u8, h1.bytes) == ':' {
            mem:ascii_bytes_to_lower(key_adr, h1.bytes)
            if mem:equal(key_adr, h1.data, h1.bytes) {
                let value_adr = key_adr + h1.bytes + 1;
                while @ptrv(value_adr, u8).is_html_spacing() : value_adr += 1
                let value_len = ((input.data.offset(index)) - value_adr).@cast(uint)
                let content_len = mem:bytes_to_uint(value_adr, value_len) ! { throw invalid }
                bdata.length = content_len
                context.content_length = content_len
            }
        } else if header_len > h2.bytes && @ptrv(key_adr, u8, h2.bytes) == ':' {
            mem:ascii_bytes_to_lower(key_adr, h2.bytes)
            if mem:equal(key_adr, h2.data, h2.bytes) {
                let value_adr = key_adr + h2.bytes + 1;
                while @ptrv(value_adr, u8).is_html_spacing() : value_adr += 1
                let v = "chunked"
                if mem:equal(value_adr, v.data, v.bytes) {
                    context.chunked = true
                    bdata.length = 0
                }
            }
        } else if header_len > h3.bytes && @ptrv(key_adr, u8, h3.bytes) == ':' {
            mem:ascii_bytes_to_lower(key_adr, h3.bytes)
            if mem:equal(key_adr, h3.data, h3.bytes) {
                context.has_host = true
            }
        }

        pos = index + 2
        context.parsed_index = pos
    }

    // Body
    if stage == 2 {
        let chunked = context.chunked
        let input_left = length - pos

        if chunked {
            let chunks = context.chunks

            while true {
                // Read length line
                let chlen = bdata.length
                if chlen == 0 {
                    let index = input.index_of('\r', pos) ! {
                        throw incomplete
                    }
                    let line = input.part(pos, index - pos)
                    let len = (line.hex_to_uint() ! throw invalid) + 2

                    pos = index + 2
                    context.parsed_index = pos
                    input_left = length - pos

                    if len == 2 { 
                        // Done
                        pos += 2
                        context.parsed_index += 2
                        break
                    }

                    chlen = len
                    bdata.length = len
                }
                // Read chunk content
                if input_left < chlen : throw incomplete

                let part_len = chlen - 2
                context.body_received += part_len

                // Save to file or append to body buffer
                let file = context.save_to_file
                if isset(file) {
                    file.write_bytes(input.data.offset(pos), part_len) ! throw file_error
                } else {
                    chunks.append_from_ptr(input.data.offset(pos), part_len)
                }

                // Clear input
                input.clear_part(pos, chlen)

                //
                bdata.length = 0
                input_left = length - chlen
            }

        } else {
            // Non chunked
            let content_len = context.content_length

            // Write body to file
            let file = context.save_to_file
            if isset(file) {
                // let body_data = input.data + pos
                let body_bytes = input.length - pos

                context.body_received += body_bytes

                if body_bytes > 0 {
                    file.write_bytes(input.data.offset(pos), body_bytes) ! {
                        throw file_error
                    }
                    input.clear_part(pos, body_bytes)
                }
            }

            if input_left < content_len && context.body_received < content_len {
                throw incomplete
            }

            pos += content_len
            context.parsed_index = pos
        }

        context.stage = 3
        context.done = true
    }
}
