--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      OpportunityLiu
-- @file        todisplay.lua
--

-- define module
local todisplay = todisplay or {}

-- load modules
local colors  = require("base/colors")
local math    = require("base/math")


function todisplay._reset()
    local reset = todisplay._RESET
    if not reset then
        reset = colors.translate("${reset}")
        todisplay._RESET = reset
    end
    return reset
end

-- format string with theme colors
function todisplay._format(fmtkey, fmtdefault, ...)
    local theme = colors.theme()
    if theme then
        return colors.translate(string.format(theme:get(fmtkey), ...), { patch_reset = false, ignore_unknown = true })
    else
        return string.format(fmtdefault, ...)
    end
end

-- translate string with theme formats
function todisplay._translate(str)
    local theme = colors.theme()
    if theme then
        return colors.translate(str, { patch_reset = false, ignore_unknown = true })
    else
        return colors.ignore(str)
    end
end

-- print keyword
function todisplay._print_keyword(keyword)
    return todisplay._translate("${reset}${color.dump.keyword}") .. tostring(keyword) .. todisplay._reset()
end

-- print string
function todisplay._print_string(str, as_key)
    local quote = (not as_key) or (not str:match("^[a-zA-Z_][a-zA-Z0-9_]*$"))
    if quote then
        return todisplay._translate([[${reset}${color.dump.string_quote}"${reset}${color.dump.string}]])
            .. str
            .. todisplay._translate([[${reset}${color.dump.string_quote}"${reset}]])
    else
        return todisplay._translate("${reset}${color.dump.string}") .. str .. todisplay._reset()
    end
end

-- print number
function todisplay._print_number(num)
    return todisplay._translate("${reset}${color.dump.number}") .. tostring(num) .. todisplay._reset()
end

-- print function
function todisplay._print_function(func)
    local funcinfo = debug.getinfo(func)
    local srcinfo = funcinfo.short_src
    if funcinfo.linedefined >= 0 then
        srcinfo = srcinfo .. ":" .. funcinfo.linedefined
    end
    local funcname = funcinfo.name and (funcinfo.name .. " ") or ""
    return todisplay._translate("${reset}${color.dump.function}function ${bright}")
        .. funcname
        .. todisplay._translate("${reset}${dim}")
        .. srcinfo .. todisplay._reset()
end

function todisplay._get_tostr_method(value)
    local metatable = debug.getmetatable(value)
    if metatable then
        local __todisplay = rawget(metatable, "__todisplay")
        local __tostring = rawget(metatable, "__tostring")
        return __todisplay, __tostring
    end
    return nil, nil
end

-- print value with default format
function todisplay._print_default_scalar(value, style, formatkey)
    local __todisplay, __tostring = todisplay._get_tostr_method(value)
    if __todisplay then
        local ok, str = pcall(__todisplay, value)
        if ok then
            value = todisplay._translate(str)
            -- disable format
            formatkey = nil
        end
    elseif __tostring then
        local ok, str = pcall(__tostring, value)
        if ok then
            value = str
        end
    end
    if formatkey then
        value = todisplay._format(formatkey, "%s", value)
    end
    local reset = todisplay._reset()
    return reset .. todisplay._translate(style) .. value .. reset
end

-- print udata value with scalar format
function todisplay._print_udata_scalar(value)
    return todisplay._print_default_scalar(value, "${color.dump.udata}", "text.dump.udata_format")
end

-- print table value with scalar format
function todisplay._print_table_scalar(value, expand)
    if debug.getmetatable(value) or not expand then
        return todisplay._print_default_scalar(value, "${color.dump.table}", "text.dump.table_format")
    end
    local pvalues = {}
    local has_string_key = false
    local as, ae = math.huge, -math.huge

    local k, v
    for i = 1, 10 do
        k, v = next(value, k)
        if k == nil then break end

        if type(k) == "string" then
            pvalues[k] = todisplay._print_scalar(v, false)
            has_string_key = true
        elseif type(k) == "number" and math.isint(k) then
            pvalues[k] = todisplay._print_scalar(v, false)
            as = math.min(k, as)
            ae = math.max(k, ae)
        else
            -- no common table or array
            return todisplay._print_default_scalar(value, "${color.dump.table}", "text.dump.table_format")
        end
    end
    if k ~= nil then
        -- to large
        return todisplay._print_default_scalar(value, "${color.dump.table}", "text.dump.table_format")
    end

    local results = {}
    local is_arr = as >= 1 and ae <= 20
    if is_arr then
        local nilstr
        for i = 1, ae do
            local pv = pvalues[i]
            if pv == nil then
                if not nilstr then
                    nilstr = todisplay._print_keyword("nil")
                end
                pv = nilstr
            end
            results[i] = pv
        end
    end
    if has_string_key then
        for pk, pv in pairs(pvalues) do
            if type(pk) == "string" then
                local pkstr = todisplay._print_string(pk, true)
                table.insert(results, pkstr .. " = " .. pv)
            elseif not is_arr then
                local pkstr = todisplay._print_number(pk)
                table.insert(results, pkstr .. " = " .. pv)
            end
        end
    end
    if #results == 0 then
        return todisplay._translate("${reset}${color.dump.table}") .. "{ }" .. todisplay._reset()
    end
    return todisplay._translate("${reset}${color.dump.table}") .. "{ " .. table.concat(results, ", ") .." }" .. todisplay._reset()
end

-- print scalar value
function todisplay._print_scalar(value, expand)
    if type(value) == "nil" or type(value) == "boolean" then
        return todisplay._print_keyword(value)
    elseif type(value) == "number" then
        return todisplay._print_number(value)
    elseif type(value) == "string" then
        return todisplay._print_string(value)
    elseif type(value) == "function" then
        return todisplay._print_function(value)
    elseif type(value) == "userdata" then
        return todisplay._print_udata_scalar(value)
    elseif type(value) == "table" then
        return todisplay._print_table_scalar(value, expand)
    else
        return todisplay._print_default_scalar(value, "${color.dump.default}", "text.dump.default_format")
    end
end

function todisplay.print(value)
    return todisplay._print_scalar(value, true)
end

setmetatable(todisplay, {
    __call = function (_, ...)
        return todisplay.print(...)
    end
})

return todisplay
