
use utils
use json
use fs

class Parser {
    parent: ?Parser (null)
    //
    path: ?String (null)
    content: String
    i: uint (0)
    line: uint (1)
    col: uint (1)
    spaces: uint (0)
    //
    data: json:Value
    scope: ?Scope (null)
    options: ?RenderOptions (null)
    token: utils:ByteBuffer (.new())
    result: utils:ByteBuffer (.new())
    //
    // ignore: bool (false)

    fn render() String {
        let result = this.result
        let token = this.token
        let options = this.options
        let data = this.data
        let len = this.content.length
        let escaped = false
        let ignore = false
        let ignore_depth = 1

        while this.i < len {
            this.tok(true)

            // Add spaces
            let spaces = this.spaces
            while spaces-- > 0 && !ignore : result.append_byte(' ')
            //
            if this.token_is("\\") && !escaped {
                escaped = true
                continue
            }

            // Ignore
            if ignore {
                let con = true
                if !escaped {
                    if this.token_is("@each") : ignore_depth++
                    else if this.token_is("@if") : ignore_depth++
                    else if this.token_is("@end") : ignore_depth--
                    else if this.token_is("@else") || this.token_is("@elif") {
                        let scope = this.scope
                        if ignore_depth == 1 && isset(scope) && scope.type == scope_type_if : ignore_depth--
                    }
                    if ignore_depth == 0 {
                        ignore = false
                        ignore_depth = 1
                        con = false
                    } else if ignore_depth < 0 {
                        return this.error("Unexpected token: '%token'")
                    }
                }
                escaped = false
                if con : continue
            }
            // Handle token
            if !escaped {
                if this.token_is("@include") {
                    this.expect("(", false) ! return EMSG
                    this.expect("\"", false) ! return EMSG
                    let str = this.read_str() ! return EMSG

                    let dir : ?String = null
                    if isset(options) : dir = options.template_directory
                    if !isset(dir) : return this.error("Using @include without passing a template-directory path")

                    let sub_path = fs:add(dir, str)
                    let sub_content = fs:read(sub_path) ! return this.error("Template not found: " + sub_path)
                    let sub = Parser{
                        parent: this
                        path: sub_path
                        content: sub_content
                        data: data
                        options: options
                    }
                    result.append_str(sub.render())

                    this.expect(")", true) ! return EMSG
                    continue
                }
                if this.token_is("@each") {
                    this.expect("(", false) ! return EMSG
                    let val = this.read_value() ! return EMSG
                    if !val.is_array() && !val.is_object() : return this.error("@each value must be an array or object")
                    this.expect("as", true) ! return EMSG
                    let vname = this.read_var_name() ! return EMSG
                    let kname : ?String = null
                    let iname : ?String = null
                    if this.next_is(",", true) {
                        kname = this.read_var_name() ! return EMSG
                        if this.next_is(",", true) {
                            iname = this.read_var_name() ! return EMSG
                        }
                    }

                    let list : ?Array[json:Value] = null
                    if val.is_array() {
                        list = val.array_values
                    } else if val.is_object() {
                        let obvs = val.object_values
                        if isset(obvs) {
                            list = obvs.values()
                        }
                    }
                    if !isset(list) : return this.error("Missing array/object values")

                    let scope = this.new_scope(scope_type_each)
                    this.scope = scope
                    scope.vname = vname
                    scope.kname = kname
                    scope.iname = iname
                    scope.list = list

                    if val.is_object() {
                        let vals = val.object_values
                        if isset(vals) : scope.list_keys = vals.keys()
                    }

                    scope.set_list_index(0) ! {
                        ignore = true
                    }

                    this.expect(")", true) ! return EMSG
                    scope.start = this.i
                    scope.start_line = this.line
                    scope.start_col = this.col
                    continue
                }
                if this.token_is("@if") {
                    this.expect("(", false) ! return EMSG
                    let val = this.read_value() ! return EMSG
                    this.expect(")", true) ! return EMSG

                    let scope = this.new_scope(scope_type_if)
                    this.scope = scope

                    if this.value_is_true(val) {
                        scope.if_is_handled = true
                    } else{
                        ignore = true
                    }
                    continue
                }
                if this.token_is("@end") {
                    let scope = this.scope
                    if !isset(scope) : return this.error("Unexpected token: '@end'")
                    if scope.type == scope_type_each {
                        let index = scope.index
                        let has_item = true
                        scope.set_list_index(index + 1) ! {
                            has_item = false
                            this.scope = scope.parent
                        }
                        if has_item {
                            this.i = scope.start
                            this.line = scope.start_line
                            this.col = scope.start_col
                        }
                    } else if scope.type == scope_type_if {
                        this.scope = scope.parent
                    }
                    continue
                }
                if this.token_is("@else") {
                    let scope = this.scope
                    if !isset(scope) : return this.error("Using @else without an '@if' token before it")
                    if scope.type != scope_type_if : return this.error("Using @else without an '@if' token before it")

                    if scope.if_is_handled {
                        ignore = true
                    } else {
                        scope.if_is_handled = true
                    }
                    continue
                }
                if this.token_is("@elif") {
                    let scope = this.scope
                    if !isset(scope) : return this.error("Using @elif without an '@if' token before it")
                    if scope.type != scope_type_if : return this.error("Using @elif without an '@if' token before it")

                    this.expect("(", false) ! return EMSG
                    let val = this.read_value() ! return EMSG
                    this.expect(")", true) ! return EMSG

                    if !scope.if_is_handled && this.value_is_true(val) {
                        scope.if_is_handled = true
                    } else{
                        ignore = true
                    }
                    continue
                }
                if this.token_is("{{") || this.token_is("{!") {
                    let raw = this.token_is("{!")
                    let val = this.read_value() ! return EMSG
                    let str = val.to_string()
                    // Sanitize
                    if !raw {
                        if isset(options) {
                            let san = options.sanitize
                            if isset(san) : str = san(str)
                        }
                    }
                    //
                    result.append_str(str)
                    //
                    if raw : this.expect("!}", true, true) ! return EMSG
                    else : this.expect("}}", true, true) ! return EMSG
                    continue
                }
            }


            //
            if escaped {
                escaped = false
                if token.get(0) != '@' : result.append_byte('\\')
            }
            result.append(token)
        }

        return result
    }

