local dat_obj = {}
local match_key = nil

local function dat_lexer(f, fname)
    local line, err = f:read("*l")
    local location = {line_no = 1, column = 1, fname = fname}
    return function()
        local tok = nil
        while not tok do
            if not line then
                return nil
            end
            pre_space, tok, line = string.match(line, "^(%s*)(..-)([()]*%s.*)")
            if tok and string.match(tok, "^\"") then
                tok, line = string.match(tok..line, "^\"([^\"]-)\"(.*)")
            elseif tok and string.match(tok, "^[()]") then
                line = tok:sub(2) .. line
                tok = tok:sub(1,1)
            end
            location.column = location.column  + #(pre_space or "")
            tok_loc = {
                line_no = location.line_no,
                column = location.column,
                fname = location.fname
            }
            if not line then
                line = f:read("*l")
                location.line_no = location.line_no + 1
                location.column = 1
            else
                location.column = location.column + #tok
            end
        end
        -- print(tok)
        return tok, tok_loc
    end
end

local function dat_parse_table(lexer, start_loc)
    local res = {}
    local state = "key"
    local key = nil
    for tok, loc in lexer do
        if state == "key" then
            if tok == ")" then
                return res
            elseif tok == "(" then
                error(string.format(
                    "%s:%d:%d: fatal error: Unexpected '(' instead of key",
                    loc.fname,
                    loc.line_no,
                    loc.column
                ))
            else
                key = tok
                state = "value"
            end
        else
            if tok == "(" then
                res[key] = dat_parse_table(lexer, loc)
            elseif tok == ")" then
                error(string.format(
                    "%s:%d:%d: fatal error: Unexpected ')' instead of value",
                    loc.fname,
                    loc.line_no,
                    loc.column
                ))
            else
                res[key] = tok
            end
            state = "key"
        end
    end
    error(string.format(
        "%s:%d:%d: fatal error: Missing ')' for '('",
        start_loc.fname,
        start_loc.line_no,
        start_loc.column
    ))
end

local function dat_parser(lexer)
    local res = {}
    local state = "key"
    local key = nil
    local skip = true
    for tok, loc in lexer do
        if state == "key" then
            if tok == "game" then
                skip = false
            end
            state = "value"
        else
            if tok == "(" then
                local v = dat_parse_table(lexer, loc)
                if not skip then
                    table.insert(res, v)
                    skip = true
                end
            else
                error(string.format(
                    "%s:%d:%d: fatal error: Expected '(' found '%s'",
                    loc.fname,
                    loc.line_no,
                    loc.column,
                    tok
                ))
            end
            state = "key"
        end
    end
    return res
end

local function unhex(s)
    if not s then return nil end
    return (s:gsub('..', function (c)
        return string.char(tonumber(c, 16))
    end))
end

local function get_match_key(mk, t)
    for p in string.gmatch(mk, "(%w+)[.]?") do
        if p == nil or t == nil then
            error("Invalid match key '"..mk.."'")
        end
        t = t[p]
    end
    return t
end

table.update = function(a, b)
    for k,v in pairs(b) do
        a[k] = v
    end
end

function init(...)
    local args = {...}
    table.remove(args, 1)
    if #args == 0 then
        assert(dat_path, "dat file argument is missing")
    end

    if #args > 1 then
        match_key = table.remove(args, 1)
    end

    local dat_hash = {}
    for _, dat_path in ipairs(args) do
        local dat_file, err = io.open(dat_path, "r")
        if err then
            error("could not open dat file '" .. dat_path .. "':" .. err)
        end

        print("Parsing dat file '" .. dat_path .. "'...")
        local objs = dat_parser(dat_lexer(dat_file, dat_path))
        dat_file:close()
        for _, obj in pairs(objs) do
            if match_key then
                local mk = get_match_key(match_key, obj)
                if mk == nil then
                    error("missing match key '" .. match_key .. "' in one of the entries")
                end
                if dat_hash[mk] == nil then
                    dat_hash[mk] = {}
                    table.insert(dat_obj, dat_hash[mk])
                end
                table.update(dat_hash[mk], obj)
            else
                table.insert(dat_obj, obj)
            end
        end
    end
end

function get_value()
    local t = table.remove(dat_obj)
    if not t then
        return
    else
        return {
            name = t.name,
            description = t.description,
            rom_name = t.rom.name,
            size = uint(tonumber(t.rom.size)),
            users = uint(tonumber(t.users)),
            releasemonth = uint(tonumber(t.releasemonth)),
            releaseyear = uint(tonumber(t.releaseyear)),
            rumble = uint(tonumber(t.rumble)),
            analog = uint(tonumber(t.analog)),

            famitsu_rating = uint(tonumber(t.famitsu_rating)),
            edge_rating = uint(tonumber(t.edge_rating)),
            edge_issue = uint(tonumber(t.edge_issue)),
            edge_review = t.edge_review,

            enhancement_hw = t.enhancement_hw,
            barcode = t.barcode,
            esrb_rating = t.esrb_rating,
            elspa_rating = t.elspa_rating,
            pegi_rating = t.pegi_rating,
            cero_rating = t.cero_rating,
            franchise   = t.franchise,

            developer = t.developer,
            publisher = t.publisher,
            origin = t.origin,

            crc = binary(unhex(t.rom.crc)),
            md5 = binary(unhex(t.rom.md5)),
            sha1 = binary(unhex(t.rom.sha1)),
            serial = binary(t.serial or t.rom.serial),
        }
    end
end
