-- -- https://github.com/ayongm2/TableMonitor
-- -- https://github.com/jojo59516/traceable
-- local next = next
-- local pairs = pairs
-- local ipairs = ipairs
-- local unpack = table.unpack
-- local load = load
-- local setmetatable = setmetatable
-- local assert = assert
-- local getmetatable = getmetatable
-- local type = type
-- local tonumber = tonumber
-- -----------------------------------------------------
-- local traceable = {}
-- local _M = traceable
-- local function merge(dst, src)
--     for k, v in next, src do
--         dst[k] = v
--     end
--     return dst
-- end
-- local function normalize_field_path(field_path)
--     return field_path:gsub("^([-?%d]+)", "[%1]"):gsub("%.([-?%d]+)", "[%1]"):gsub("^([^.%[%]]+)", "['%1']"):gsub(
--         "%.([^.%[%]]+)",
--         "['%1']"
--     )
-- end
-- local getter_template =
--     [[
--     return function(t)
--         local success, value = pcall(function (t)
--             return t%s
--         end, t)
--         if success then
--             return value
--         end
--     end
-- ]]
-- local setter_template =
--     [[
--     return function(t, v)
--         local success, err = pcall(function (t, v)
--             t%s = v
--         end, t, v)
--     end
-- ]]
-- local getter_cache = {}
-- local setter_cache = {}
-- local function compile_normalized_property(field_path)
--     local getter, setter = getter_cache[field_path], setter_cache[field_path]
--     if not getter then
--         getter = assert(load(getter_template:format(field_path)))()
--         getter_cache[field_path] = getter
--         setter = assert(load(setter_template:format(field_path)))()
--         setter_cache[field_path] = setter
--     end
--     return getter, setter
-- end
-- function _M.compile_property(field_path)
--     return compile_normalized_property(normalize_field_path(field_path))
-- end
-- local set
-- local function create()
--     local o = {
--         dirty = false,
--         ignored = false,
--         opaque = false,
--         _stage = {},
--         _traced = {},
--         _lastversion = {},
--         _parent = false
--     }
--     return setmetatable(
--         o,
--         {
--             __index = o._stage,
--             __newindex = set,
--             __len = _M.len,
--             __pairs = _M.pairs,
--             __ipairs = _M.ipairs
--         }
--     )
-- end
-- local function mark_dirty(t)
--     while t and not t.dirty do
--         t.dirty = true
--         t = t._parent
--     end
-- end
-- set = function(t, k, v, force)
--     local stage = t._stage
--     local u = stage[k]
--     if _M.is_traceable(v) then
--         if _M.is(u) then
--             for _ in next, u._stage do
--                 if v[k] == nil then
--                     u[k] = nil
--                 end
--             end
--         else
--             u = create()
--             u._parent = t
--             stage[k] = u
--         end
--         merge(u, v)
--     else
--         local traced = t._traced
--         local lastverison = t._lastversion
--         if stage[k] ~= v or force then
--             if not traced[k] then
--                 traced[k] = true
--                 lastverison[k] = u
--             end
--             stage[k] = v
--             mark_dirty(t)
--         end
--     end
-- end
-- function _M.is(t)
--     local mt = getmetatable(t)
--     return type(mt) == "table" and mt.__newindex == set
-- end
-- function _M.is_traceable(t)
--     if type(t) ~= "table" then
--         return false
--     end
--     local mt = getmetatable(t)
--     return mt == nil or mt.__newindex == set
-- end
-- function _M.new(t)
--     local o = create()
--     if t then
--         merge(o, t)
--     end
--     return o
-- end
-- function _M.mark_changed(t, k)
--     local v = t[k]
--     if _M.is(v) then
--         mark_dirty(v)
--     else
--         set(t, k, v, true)
--     end
-- end
-- function _M.set_ignored(t, ignored)
--     t.ignored = ignored
--     if not ignored and t.dirty then
--         mark_dirty(t._parent)
--     end
-- end
-- function _M.set_opaque(t, opaque)
--     t.opaque = opaque
-- end
-- local k_stub =
--     setmetatable(
--     {},
--     {
--         __tostring = function()
--             return "k_stub"
--         end
--     }
-- )
-- function _M.compile_map(src)
--     local o = {}
--     for i = 1, #src do
--         local v = src[i]
--         local argc = #v
--         -- v = {argc, action, arg1, arg2, ..., argn}
--         v = {argc, unpack(v)} -- copy
--         for j = 1, argc - 1 do
--             local k = j + 2
--             local arg = normalize_field_path(v[k])
--             v[k] = compile_normalized_property(arg)
--             local p, c = nil, o
--             for field in arg:gmatch('([^%[%]\'"]+)') do
--                 field = tonumber(field) or field
--                 p, c = c, c[field]
--                 if not c then
--                     c = {}
--                     p[field] = c
--                 end
--             end
--             local stub = c[k_stub]
--             if not stub then
--                 stub = {[1] = 1}
--                 c[k_stub] = stub
--             end
--             local n = stub[1] + 1
--             stub[1] = n
--             stub[n] = v
--         end
--     end
--     return o
-- end
-- local function get_map_stub(map, k)
--     local map_k = map[k]
--     return map_k and map_k[k_stub]
-- end
-- local argv = {}
-- local function do_map(t, mappings, mapped)
--     for i = 2, mappings[1] do
--         local mapping = mappings[i]
--         if not mapped[mapping] then
--             mapped[mapping] = true
--             local argc = mapping[1]
--             argv[1] = t
--             for i = 2, argc do
--                 argv[i] = mapping[i + 1](t)
--             end
--             local action = mapping[2]
--             action(unpack(argv, 1, argc))
--         end
--     end
-- end
-- local function next_traced(t, k)
--     local k = next(t._traced, k)
--     return k, t._stage[k], t._lastversion[k]
-- end
-- local function _commit(keep_dirty, t, sub, changed, newestversion, lastversion, map, mapped)
--     if not sub.dirty then
--         return false
--     end
--     local traced = sub._traced
--     local trace = sub._lastversion
--     local has_changed = next(traced) ~= nil
--     for k, v in next, sub._stage do
--         if _M.is(v) and not v.ignored then
--             if v.opaque then
--                 has_changed = _commit(keep_dirty, t, v)
--                 if has_changed then
--                     if changed then
--                         changed[k] = true
--                     end
--                 end
--             else
--                 local c, n, l = changed and {}, newestversion and {}, lastversion and {}
--                 has_changed = _commit(keep_dirty, t, v, c, n, l, map and map[k], mapped)
--                 if has_changed then
--                     if changed then
--                         changed[k] = c
--                     end
--                     if newestversion then
--                         newestversion[k] = n
--                     end
--                     if lastversion then
--                         lastversion[k] = l
--                     end
--                 end
--             end
--             if has_changed and map then
--                 local stub = get_map_stub(map, k)
--                 if stub then
--                     do_map(t, stub, mapped)
--                 end
--             end
--         end
--     end
--     for k, v, u in next_traced, sub do
--         if not keep_dirty then
--             traced[k] = nil
--             trace[k] = nil
--         end
--         if changed then
--             changed[k] = true
--         end
--         if newestversion then
--             newestversion[k] = v
--         end
--         if lastversion then
--             lastversion[k] = u
--         end
--         if map then
--             local stub = get_map_stub(map, k)
--             if stub then
--                 do_map(t, stub, mapped)
--             end
--         end
--     end
--     if not keep_dirty then
--         sub.dirty = false
--     end
--     return has_changed
-- end
-- local flag_map = {
--     [("k"):byte()] = "changed",
--     [("v"):byte()] = "newestversion",
--     [("u"):byte()] = "lastversion"
-- }
-- local function commit(t, map, diff_flags, keep_dirty)
--     local has_changed, diff
--     if diff_flags then
--         diff = {}
--         for i = 1, #diff_flags do
--             local field_name = flag_map[diff_flags:byte(i)]
--             if field_name then
--                 diff[field_name] = {}
--             end
--         end
--         has_changed = _commit(keep_dirty, t, t, diff.changed, diff.newestversion, diff.lastversion, map, map and {})
--     else
--         has_changed = _commit(keep_dirty, t, t, nil, nil, nil, map, map and {})
--     end
--     return has_changed, diff
-- end
-- function _M.commit(t, map, diff_flags)
--     if type(map) == "string" then
--         map, diff_flags = nil, map
--     end
--     return commit(t, map, diff_flags, false)
-- end
-- function _M.diff(t, map, diff_flags)
--     if type(map) == "string" then
--         map, diff_flags = nil, map
--     end
--     return commit(t, map, diff_flags, true)
-- end
-- local function do_maps(t, sub, map, mapped)
--     for k, map_k in next, map do
--         local stub = map_k[k_stub]
--         if stub then
--             do_map(t, stub, mapped)
--         end
--         local v = sub[k]
--         if type(v) == "table" then
--             do_maps(t, v, map_k, mapped)
--         end
--     end
-- end
-- function _M.map(t, map)
--     do_maps(t, t, map, map and {})
-- end
-- local function forward_to(field, func)
--     return function(t, ...)
--         return func(t[field], ...)
--     end
-- end
-- function _M.len(t)
--     return #(t._stage)
-- end
-- _M.next = forward_to("_stage", next)
-- _M.pairs = forward_to("_stage", pairs)
-- _M.ipairs = forward_to("_stage", ipairs)
-- _M.unpack = forward_to("_stage", unpack)
-- return _M
-- -- local traceable = require "traceable"
-- -- local mt = {
-- --     __tostring = function(t)
-- --         return "object"
-- --     end
-- -- }
-- -- local object = setmetatable({}, mt)
-- -- local raw_data = {
-- --     boolean_value = false,
-- --     number_value = 1,
-- --     string_value = "string",
-- --     list_value = {1, 2, 3},
-- --     nested_table_value = {
-- --         nested_table_value = {
-- --             stub_value = true
-- --         }
-- --     },
-- --     object_value = object
-- -- }
-- -- local data = traceable.new(raw_data)
-- -- data.list_value = {1, 2, 3}
-- -- data.list_value = {1, 2, 3, 4}
-- -- data.list_value = {1, 2, 3}
-- -- traceable.commit(data)
-- -- print("traceable data.dirty 00000 ", data.dirty)
-- -- table.insert(data.list_value, 4)
-- -- traceable.commit(data)
-- -- print("traceable data.dirty aaaa", data.dirty)
-- -- traceable.commit(data)
-- -- print("traceable data.dirty bbbb", data.dirty)
-- -- table.insert(data.list_value, 5)
-- -- table.remove(data.list_value, #data.list_value)
-- -- print("traceable data.dirty ccccc", data.dirty)



local string_find = string.find
local string_match = string.match
local string_format = string.format
local string_sub = string.sub
local table_insert = table.insert
local table_remove = table.remove
local ipairs = ipairs
local tonumber = tonumber
local tostring = tostring
local type = type
local setmetatable = setmetatable
local getmetatable = getmetatable

local string_split = function(input, delimiter)
    input = tostring(input)
    delimiter = tostring(delimiter)
    if (delimiter == '') then
        return false
    end
    local pos, arr = 0, {}
    -- for each divider found
    for st, sp in function()
        return string_find(input, delimiter, pos, true)
    end do
        table_insert(arr, string_sub(input, pos, st - 1))
        pos = sp + 1
    end
    table_insert(arr, string_sub(input, pos))
    return arr
end

local table_removebyvalue = function(array, value, removeall)
    local c, i, max = 0, 1, #array
    while i <= max do
        if array[i] == value then
            table_remove(array, i)
            c = c + 1
            i = i - 1
            max = max - 1
            if not removeall then
                break
            end
        end
        i = i + 1
    end
    return c
end

local function getByPath(t, path)
    if not path then
        return t
    elseif string_find(path, "%.") then
        local paths = string_split(path, ".")
        local cur = t
        local hasN = string_find(path, "%[") ~= nil
        for i, keyname in ipairs(paths) do
            local v
            if hasN then
                local n = tonumber(string_match(keyname, "^%[(.+)%]$"))
                if n then
                    v = cur[n]
                end
            end
            if not v then
                v = cur[keyname]
            end
            if v then
                cur = v
            else
                return nil
            end
        end
        return cur
    else
        return t[path]
    end
end

local function setByPath(t, path, value)
    if not path then
        t = value
    elseif string_find(path, "%.") then
        local paths = string_split(path, ".")
        local cur = t
        local count = #paths
        local hasN = string_find(path, "%[") ~= nil
        for i = 1, count - 1 do
            local keyname = paths[i]
            local v
            if hasN then
                local n = tonumber(string_match(keyname, "^%[(.+)%]$"))
                if n then
                    v = cur[n]
                    keyname = n
                end
            end
            if not v then
                v = cur[keyname]
            end
            if v then
                cur = v
            else
                cur[keyname] = {}
                cur = cur[keyname]
            end
        end
        local lastKeyname = paths[count]
        if hasN then
            lastKeyname = tonumber(string_match(lastKeyname, "^%[(.+)%]$")) or lastKeyname
        end
        cur[lastKeyname] = value
    else
        t[path] = value
    end
    return t
end


local table_monitor_

function table_monitor_(tb, callback, path)
    local data_ = tb or {}
    local monitors_ = {}
    local spath = path
    local function createKey(subpath, key)
        if subpath then
            if "number" == type(key) then
                return string_format("%s.[%s]", subpath, key)
            else
                return string_format("%s.%s", subpath, key)
            end
        else
            if "number" == type(key) then
                return string_format("[%s]", key)
            else
                return key
            end
        end
    end
    local mt = {
        __index = function(_, k)
            if "__ismonitor__" == k then
                return true
            end
            if not data_ then
                return
            end
            local result = data_[k]
            if type(result) == "table" then
                if not monitors_[k] then
                    monitors_[k] = table_monitor_(result, callback, createKey(spath, k))
                end
                return monitors_[k]
            else
                return result
            end
        end,
        __newindex = function(_, k, v)
            local oldValue
            if data_ then
                oldValue = data_[k]
            end
            if oldValue ~= v then
                data_[k] = v
                if callback then
                    local key = createKey(spath, k)
                    if type(v) == "table" then
                        monitors_[k] = table_monitor_(v, callback, key)
                        callback(key, monitors_[k], oldValue)
                    else
                        if monitors_[k] then
                            monitors_[k] = nil
                        end
                        callback(key, v, oldValue)
                    end
                end
            end
        end,
        __call = function(t, k, v)
            if "[path]" == k then
                return spath
            elseif "string" == type(k) then
                if v then
                    setByPath(t, k, v)
                else
                    return getByPath(t, k)
                end
            else
                return data_
            end
        end,
    }
    return setmetatable({}, mt)
end

local M = {}
function M.new(tb, callback)
    if tb.__ismonitor__ then
        tb = tb()
    end
    local callbacks = {}
    if callback then
        callbacks[#callbacks + 1] = callback
    end
    local addCallback = function(self, cb)
        for _, cc in ipairs(callbacks) do
            if cc == cb then
                return
            end
        end
        callbacks[#callbacks + 1] = cb
    end
    local removeCallback = function(self, cb)
        table_removebyvalue(callbacks, cb, true)
    end
    local removeAllCallbacks = function(self)
        callbacks = {}
    end
    local cb = function(path, value, oldValue)
        for _, listener in ipairs(callbacks) do
            listener(path, value, oldValue)
        end
    end
    local result = table_monitor_(tb, cb)

    local mt = getmetatable(result)
    local index_fn = mt.__index
    mt.__index = function(t, k)
        if k == "addCallback" then
            return addCallback
        elseif k == "removeCallback" then
            return removeCallback
        elseif k == "removeAllCallbacks" then
            return removeAllCallbacks
        else
            return index_fn(t, k)
        end
    end
    setmetatable(result, mt)
    return result
end

return M


-- local traceable = require("traceable")
-- local a = {
-- 	a1 = 1,
-- 	a2 = { 1, 2, 3},
-- 	a3 = {
-- 		a31 = "a",
-- 		a32 = {
-- 			"b"
-- 		},
-- 	},
-- }

-- local ta = traceable.new(a, function ( path, value, oldValue )
-- 	print(string.format("table change at %s from ", path), oldValue, "to ", value)
-- end)

-- print("start~~~")
-- print(ta.a3.a32[1])
-- ta.a3.a32[2] = 2
-- ta.a3.a32 = {"c"}
-- ta.a3.a32.a = 3
-- ta.a3.a32 = 33
-- print("~~~", ta.a3.a32)
-- print(ta.a2[3])
-- ta.a3.a32 = {33}
-- print(ta.a3.a32[1])
-- ta.a3.a32 = nil
-- ta.a3.a32 = {5}
-- print(ta.a3.a32)
-- print("over~~~")