    fn new_scope(type: int) Scope {
        return Scope {
            type: type
            parent: this.scope
            start: this.i
            start_line: this.line
            start_col: this.col
        }
    }

    fn value_is_true(val: json:Value) bool {
        if val.is_bool() : return val.bool_value
        if val.is_number() : return val.int_value > 0 || val.float_value > 0
        if val.is_array() {
            let list = val.array_values
            if isset(list) : return list.length > 0
        }
        if val.is_object() {
            let list = val.object_values
            if isset(list) : return list.length > 0
        }
        return !val.is_null()
    }

    fn read_value(prio: int (999)) json:Value !invalid {
        let data = this.data
        let token = this.token
        let val : ?json:Value = null

        this.tok(true, true)
        // @tokens
        if token.equals_str("@length") {
            this.expect("(", false) ! throw invalid, EMSG
            let of = this.read_value() !>
            this.expect(")", true) ! throw invalid, EMSG
            val = json:new_uint(of.length())
        }
        // Numbers
        if !isset(val) {
            if token.get(0).is_number() {
                let num : String = token
                val = json:new_int(num.to_int() !? 0)
            }
        }

        // Variables
        if !isset(val) {
            let name : String = token
            let scope = this.scope
            while isset(scope) {
                let sd = scope.data
                if isset(sd) {
                    let sv = sd.get(name) !? null
                    if isset(sv) {
                        val = sv
                        break
                    }
                }
                scope = scope.parent
            }
            if !isset(val) {
                if !data.has(name) : throw invalid, this.error("Data not found: '%name'")
                val = data.get(name)
            }
        }

        while true {
            if !this.next_is(".", false) : break
            this.tok(false, true)
            let name : String = token
            if !val.has(name) : throw invalid, this.error("Property not found: '%name'")
            val = val.get(name)
        }

        if prio > 1 {
            while true {
                this.tok(true, false)
                if token.equals_str("==") || token.equals_str("!=") || token.equals_str(">=") || token.equals_str("<=") || token.equals_str(">") || token.equals_str("<") {
                    this.tok(true, true)
                    let op = "=="
                    if token.equals_str("!=") : op = "!="
                    else if token.equals_str(">=") : op = ">="
                    else if token.equals_str("<=") : op = "<="
                    else if token.equals_str(">") : op = ">"
                    else if token.equals_str("<") : op = "<"

                    let right = this.read_value(1) !>
                    if val.is_number() && right.is_number() {
                        let n1 : float = val.int_value
                        let n2 : float = right.int_value
                        if val.float_value > 0 : n1 = val.float_value
                        if right.float_value > 0 : n2 = right.float_value
                        let res = false
                        if  op == "==" : res = n1 == n2
                        else if op == "!=" : res = n1 != n2
                        else if op == ">=" : res = n1 >= n2
                        else if op == "<=" : res = n1 <= n2
                        else if op == ">" : res = n1 > n2
                        else if op == "<" : res = n1 < n2
                        val = json:new_bool(res)
                    } else {
                        val = json:new_bool(false)
                    }
                    continue
                }
                break
            }
        }

        return val
    }

