minetest.mkdir(minetest.get_worldpath().."/config")
function get_path(confname)
    return minetest.get_worldpath().."/config/"..confname
end
function read_conf(text)
    local lines = modlib.text.split_lines(text, nil, true)
    local dict = {}
    for i, line in ipairs(lines) do
        local error_base = "Line " .. (i+1) .. ": "
        line = modlib.text.trim_left(lines[i])
        if line ~= "" and line:sub(1,1) ~= "#" then
            line = modlib.text.split(line, "=", 2)
            if #line ~= 2 then
                error(error_base .. "No value given")
            end
            local prop = modlib.text.trim_right(line[1])
            if prop == "" then
                error(error_base .. "No key given")
            end
            local val = modlib.text.trim_left(line[2])
            if val == "" then
                error(error_base .. "No value given")
            end
            if modlib.text.starts_with(val, '"""') then
                val = val:sub(3)
                local total_val = {}
                local function readMultiline()
                    while i < #lines do
                        if modlib.text.ends_with(val, '"""') then
                            val = val:sub(1, val:len() - 3)
                            return
                        end
                        table.insert(total_val, val)
                        i = i + 1
                        val = lines[i]
                    end
                    i = i - 1
                    error(error_base .. "Unclosed multiline block")
                end
                readMultiline()
                table.insert(total_val, val)
                val = table.concat(total_val, "\n")
            else
                val = modlib.text.trim_right(val)
            end
            if dict[prop] then
                error(error_base .. "Duplicate key")
            end
            dict[prop] = val
        end
    end
    return dict
end
function build_tree(dict)
    local tree = {}
    for key, value in pairs(dict) do
        local path = modlib.text.split_unlimited(key, ".")
        local subtree = tree
        for i = 1, #path - 1 do
            local index = tonumber(path[i]) or path[i]
            subtree[index] = subtree[index] or {}
            subtree = subtree[index]
        end
        subtree[path[#path]] = value
    end
    return tree
end
function build_setting_tree()
    modlib.conf.settings = build_tree(minetest.settings:to_table())
end
function check_config_constraints(config, constraints, handler)
    local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end)
    if not no_error then
        handler(error_or_retval)
    end
end
function load(filename, constraints)
    local config = minetest.parse_json(modlib.file.read(filename))
    if constraints then
        check_config_constraints(config, constraints, function(message)
            error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
        end)
    end
    return config
end
function load_or_create(filename, replacement_file, constraints)
    modlib.file.create_if_not_exists_from_file(filename, replacement_file)
    return load(filename, constraints)
end
function import(modname, constraints, no_settingtypes)
    local default_config = modlib.mod.get_resource(modname, "default_config.json")
    local default_conf = minetest.parse_json(modlib.file.read(default_config))
    local config = load_or_create(get_path(modname)..".json", default_config, constraints)
    local formats = {
        { extension = ".lua", load = minetest.deserialize },
        { extension = ".luon", load = function(text) minetest.deserialize("return "..text) end },
        { extension = ".conf", load = function(text) return fix_types(build_tree(read_conf(text)), constraints) end }
    }
    for _, format in ipairs(formats) do
        local conf = modlib.file.read(get_path(modname)..format.extension)
        if conf then
            config = merge_config(config, format.load(conf))
        end
    end
    if not no_settingtypes then
        constraints.name = modname
        local settingtypes = generate_settingtypes(default_conf, constraints)
        modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes)
    end
    local additional_settings = modlib.conf.settings[modname] or {}
    additional_settings = fix_types(additional_settings, constraints)
    -- TODO implement merge_config_legal(default_conf, ...)
    config = merge_config(config, additional_settings)
    if constraints then
        check_config_constraints(config, constraints, function(message)
            error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message)
        end)
    end
    return config
end
function merge_config(config, additional_settings)
    if not config or type(additional_settings) ~= "table" then
        return additional_settings
    end
    for setting, value in pairs(additional_settings) do
        if config[setting] then
            config[setting] = merge_config(config[setting], value)
        end
    end
    return config
