--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--

--- Wrapped table module.
--
-- @module core.table

local newproxy     = newproxy
local getmetatable = getmetatable
local setmetatable = setmetatable
local select       = select
local tostring     = tostring
local new_tab      = require("table.new")
local nkeys        = require("table.nkeys")
local ipairs       = ipairs
local pairs        = pairs
local type         = type
local ngx_re       = require("ngx.re")
local isarray      = require("table.isarray")


local _M = {
    version = 0.2,
    new     = new_tab,
    clear   = require("table.clear"),
    nkeys   = nkeys,
    insert  = table.insert,
    concat  = table.concat,
    sort    = table.sort,
    clone   = require("table.clone"),
    isarray = require("table.isarray"),
    isempty = require("table.isempty"),
}


setmetatable(_M, {__index = table})


function _M.insert_tail(tab, ...)
    local idx = #tab
    for i = 1, select('#', ...) do
        idx = idx + 1
        tab[idx] = select(i, ...)
    end

    return idx
end


function _M.set(tab, ...)
    for i = 1, select('#', ...) do
        tab[i] = select(i, ...)
    end
end


function _M.try_read_attr(tab, ...)
    local count = select('#', ...)

    for i = 1, count do
        local attr = select(i, ...)
        if type(tab) ~= "table" then
            return nil
        end

        tab = tab[attr]
    end

    return tab
end

---
--  Test if an element exists in an array.
--
-- @function core.table.array_find
-- @tparam table array The tested array.
-- @tparam string val The tested value.
-- @treturn number The index of tested value.
-- @usage
-- local arr = {"a", "b", "c"}
-- local idx = core.table.array_find(arr, "b") -- idx = 2
local function array_find(array, val)
    if type(array) ~= "table" then
        return nil
    end

    for i, v in ipairs(array) do
        if v == val then
            return i
        end
    end

    return nil
end
_M.array_find = array_find


-- only work under lua51 or luajit
function _M.setmt__gc(t, mt)
    local prox = newproxy(true)
    getmetatable(prox).__gc = function() mt.__gc(t) end
    t[prox] = true
    return setmetatable(t, mt)
end


local deepcopy
do
    local function _deepcopy(orig, copied, parent, opts)
        -- If the array-like table contains nil in the middle,
        -- the len might be smaller than the expected.
        -- But it doesn't affect the correctness.
        local len = #orig
        local copy = new_tab(len, nkeys(orig) - len)
        -- prevent infinite loop when a field refers its parent
        copied[orig] = copy
        for orig_key, orig_value in pairs(orig) do
            local path = parent .. "." .. tostring(orig_key)
            if opts and array_find(opts.shallows, path) then
                copy[orig_key] = orig_value
            else
                if type(orig_value) == "table" then
                    if copied[orig_value] then
                        copy[orig_key] = copied[orig_value]
                    else
                        copy[orig_key] = _deepcopy(orig_value, copied, path, opts)
                    end
                else
                    copy[orig_key] = orig_value
                end
            end
        end

        local mt = getmetatable(orig)
        if mt ~= nil then
            setmetatable(copy, mt)
        end

        return copy
    end


    local copied_recorder = {}

    function deepcopy(orig, opts)
        local orig_type = type(orig)
        if orig_type ~= 'table' then
            return orig
        end

        local res = _deepcopy(orig, copied_recorder, "self", opts)
        _M.clear(copied_recorder)
        return res
    end
end
_M.deepcopy = deepcopy


local ngx_null = ngx.null
local function merge(origin, extend)
    for k,v in pairs(extend) do
        if type(v) == "table" then
            if type(origin[k] or false) == "table" then
                if isarray(origin[k]) or isarray(v) then
                    origin[k] = v
                else
                    merge(origin[k] or {}, extend[k] or {})
                end
            else
                origin[k] = v
            end
        elseif v == ngx_null then
            origin[k] = nil
        else
            origin[k] = v
        end
    end

    return origin
end
_M.merge = merge


local function patch(node_value, sub_path, conf)
    local sub_value = node_value
    local sub_paths = ngx_re.split(sub_path, "/")
    for i = 1, #sub_paths - 1 do
        local sub_name = sub_paths[i]
        if sub_value[sub_name] == nil then
            sub_value[sub_name] = {}
        end

        sub_value = sub_value[sub_name]

        if type(sub_value) ~= "table" then
            return 400, "invalid sub-path: /"
                      .. _M.concat(sub_paths, 1, i)
        end
    end

    if type(sub_value) ~= "table" then
        return 400, "invalid sub-path: /" .. sub_path
    end

    local sub_name = sub_paths[#sub_paths]
    if sub_name and sub_name ~= "" then
        sub_value[sub_name] = conf
    else
        node_value = conf
    end

    return nil, nil, node_value
end
_M.patch = patch


-- Compare two tables as if they are sets (only compare the key part)
function _M.set_eq(a, b)
    if nkeys(a) ~= nkeys(b) then
        return false
    end

    for k in pairs(a) do
        if b[k] == nil then
            return false
        end
    end

    return true
end


-- Compare two elements, including their descendants
local function deep_eq(a, b)
    local type_a = type(a)
    local type_b = type(b)

    if type_a ~= 'table' or type_b ~= 'table' then
        return a == b
    end

    local n_a = nkeys(a)
    local n_b = nkeys(b)
    if n_a ~= n_b then
        return false
    end

    for k, v_a in pairs(a) do
        local v_b = b[k]
        local eq = deep_eq(v_a, v_b)
        if not eq then
            return false
        end
    end

    return true
end
_M.deep_eq = deep_eq


-- pick takes the given attributes out of object
function _M.pick(obj, attrs)
    local data = {}
    for k, v in pairs(obj) do
        if attrs[k] ~= nil then
            data[k] = v
        end
    end

    return data
end


return _M