    fn read_var_name() String !invalid {
        let data = this.data
        let token = this.token
        this.tok(true, true)
        let name : String = token
        if !name.get(0).is_alpha() : throw invalid, this.error("Invalid variable name syntax: '%name' ([a-zA-Z0-9_])")
        return name
    }

    fn error(msg: String) String {
        return "[ Error at line " + this.line + " in " + (this.path ?? "{content}") + " | Error: " + msg + " ]"
    }

    fn next_is(tok: String, allow_space: bool) bool {
        this.tok(allow_space, false)
        if !this.token_is(tok) : return false
        this.tok(allow_space, true)
        return true
    }

    fn expect(tok: String, allow_space: bool, update: bool (true)) !notfound {
        this.tok(allow_space, update)
        if !this.token_is(tok) : throw notfound, this.error("Expected '%{tok}' instead of '%{this.token}'")
    }

    fn read_str() String !missing_end {
        let token = this.token
        token.clear()

        let i = this.i
        let col = this.col
        let line = this.line

        let content = this.content
        let bytes = content.bytes
        while i < bytes{
            let ch = content.get(i)
            i++
            col++
            if ch == '\n' {
                line++
                col = 1
            } else if ch == '"' {
                this.i = i
                this.col = col
                this.line = line
                return token
            }
            token.append_byte(ch)
        }
        throw missing_end, this.error("Missing double-quote")
    }

    fn tok(allow_space: bool, update: bool (true)) {
        let token = this.token
        token.clear()

        let i = this.i
        let col = this.col
        let line = this.line
        let spaces = 0

        let content = this.content
        let bytes = content.bytes
        while i < bytes{
            let ch = content.get(i)
            i++
            col++
            if allow_space && ch == ' ' {
                spaces++
                continue
            }
            allow_space = false
            // Variables
            if ch.is_alpha() || ch == '_' || ch == '@' {
                token.append_byte(ch)
                let nch = content.get(i)
                while nch.is_alpha() || nch == '_' || nch.is_number() {
                    i++
                    col++
                    token.append_byte(nch)
                    nch = content.get(i)
                }
                break
            }
            // Numbers
            if ch.is_number() {
                token.append_byte(ch)
                let nch = content.get(i)
                while nch.is_number() {
                    i++
                    col++
                    token.append_byte(nch)
                    nch = content.get(i)
                }
                break
            }

            token.append_byte(ch)
            if ch == '\n' {
                line++
                col = 1
            }
            //
            let nch = content.get(i)
            if
            (ch == '=' && nch == '=')
            || (ch == '!' && (nch == '=' || nch == '}')) // != !}
            || (ch == '{' && (nch == '{' || nch == '!')) // {{ {!
            || (ch == '}' && nch == '}')
            || (ch == '<' && nch == '=')
            || (ch == '>' && nch == '=')
            {
                i++
                col++
                token.append_byte(nch)
            }

            break
        }

        this.spaces = spaces
        if update {
            this.i = i
            this.col = col
            this.line = line
        }
    }

    fn token_is(tok: String) bool {
        return this.token.equals_str(tok)
    }
}