--- **AceDB-3.0** manages the SavedVariables of your addon.
-- It offers profile management, smart defaults and namespaces for modules.\\
-- Data can be saved in different data-types, depending on its intended usage.
-- The most common data-type is the `profile` type, which allows the user to choose
-- the active profile, and manage the profiles of all of his characters.\\
-- The following data types are available:
-- * **char** Character-specific data. Every character has its own database.
-- * **realm** Realm-specific data. All of the players characters on the same realm share this database.
-- * **class** Class-specific data. All of the players characters of the same class share this database.
-- * **race** Race-specific data. All of the players characters of the same race share this database.
-- * **faction** Faction-specific data. All of the players characters of the same faction share this database.
-- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database.
-- * **locale** Locale specific data, based on the locale of the players game client.
-- * **global** Global Data. All characters on the same account share this database.
-- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used.
--
-- Creating a new Database using the `:New` function will return a new DBObject. A database will inherit all functions
-- of the DBObjectLib listed here. \\
-- If you create a new namespaced child-database (`:RegisterNamespace`), you'll get a DBObject as well, but note
-- that the child-databases cannot individually change their profile, and are linked to their parents profile - and because of that,
-- the profile related APIs are not available. Only `:RegisterDefaults` and `:ResetProfile` are available on child-databases.
--
-- For more details on how to use AceDB-3.0, see the [[AceDB-3.0 Tutorial]].
--
-- You may also be interested in [[libdualspec-1-0|LibDualSpec-1.0]] to do profile switching automatically when switching specs.
--
-- @usage
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("DBExample")
--
-- -- declare defaults to be used in the DB
-- local defaults = {
--   profile = {
--     setting = true,
--   }
-- }
--
-- function MyAddon:OnInitialize()
--   -- Assuming the .toc says ## SavedVariables: MyAddonDB
--   self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
-- end
-- @class file
-- @name AceDB-3.0.lua
-- @release $Id: AceDB-3.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 27
local AceDB = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)

if not AceDB then return end -- No upgrade needed

-- Lua APIs
local type, pairs, next, error = type, pairs, next, error
local setmetatable, rawset, rawget = setmetatable, rawset, rawget

-- WoW APIs
local _G = _G

AceDB.db_registry = AceDB.db_registry or {}
AceDB.frame = AceDB.frame or CreateFrame("Frame")

local CallbackHandler
local CallbackDummy = {
    Fire = function()
    end
}

local DBObjectLib = {}

--[[-------------------------------------------------------------------------
	AceDB Utility Functions
---------------------------------------------------------------------------]]

-- Simple shallow copy for copying defaults
local function copyTable(src, dest)
    if type(dest) ~= "table" then dest = {} end
    if type(src) == "table" then
        for k, v in pairs(src) do
            if type(v) == "table" then
                -- try to index the key first so that the metatable creates the defaults, if set, and use that table
                v = copyTable(v, dest[k])
            end
            dest[k] = v
        end
    end
    return dest
end

-- Called to add defaults to a section of the database
--
-- When a ["*"] default section is indexed with a new key, a table is returned
-- and set in the host table.  These tables must be cleaned up by removeDefaults
-- in order to ensure we don't write empty default tables.
local function copyDefaults(dest, src)
    -- this happens if some value in the SV overwrites our default value with a non-table
    -- if type(dest) ~= "table" then return end
    for k, v in pairs(src) do
        if k == "*" or k == "**" then
            if type(v) == "table" then
                -- This is a metatable used for table defaults
                local mt = {
                    -- This handles the lookup and creation of new subtables
                    __index = function(t, k2)
                        if k2 == nil then return nil end
                        local tbl = {}
                        copyDefaults(tbl, v)
                        rawset(t, k2, tbl)
                        return tbl
                    end
                }
                setmetatable(dest, mt)
                -- handle already existing tables in the SV
                for dk, dv in pairs(dest) do
                    if not rawget(src, dk) and type(dv) == "table" then copyDefaults(dv, v) end
                end
            else
                -- Values are not tables, so this is just a simple return
                local mt = {
                    __index = function(t, k2)
                        return k2 ~= nil and v or nil
                    end
                }
                setmetatable(dest, mt)
            end
        elseif type(v) == "table" then
            if not rawget(dest, k) then rawset(dest, k, {}) end
            if type(dest[k]) == "table" then
                copyDefaults(dest[k], v)
                if src['**'] then copyDefaults(dest[k], src['**']) end
            end
        else
            if rawget(dest, k) == nil then rawset(dest, k, v) end
        end
    end
