
use utils 
use type 

value type_none (0)
value type_null (1)
value type_string (2)
value type_bool (3)
value type_number (4)
value type_array (5)
value type_object (6)

+ fn decode(json: String) Value !invalid {
    let data = utils:ByteBuffer.new(json.bytes)
    data.append_str(json)
    return Parser.parse(data) ! throw invalid, EMSG
}

class Parser {
    index: type:uint
    data: utils:ByteBuffer
    str_buf: utils:ByteBuffer

    - static fn parse(data: utils:ByteBuffer) Value !invalid {
        let p = Parser{
            index: 0
            data: data
            str_buf: utils:ByteBuffer.new(100)
        }

        let result = p.parse_value() ! throw invalid, EMSG
        p.skip_whitespace()
        if p.index != p.data.length {
            // println("Invalid trailing chars")
            throw invalid, "Trailing character at index: " + p.index
        }
        return result
    }

    - fn parse_value() Value !invalid {
        this.skip_whitespace()
        let ch = this.data.get(this.index++)
        if ch == '{' {
            return this.parse_object() ! throw invalid, EMSG
        }
        if ch == '[' {
            return this.parse_array() ! throw invalid, EMSG
        }
        if ch == '"' {
            let str = this.read_string() ! throw invalid, EMSG
            return Value {
                type: type_string
                string_value: str
            }
        }
        if (ch >= '0' && ch <= '9') || ch == '-' {
            this.index--
            return this.parse_number() ! throw invalid, EMSG
        }
        if ch >= 'a' && ch <= 'z' {
            this.index--
            let word = this.get_word()
            if word == "null" : return Value { type: type_null }
            if word == "true" : return Value { type: type_bool, bool_value: true }
            if word == "false" : return Value { type: type_bool, bool_value: false }
        }
        throw invalid, "Unknown value character '%{ch.to_ascii_string()}' at index: " + this.index
    }

    - fn parse_object() Value !invalid {
        let count = 0
        let values = Map[Value]{}
        while true {
            this.skip_whitespace()
            let ch = this.data.get(this.index++)
            if ch == '}' {
                break
            }
            if count > 0 {
                if ch != ',' : throw invalid, "Missing object comma at index: " + this.index
                this.skip_whitespace()
                ch = this.data.get(this.index++)
            }
            count++
            if ch == '"' {
                let key = this.read_string() ! throw invalid, EMSG
                this.expect(':') ! throw invalid
                let value = this.parse_value() ! throw invalid, EMSG
                values.set(key, value)
                continue
            }
            throw invalid, "Invalid json object syntax at index: " + this.index
        }
        return Value {
            type: type_object
            object_values: values
        }
    }

    - fn parse_array() Value !invalid {
        let values = type:Array[Value]{}
        let count = 0
        while true {
            this.skip_whitespace()
            let ch = this.data.get(this.index++)
            if ch == ']' {
                break
            }
            if count > 0 {
                if ch != ',' : throw invalid, "Missing array comma at index: " + this.index
                this.skip_whitespace()
                ch = this.data.get(this.index++)
            }
            count++
            this.index--
            let v = this.parse_value() ! throw invalid, EMSG
            values.append(v)
        }
        return Value {
            type: type_array
            array_values: values
        }
    }

    - fn parse_number() Value !invalid {
        let buf = this.str_buf
        let data = this.data
        buf.clear()
        let ch : u8 = 0
        let negative = false
        if (data.get(this.index)) == '-' {
            negative = true
            this.index++
        }
        while true {
            ch = data.get(this.index++)
            if ch >= '0' && ch <= '9' {
                buf.append_byte(ch)
                continue
            }
            break
        }

        if ch == '.' {
            // Float
            buf.append_byte('.')
            while true {
                ch = data.get(this.index++)
                if ch >= '0' && ch <= '9' {
                    buf.append_byte(ch)
                    continue
                }
                break
            }

            this.index--
            return Value {
                type: type_number
                float_value: (buf.to_string().to_float() !? 0) * (negative ? -1 : 1)
            }
        }

        // Integer
        this.index--
        return Value {
            type: type_number
            int_value: (buf.to_string().to_int() !? 0) * (negative ? -1 : 1)
        }
    }

    - fn get_word() String {
        let buf = this.str_buf
        let data = this.data
        buf.clear()
        while true {
            let ch = data.get(this.index)
            if ch >= 'a' && ch <= 'z' {
                this.index++
                buf.append_byte(ch)
                continue
            }
            break
        }
        return buf.to_string()
    }

    - fn expect(ch: u8) !invalid {
        this.skip_whitespace()
        let c = this.data.get(this.index++)
        if c != ch : throw invalid, "Expected '%{ch.to_ascii_string()}' at index: " + this.index
    }

    - fn skip_whitespace() {
        let data = this.data
        while true {
            let ch = data.get(this.index)
            if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
                this.index++
                continue
            }
            break
        }
    }

    - fn read_string() String !invalid {
        let buf = this.str_buf
        let data = this.data
        buf.clear()
        while true {
            let ch = data.get(this.index++)
            if ch == '\\' {
                ch = data.get(this.index++)
                if ch == 'b' : ch = '\b'
                else if ch == 'f' : ch = '\f'
                else if ch == 'n' : ch = '\n'
                else if ch == 'r' : ch = '\r'
                else if ch == 't' : ch = '\t'
                else if ch == 0 : throw invalid, "Unexpected EOF at index " + this.index
                buf.append_byte(ch)
                continue
            }
            if ch == '"' : break
            if ch == 0 : throw invalid, "Unexpected EOF at index " + this.index
            buf.append_byte(ch)
        }
        return buf.to_string()
    }
}