end
-- format: # comment
-- name (Readable name) type type_args
function generate_settingtypes(default_conf, constraints)
    local constraint_type = constraints.type
    if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then
        constraint_type = "table"
    end
    local settingtype, type_args
    local title = constraints.title
    if not title then
        title = modlib.text.split(constraints.name, "_")
        title[1] = modlib.text.upper_first(title[1])
        title = table.concat(title, " ")
    end
    if constraint_type == "boolean" then
        settingtype = "bool"
        default_conf = default_conf and "true" or "false"
    elseif constraint_type == "string" then
        settingtype = "string"
    elseif constraint_type == "number" then
        settingtype = constraints.int and "int" or "float"
        local range = constraints.range
        if range then
            -- TODO consider better max
            type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or math.pow(2, 30))
        end
    elseif constraint_type == "table" then
        local handled = {}
        local settings = {}
        local function setting(key, value_constraints)
            if handled[key] then
                return
            end
            handled[key] = true
            value_constraints.name = constraints.name .. "." .. key
            value_constraints.title = title .. " " .. key
            table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints))
        end
        for _, table in ipairs{"children", "required_children", "possible_children"} do
            for key, constraints in pairs(constraints[table] or {}) do
                setting(key, constraints)
            end
        end
        return table.concat(settings, "\n")
    end
    if not constraint_type then
        return ""
    end
    local comment = constraints.comment
    if comment then
        comment = "# " .. comment .. "\n"
    else
        comment = ""
    end
    assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf))
    return comment .. constraints.name .. " (" .. title  .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "")
end
function fix_types(value, constraints)
    local type = type(value)
    local expected_type = constraints.type
    if expected_type and expected_type ~= type then
        assert(type == "string", "Can't fix non-string value")
        if expected_type == "boolean" then
            assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value)
            value = value == "true"
        elseif expected_type == "number" then
            assert(tonumber(value), "Not a number: " .. value)
            value = tonumber(value)
        end
    end
    if type == "table" then
        for key, val in pairs(value) do
            for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do
                child_constraints = (constraints[child_constraints] or {})[key]
                if child_constraints then
                    val = fix_types(val, child_constraints)
                end
            end
            if constraints.values then
                val = fix_types(val, constraints.values)
            end
            if constraints.keys then
                value[key] = nil
                value[fix_types(key, constraints.keys)] = val
            else
                value[key] = val
            end
        end
    end
    return value
end
function check_constraints(value, constraints)
    local t = type(value)
    if constraints.type and constraints.type ~= t then
        error("Wrong type: Expected "..constraints.type..", found "..t)
    end
    if (t == "number" or t == "string") and constraints.range then
        if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then
            error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value))
        end
    end
    if t == "number" and constraints.int and value % 1 ~= 0 then
        error("Not an integer number: " .. minetest.write_json(value))
    end
    if constraints.possible_values and not constraints.possible_values[value] then
        error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value))
    end
    if t == "table" then
        if constraints.children then
            for key, val in pairs(value) do
                local child_constraints = constraints.children[key]
                if not child_constraints then
                    error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
                else
                    check_constraints(val, child_constraints)
                end
            end
            for key, _ in pairs(constraints.children) do
                if value[key] == nil then
                    error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
                end
            end
        end
        if constraints.required_children then
            for key, value_constraints in pairs(constraints.required_children) do
                local val = value[key]
                if val then
                    check_constraints(val, value_constraints)
                else
                    error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
                end
            end
        end
        if constraints.possible_children then
            for key, value_constraints in pairs(constraints.possible_children) do
                local val = value[key]
                if val then
                    check_constraints(val, value_constraints)
                end
            end
        end
        if constraints.keys then
            for key,_ in pairs(value) do
                check_constraints(key, constraints.keys)
            end
        end
        if constraints.values then
            for _, val in pairs(value) do
                check_constraints(val, constraints.values)
            end
        end
    end
    if constraints.func then
        local possible_errors = constraints.func(value)
        if possible_errors then
            error(possible_errors)
        end
    end
end