end

-- Called to remove all defaults in the default table from the database
local function removeDefaults(db, defaults, blocker)
    -- remove all metatables from the db, so we don't accidentally create new sub-tables through them
    setmetatable(db, nil)
    -- loop through the defaults and remove their content
    for k, v in pairs(defaults) do
        if k == "*" or k == "**" then
            if type(v) == "table" then
                -- Loop through all the actual k,v pairs and remove
                for key, value in pairs(db) do
                    if type(value) == "table" then
                        -- if the key was not explicitly specified in the defaults table, just strip everything from * and ** tables
                        if defaults[key] == nil and (not blocker or blocker[key] == nil) then
                            removeDefaults(value, v)
                            -- if the table is empty afterwards, remove it
                            if next(value) == nil then db[key] = nil end
                            -- if it was specified, only strip ** content, but block values which were set in the key table
                        elseif k == "**" then
                            removeDefaults(value, v, defaults[key])
                        end
                    end
                end
            elseif k == "*" then
                -- check for non-table default
                for key, value in pairs(db) do
                    if defaults[key] == nil and v == value then db[key] = nil end
                end
            end
        elseif type(v) == "table" and type(db[k]) == "table" then
            -- if a blocker was set, dive into it, to allow multi-level defaults
            removeDefaults(db[k], v, blocker and blocker[k])
            if next(db[k]) == nil then db[k] = nil end
        else
            -- check if the current value matches the default, and that its not blocked by another defaults table
            if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then db[k] = nil end
        end
    end
end

-- This is called when a table section is first accessed, to set up the defaults
local function initSection(db, section, svstore, key, defaults)
    local sv = rawget(db, "sv")

    local tableCreated
    if not sv[svstore] then sv[svstore] = {} end
    if not sv[svstore][key] then
        sv[svstore][key] = {}
        tableCreated = true
    end

    local tbl = sv[svstore][key]

    if defaults then copyDefaults(tbl, defaults) end
    rawset(db, section, tbl)

    return tableCreated, tbl
end

-- Metatable to handle the dynamic creation of sections and copying of sections.
local dbmt = {
    __index = function(t, section)
        local keys = rawget(t, "keys")
        local key = keys[section]
        if key then
            local defaultTbl = rawget(t, "defaults")
            local defaults = defaultTbl and defaultTbl[section]

            if section == "profile" then
                local new = initSection(t, section, "profiles", key, defaults)
                if new then
                    -- Callback: OnNewProfile, database, newProfileKey
                    t.callbacks:Fire("OnNewProfile", t, key)
                end
            elseif section == "profiles" then
                local sv = rawget(t, "sv")
                if not sv.profiles then sv.profiles = {} end
                rawset(t, "profiles", sv.profiles)
            elseif section == "global" then
                local sv = rawget(t, "sv")
                if not sv.global then sv.global = {} end
                if defaults then copyDefaults(sv.global, defaults) end
                rawset(t, section, sv.global)
            else
                initSection(t, section, section, key, defaults)
            end
        end

        return rawget(t, section)
    end
}

local function validateDefaults(defaults, keyTbl, offset)
    if not defaults then return end
    offset = offset or 0
    for k in pairs(defaults) do
        if not keyTbl[k] or k == "profiles" then
            error(("Usage: AceDBObject:RegisterDefaults(defaults): '%s' is not a valid datatype."):format(k), 3 + offset)
        end
    end
end

local preserve_keys = {
    ["callbacks"] = true,
    ["RegisterCallback"] = true,
    ["UnregisterCallback"] = true,
    ["UnregisterAllCallbacks"] = true,
    ["children"] = true
}

local realmKey = GetRealmName()
local charKey = UnitName("player") .. " - " .. realmKey
local _, classKey = UnitClass("player")
local _, raceKey = UnitRace("player")
local factionKey = UnitFactionGroup("player")
local factionrealmKey = factionKey .. " - " .. realmKey
local localeKey = GetLocale():lower()

local regionTable = {"US", "KR", "EU", "TW", "CN"}
-- TODO: better version, but PTR has regionKey >5 (72)
local regionKey = regionTable[GetCurrentRegion()] or 3
local factionrealmregionKey = factionrealmKey .. " - " .. regionKey

-- Actual database initialization function
local function initdb(sv, defaults, defaultProfile, olddb, parent)
    -- Generate the database keys for each section

    -- map "true" to our "Default" profile
    if defaultProfile == true then defaultProfile = "Default" end

    local profileKey
    if not parent then
        -- Make a container for profile keys
        if not sv.profileKeys then sv.profileKeys = {} end

        -- Try to get the profile selected from the char db
        profileKey = sv.profileKeys[charKey] or defaultProfile or charKey

        -- save the selected profile for later
        sv.profileKeys[charKey] = profileKey
    else
        -- Use the profile of the parents DB
        profileKey = parent.keys.profile or defaultProfile or charKey

        -- clear the profileKeys in the DB, namespaces don't need to store them
        sv.profileKeys = nil
    end

    -- This table contains keys that enable the dynamic creation
    -- of each section of the table.  The 'global' and 'profiles'
    -- have a key of true, since they are handled in a special case
    local keyTbl = {
        ["char"] = charKey,
        ["realm"] = realmKey,
        ["class"] = classKey,
        ["race"] = raceKey,
        ["faction"] = factionKey,
        ["factionrealm"] = factionrealmKey,
        ["factionrealmregion"] = factionrealmregionKey,
        ["profile"] = profileKey,
        ["locale"] = localeKey,
        ["global"] = true,
        ["profiles"] = true
    }

    validateDefaults(defaults, keyTbl, 1)

    -- This allows us to use this function to reset an entire database
    -- Clear out the old database
    if olddb then for k, v in pairs(olddb) do if not preserve_keys[k] then olddb[k] = nil end end end

    -- Give this database the metatable so it initializes dynamically
    local db = setmetatable(olddb or {}, dbmt)

    if not rawget(db, "callbacks") then
        -- try to load CallbackHandler-1.0 if it loaded after our library
        if not CallbackHandler then CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0", true) end
        db.callbacks = CallbackHandler and CallbackHandler:New(db) or CallbackDummy
    end

    -- Copy methods locally into the database object, to avoid hitting
    -- the metatable when calling methods

    if not parent then
        for name, func in pairs(DBObjectLib) do db[name] = func end
    else
        -- hack this one in
        db.RegisterDefaults = DBObjectLib.RegisterDefaults
        db.ResetProfile = DBObjectLib.ResetProfile
    end

    -- Set some properties in the database object
    db.profiles = sv.profiles
    db.keys = keyTbl
    db.sv = sv
    -- db.sv_name = name
    db.defaults = defaults
    db.parent = parent

    -- store the DB in the registry
    AceDB.db_registry[db] = true

    return db
end

-- handle PLAYER_LOGOUT
-- strip all defaults from all databases
-- and cleans up empty sections
local function logoutHandler(frame, event)
    if event == "PLAYER_LOGOUT" then
        for db in pairs(AceDB.db_registry) do
            db.callbacks:Fire("OnDatabaseShutdown", db)
            db:RegisterDefaults(nil)

            -- cleanup sections that are empty without defaults
            local sv = rawget(db, "sv")
            for section in pairs(db.keys) do
                if rawget(sv, section) then
                    -- global is special, all other sections have sub-entrys
                    -- also don't delete empty profiles on main dbs, only on namespaces
                    if section ~= "global" and (section ~= "profiles" or rawget(db, "parent")) then
                        for key in pairs(sv[section]) do
                            if not next(sv[section][key]) then sv[section][key] = nil end
                        end
                    end
                    if not next(sv[section]) then sv[section] = nil end
                end
            end
        end
    end
end

AceDB.frame:RegisterEvent("PLAYER_LOGOUT")
AceDB.frame:SetScript("OnEvent", logoutHandler)

--[[-------------------------------------------------------------------------
	AceDB Object Method Definitions
---------------------------------------------------------------------------]]

--- Sets the defaults table for the given database object by clearing any
-- that are currently set, and then setting the new defaults.
-- @param defaults A table of defaults for this database
function DBObjectLib:RegisterDefaults(defaults)
    if defaults and type(defaults) ~= "table" then
        error(("Usage: AceDBObject:RegisterDefaults(defaults): 'defaults' - table or nil expected, got %q."):format(
                  type(defaults)), 2)
    end

    validateDefaults(defaults, self.keys)

    -- Remove any currently set defaults
    if self.defaults then
        for section, key in pairs(self.keys) do
            if self.defaults[section] and rawget(self, section) then
                removeDefaults(self[section], self.defaults[section])
            end
        end
    end

    -- Set the DBObject.defaults table
    self.defaults = defaults

    -- Copy in any defaults, only touching those sections already created
    if defaults then
        for section, key in pairs(self.keys) do
            if defaults[section] and rawget(self, section) then
                copyDefaults(self[section], defaults[section])
            end
        end
    end
end

--- Changes the profile of the database and all of it's namespaces to the
-- supplied named profile
-- @param name The name of the profile to set as the current profile
function DBObjectLib:SetProfile(name)
    if type(name) ~= "string" then
        error(("Usage: AceDBObject:SetProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
    end

    -- changing to the same profile, dont do anything
    if name == self.keys.profile then return end

    local oldProfile = self.profile
    local defaults = self.defaults and self.defaults.profile

    -- Callback: OnProfileShutdown, database
    self.callbacks:Fire("OnProfileShutdown", self)

    if oldProfile and defaults then
        -- Remove the defaults from the old profile
        removeDefaults(oldProfile, defaults)
    end

    self.profile = nil
    self.keys["profile"] = name

    -- if the storage exists, save the new profile
    -- this won't exist on namespaces.
    if self.sv.profileKeys then self.sv.profileKeys[charKey] = name end

    -- populate to child namespaces
    if self.children then for _, db in pairs(self.children) do DBObjectLib.SetProfile(db, name) end end

    -- Callback: OnProfileChanged, database, newProfileKey
    self.callbacks:Fire("OnProfileChanged", self, name)
end

--- Returns a table with the names of the existing profiles in the database.
-- You can optionally supply a table to re-use for this purpose.
-- @param tbl A table to store the profile names in (optional)
function DBObjectLib:GetProfiles(tbl)
    if tbl and type(tbl) ~= "table" then
        error(("Usage: AceDBObject:GetProfiles(tbl): 'tbl' - table or nil expected, got %q."):format(type(tbl)), 2)
    end

    -- Clear the container table
    if tbl then
        for k, v in pairs(tbl) do tbl[k] = nil end
    else
        tbl = {}
    end

    local curProfile = self.keys.profile

    local i = 0
    for profileKey in pairs(self.profiles) do
        i = i + 1
        tbl[i] = profileKey
        if curProfile and profileKey == curProfile then curProfile = nil end
    end

    -- Add the current profile, if it hasn't been created yet
    if curProfile then
        i = i + 1
        tbl[i] = curProfile
    end

    return tbl, i
end

--- Returns the current profile name used by the database
function DBObjectLib:GetCurrentProfile()
    return self.keys.profile
end

--- Deletes a named profile.  This profile must not be the active profile.
-- @param name The name of the profile to be deleted
-- @param silent If true, do not raise an error when the profile does not exist
function DBObjectLib:DeleteProfile(name, silent)
    if type(name) ~= "string" then
        error(("Usage: AceDBObject:DeleteProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
    end

    if self.keys.profile == name then
        error(("Cannot delete the active profile (%q) in an AceDBObject."):format(name), 2)
    end

    if not rawget(self.profiles, name) and not silent then
        error(("Cannot delete profile %q as it does not exist."):format(name), 2)
    end

    self.profiles[name] = nil

    -- populate to child namespaces
    if self.children then for _, db in pairs(self.children) do DBObjectLib.DeleteProfile(db, name, true) end end

    -- switch all characters that use this profile back to the default
    if self.sv.profileKeys then
        for key, profile in pairs(self.sv.profileKeys) do
            if profile == name then self.sv.profileKeys[key] = nil end
        end
    end

    -- Callback: OnProfileDeleted, database, profileKey
    self.callbacks:Fire("OnProfileDeleted", self, name)
end

--- Copies a named profile into the current profile, overwriting any conflicting
-- settings.
-- @param name The name of the profile to be copied into the current profile
-- @param silent If true, do not raise an error when the profile does not exist
function DBObjectLib:CopyProfile(name, silent)
    if type(name) ~= "string" then
        error(("Usage: AceDBObject:CopyProfile(name): 'name' - string expected, got %q."):format(type(name)), 2)
    end

    if name == self.keys.profile then
        error(("Cannot have the same source and destination profiles (%q)."):format(name), 2)
    end

    if not rawget(self.profiles, name) and not silent then
        error(("Cannot copy profile %q as it does not exist."):format(name), 2)
    end

    -- Reset the profile before copying
    DBObjectLib.ResetProfile(self, nil, true)

    local profile = self.profile
    local source = self.profiles[name]

    copyTable(source, profile)

    -- populate to child namespaces
    if self.children then for _, db in pairs(self.children) do DBObjectLib.CopyProfile(db, name, true) end end

    -- Callback: OnProfileCopied, database, sourceProfileKey
    self.callbacks:Fire("OnProfileCopied", self, name)
end

--- Resets the current profile to the default values (if specified).
-- @param noChildren if set to true, the reset will not be populated to the child namespaces of this DB object
-- @param noCallbacks if set to true, won't fire the OnProfileReset callback
function DBObjectLib:ResetProfile(noChildren, noCallbacks)
    local profile = self.profile

    for k, v in pairs(profile) do profile[k] = nil end

    local defaults = self.defaults and self.defaults.profile
    if defaults then copyDefaults(profile, defaults) end

    -- populate to child namespaces
    if self.children and not noChildren then
        for _, db in pairs(self.children) do DBObjectLib.ResetProfile(db, nil, noCallbacks) end
    end

    -- Callback: OnProfileReset, database
    if not noCallbacks then self.callbacks:Fire("OnProfileReset", self) end
end

--- Resets the entire database, using the string defaultProfile as the new default
-- profile.
-- @param defaultProfile The profile name to use as the default
function DBObjectLib:ResetDB(defaultProfile)
    if defaultProfile and type(defaultProfile) ~= "string" then
        error(
            ("Usage: AceDBObject:ResetDB(defaultProfile): 'defaultProfile' - string or nil expected, got %q."):format(
                type(defaultProfile)), 2)
    end

    local sv = self.sv
    for k, v in pairs(sv) do sv[k] = nil end

    initdb(sv, self.defaults, defaultProfile, self)

    -- fix the child namespaces
    if self.children then
        if not sv.namespaces then sv.namespaces = {} end
        for name, db in pairs(self.children) do
            if not sv.namespaces[name] then sv.namespaces[name] = {} end
            initdb(sv.namespaces[name], db.defaults, self.keys.profile, db, self)
        end
    end

    -- Callback: OnDatabaseReset, database
    self.callbacks:Fire("OnDatabaseReset", self)
    -- Callback: OnProfileChanged, database, profileKey
    self.callbacks:Fire("OnProfileChanged", self, self.keys["profile"])

    return self
end

--- Creates a new database namespace, directly tied to the database.  This
-- is a full scale database in it's own rights other than the fact that
-- it cannot control its profile individually
-- @param name The name of the new namespace
-- @param defaults A table of values to use as defaults
function DBObjectLib:RegisterNamespace(name, defaults)
    if type(name) ~= "string" then
        error(("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - string expected, got %q."):format(type(
                                                                                                                     name)),
              2)
    end
    if defaults and type(defaults) ~= "table" then
        error(
            ("Usage: AceDBObject:RegisterNamespace(name, defaults): 'defaults' - table or nil expected, got %q."):format(
                type(defaults)), 2)
    end
    if self.children and self.children[name] then
        error(
            ("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - a namespace called %q already exists."):format(
                name), 2)
    end

    local sv = self.sv
    if not sv.namespaces then sv.namespaces = {} end
    if not sv.namespaces[name] then sv.namespaces[name] = {} end

    local newDB = initdb(sv.namespaces[name], defaults, self.keys.profile, nil, self)

    if not self.children then self.children = {} end
    self.children[name] = newDB
    return newDB
end

--- Returns an already existing namespace from the database object.
-- @param name The name of the new namespace
-- @param silent if true, the addon is optional, silently return nil if its not found
-- @usage
-- local namespace = self.db:GetNamespace('namespace')
-- @return the namespace object if found
function DBObjectLib:GetNamespace(name, silent)
    if type(name) ~= "string" then
        error(("Usage: AceDBObject:GetNamespace(name): 'name' - string expected, got %q."):format(type(name)), 2)
    end
    if not silent and not (self.children and self.children[name]) then
        error(("Usage: AceDBObject:GetNamespace(name): 'name' - namespace %q does not exist."):format(name), 2)
    end
    if not self.children then self.children = {} end
    return self.children[name]
end

--[[-------------------------------------------------------------------------
	AceDB Exposed Methods
---------------------------------------------------------------------------]]

--- Creates a new database object that can be used to handle database settings and profiles.
-- By default, an empty DB is created, using a character specific profile.
--
-- You can override the default profile used by passing any profile name as the third argument,
-- or by passing //true// as the third argument to use a globally shared profile called "Default".
--
-- Note that there is no token replacement in the default profile name, passing a defaultProfile as "char"
-- will use a profile named "char", and not a character-specific profile.
-- @param tbl The name of variable, or table to use for the database
-- @param defaults A table of database defaults
-- @param defaultProfile The name of the default profile. If not set, a character specific profile will be used as the default.
-- You can also pass //true// to use a shared global profile called "Default".
-- @usage
-- -- Create an empty DB using a character-specific default profile.
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB")
-- @usage
-- -- Create a DB using defaults and using a shared default profile
-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true)
function AceDB:New(tbl, defaults, defaultProfile)
    if type(tbl) == "string" then
        local name = tbl
        tbl = _G[name]
        if not tbl then
            tbl = {}
            _G[name] = tbl
        end
    end

    if type(tbl) ~= "table" then
        error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'tbl' - table expected, got %q."):format(type(tbl)), 2)
    end

    if defaults and type(defaults) ~= "table" then
        error(("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaults' - table expected, got %q."):format(type(
                                                                                                                   defaults)),
              2)
    end

    if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then
        error(
            ("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaultProfile' - string or true expected, got %q."):format(
                type(defaultProfile)), 2)
    end

    return initdb(tbl, defaults, defaultProfile)
end

-- upgrade existing databases
for db in pairs(AceDB.db_registry) do
    if not db.parent then
        for name, func in pairs(DBObjectLib) do db[name] = func end
    else
        db.RegisterDefaults = DBObjectLib.RegisterDefaults
        db.ResetProfile = DBObjectLib.ResetProfile
    end
end
