-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local Settings = TSM.Init("Service.Settings")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Sound = TSM.Include("Util.Sound")
local CSV = TSM.Include("Util.CSV")
local Wow = TSM.Include("Util.Wow")
local private = {
	context = {},
	proxies = {},
	views = {},
	protectedAccessAllowed = {},
	cachedConnectedRealms = nil,
	upgradeContext = nil,
	svCopyErrorTime = 0,
	db = nil,
}
local LibRealmInfo = LibStub("LibRealmInfo")
local KEY_SEP = "@"
local SCOPE_KEY_SEP = " - "
local GLOBAL_SCOPE_KEY = " "
local DEFAULT_PROFILE_NAME = "Default"
local PLAYER = UnitName("player")
local FACTION = UnitFactionGroup("player")
local REALM = GetRealmName()
local VALID_TYPES = {
	boolean = true,
	string = true,
	table = true,
	number = true,
}
local SCOPE_TYPES = {
	global = "g",
	profile = "p",
	realm = "r",
	factionrealm = "f",
	char = "c",
	sync = "s",
}
local SCOPE_KEYS = {
	global = " ",
	profile = nil, -- set per-DB
	realm = REALM,
	factionrealm = strjoin(SCOPE_KEY_SEP, FACTION, REALM),
	char = strjoin(SCOPE_KEY_SEP, PLAYER, REALM),
	sync = strjoin(SCOPE_KEY_SEP, PLAYER, FACTION, REALM),
}
local DEFAULT_DB = {
	_version = -math.huge, -- DB version
	_currentProfile = {}, -- lookup table of the current profile name by character
	_syncAccountKey = {}, -- lookup table of the sync account key by factionrealm
	_syncOwner = {}, -- lookup table of the owner sync account key by character
	_hash = 0,
	_scopeKeys = {
		profile = {},
		realm = {},
		factionrealm = {},
		char = {},
		sync = {},
	},
	_lastModifiedVersion = {},
}

-- Changelog:
-- [6] added 'global.locale' key
-- [7] changed default value of 'tsmItemTweetEnabled' to false
-- [8] added 'global.itemCacheVersion' key
-- [9] removed 'global.itemCacheVersion' key, added 'global.clientVersion' key
-- [10] first TSM4 version - combined all module settings into a single DB
-- [11] added profile.internalData.createdDefaultOperations
-- [12] added global.shoppingOptions.pctSource
-- [13] added profile.internalData.{managementGroupTreeContext,auctioningGroupTreeContext,shoppingGroupTreeContext}
-- [14] added global.userData.savedAuctioningSearches
-- [15] added global.coreOptions.bankUITab, profile.coreOptions.{bankUIBankFramePosition,bankUIGBankFramePosition}
-- [16] moved profile.coreOptions.{bankUIBankFramePosition,bankUIGBankFramePosition} to profile.internalData.{bankUIBankFramePosition,bankUIGBankFramePosition}
-- [17] added global.internalData.{mainUIFrameContext,auctionUIFrameContext,craftingUIFrameContext}
-- [18] removed global.internalData.itemStringLookup
-- [19] added sync scope (initially with internalData.{classKey,bagQuantity,bankQuantity,reagentBankQuantity,auctionQuantity,mailQuantity}), removed factionrealm.internalData.{syncMetadata,accountKey,inventory,characters} and factionrealm.coreOptions.syncAccounts, added global.debug.chatLoggingEnabled
-- [20] added global.tooltipOptions.enabled
-- [21] added global.craftingOptions.{profitPercent,questSmartCrafting,queueSort}
-- [22] added global.coreOptions.cleanGuildBank
-- [23] changed global.shoppingOptions.maxDeSearchPercent default to 100
-- [24] added global.auctioningOptions.{showAuctionDBTab,openAllBags,ahRowDisplay}
-- [25] split realm.internalData.goldLog into sync.internalData.goldLog and factionrealm.internalData.guildGoldLog
-- [26] added profile.internalData.{shoppingTabGroupContext,auctioningTabGroupContext}
-- [27] added char.internalData.craftingCooldowns
-- [28] added global.internalData.mailingUIFrameContext
-- [29] added global.internalData.vendoringUIFrameContext
-- [30] added global.internalData.bankingUIFrameContext
-- [31] changed global.internalData.bankingUIFrameContext default (isOpen = true), added profile.internalData.{bankingWarehousingGroupTreeContext,bankingAuctioningGroupTreeContext,bankingMailingGroupTreeContext}
-- [32] removed factionrealm.internalData.gathering, added factionrealm.internalData.gatheringContext.{crafter,professions}, added profile.gatheringOptions.sources
-- [33] added global.internalData.taskListUIFrameContext
-- [34] removed realm.internalData.{lastAuctionDBCompleteScan,lastAuctionDBSaveTime,auctionDBScanData}
-- [35] added factionrealm.userData.craftingCooldownIgnore
-- [36] removed factionrealm.internalData.playerProfessions and added sync.internalData.playerProfessions
-- [37] removed global.auctioningOptions.showAuctionDBTab
-- [38] removed global.mailingOptions.{defaultMailTab,autoCheck,displayMoneyCollected,deleteEmptyNPCMail,showReloadBtn,sendDelay,defaultPage}, added global.mailingOptions.recentlyMailedList
-- [39] added profile.internalData.{craftingGroupTreeContext,mailingGroupTreeContext,vendoringGroupTreeContext,importGroupTreeContext}
-- [40] removed global.accountingOptions.{timeFormat,mvSource}
-- [41] removed global.coreOptions.groupPriceSource
-- [42] removed global.vendoringOptions.defaultMerchantTab
-- [43] removed global.coreOptions.{moveDelay,bankUITab}, removed global.auctioningOptions.{openAllBags,ahRowDisplay}, removed global.craftingOptions.{profitPercent,questSmartCrafting,queueSort}, removed global.destroyingOptions.{logDays,timeFormat}, removed global.vendoringOptions.{autoSellTrash,qsHideGrouped,qsHideSoulbound,qsBatchSize,defaultPage,qsMaxMarketValue,qsDestroyValue}, removed profile.coreOptions.{cleanBags,cleanBank,cleanReagentBank,cleanGuildBank}
-- [44] changed global.internalData.{mainUIFrameContext,auctionUIFrameContext,craftingUIFrameContext,destroyingUIFrameContext,mailingUIFrameContext,vendoringUIFrameContext,bankingUIFrameContext} default (added "scale = 1")
-- [45] added char.internalData.auctionSaleHints
-- [46] added global.shoppingOptions.{buyoutConfirm,buyoutAlertSource}
-- [47] added factionrealm.internalData.expiringMail and factionrealm.internalData.expiringAuction
-- [48] added profile.internalData.exportGroupTreeContext
-- [49] added factionrealm.internalData.{mailDisenchantablesChar,mailExcessGoldChar,mailExcessGoldLimit}
-- [50] added factionrealm.internalData.{csvAuctionDBScan,auctionDBScanTime,auctionDBScanHash}
-- [51-53] resetting factionrealm.internalData.crafts
-- [54] removed global.coreOptions.{tsmItemTweetEnabled,auctionSaleEnabled,auctionBuyEnabled}
-- [55] added global.auctionUIContext.{auctioningSelectionDividedContainer,auctioningBagScrollingTable,auctioningLogScrollingTable,auctioningAuctionScrollingTable,myAuctionsScrollingTable,shoppingSelectionDividedContainer,shoppingAuctionScrollingTable,sniperScrollingTable,frame,showDefault,shoppingSearchesTabGroup}
--  added global.bankingUIContext.{frame,isOpen,tab}
--  added global.craftingUIContext.{craftsScrollingTable,matsScrollingTable,gatheringDividedContainer,gatheringScrollingTable,professionScrollingTable,frame,showDefault,professionDividedContainer}
--  added global.destroyingUIContext.itemsScrollingTable
--  added global.mailingUIContext.{mailsScrollingTable,frame,showDefault}
--  added global.mainUIContext.{ledgerDetailScrollingTable,ledgerInventoryScrollingTable,ledgerAuctionsScrollingTable,ledgerOtherScrollingTable,ledgerTransactionsScrollingTable,ledgerResaleScrollingTable,frame,dashboardDividedContainer,groupsDividedContainer,operationsDividedContainer,importExportDividedContainer}
--  added global.taskListUIContext.{frame,isOpen}
--  added global.vendoringUIContext.{buyScrollingTable,buybackScrollingTable,sellScrollingTable,frame,showDefault}
--  added profile.mainUIContext.{groupsManagementGroupTree,importGroupTree,exportGroupTree}
--  added profile.auctionUIContext.{auctioningTabGroup,auctioningGroupTree,shoppingGroupTree}
--  added profile.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree}
--  added profile.craftingUIContext.groupTree
--  added profile.mailingUIContext.groupTree
--  added profile.vendoringUIContext.groupTree
--  removed profile.internalData.{auctioningTabGroupContext,auctioningGroupTreeContext,managementGroupTreeContext,shoppingGroupTreeContext,importGroupTreeContext,exportGroupTreeContext,bankingUIFrameContext,craftingUIFrameContext,auctionUIFrameContext,mailingUIFrameContext,vendoringUIFrameContext,destroyingUIFrameContext,mainUIFrameContext,taskListUIFrameContext}
-- [56] added factionrealm.internalData.isCraftFavorite
-- [57] updated global.auctionUIContext.auctioningAuctionScrollingTable
-- [58] updated global.auctionUIContext.sniperScrollingTable
-- [59] updated global.mainUIContext.{frame,dashboardDividedContainer,ledgerDetailScrollingTable,ledgerInventoryScrollingTable,ledgerAuctionsScrollingTable,ledgerOtherScrollingTable,ledgerTransactionsScrollingTable,ledgerResaleScrollingTable}
-- [60] updated global.auctionUIContext.{auctioningAuctionScrollingTable,shoppingAuctionScrollingTable,sniperScrollingTable}, global.craftingUIContext.professionScrollingTable
-- [61] updated global.auctionUIContext.sniperScrollingTable
-- [62] updated global.mainUIContext.{ledgerTransactionsScrollingTable,ledgerResaleScrollingTable}
-- [63] removed global.auctioningOptions.roundNormalPrice
-- [64] removed global.accountingOptions.smartBuyPrice
-- [65] added global.appearanceOptions.colorSet
-- [66] added global.auctionUIContext.auctioningSelectionVerticalDividedContainer
-- [67] updated global.mailingUIContext.mailsScrollingTable
-- [68] removed profile.internalData.{bankUIBankFramePosition,bankUIGBankFramePosition,shoppingTabGroupContext,bankingWarehousingGroupTreeContext,bankingAuctioningGroupTreeContext,bankingMailingGroupTreeContext,craftingGroupTreeContext,mailingGroupTreeContext,vendoringGroupTreeContext}
-- [69] updated global.mainUIContext.ledgerInventoryScrollingTable
-- [70] updated global.auctionUIContext.{auctioningAuctionScrollingTable,shoppingAuctionScrollingTable}
-- [71] moved profile.auctionUIContext.{auctioningGroupTree,shoppingGroupTree},profile.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree},profile.craftingUIContext.groupTree,profile.mailingUIContext.groupTree,profile.mainUIContext.{groupsManagementGroupTree,importGroupTree,exportGroupTree} to char.*
-- [72] updated global.auctionUIContext.sniperScrollingTable
-- [73] added profile.vendoringUIContext.groupTree
-- [74] added sync.internalData.money
-- [75] updated global.appearanceOptions.colorSet
-- [76] updated global.mainUIContext.operationsSummaryScrollingTable
-- [77] added global.coreOptions.protectAuctionHouse
-- [78] added global.mainUIContext.{dashboardUnselectedCharacters,dashboardTimeRange}
-- [79] updated global.shoppingOptions.maxDeSearchLvl
-- [80] updated char.auctionUIContext.{auctioningGroupTree,shoppingGroupTree},char.bankingUIContext.{warehousingGroupTree,auctioningGroupTree,mailingGroupTree},char.craftingUIContext.groupTree,char.mailingUIContext.groupTree,char.vendoringUIContext.groupTree,char.mainUIContext.{importGroupTree,exportGroupTree}
-- [81] updated global.mailingUIContext.mailsScrollingTable
-- [82] updated global.craftingUIContext.professionScrollingTable
-- [83] added sync.internalData.goldLogLastUpdate, factionrealm.internalData.guildGoldLogLastUpdate
-- [84] added global.auctionUIContext.myAuctionsScrollingTable
-- [85] removed global.craftingOptions.ignoreCDCraftCost
-- [86] updated global.craftingUIContext.craftsScrollingTable
-- [87] added global.craftingUIContext.craftsScrollingTable
-- [88] added global.shoppingOptions.searchAutoFocus
-- [89] updated global.craftingOptions.defaultCraftPriceMethod
-- [90] added global.internalData.lastCharacter
-- [91] updated global.craftingUIContext.professionScrollingTable
-- [92] updated global.vendoringUIContext.buyScrollingTable
-- [93] moved profile.auctionUIContext.auctioningTabGroup to global.auctionUIContext.auctioningTabGroup
-- [94] added global.internalData.whatsNewVersion
-- [95] added global.appearanceOptions.showTotalMoney
-- [96] updated global.userData.{savedShoppingSearches,savedAuctioningSearches}
-- [97] added global.internalData.{optionalMatBonusIdLookup,optionalMatTextLookup}

local SETTINGS_INFO = {
	version = 97,
	global = {
		debug = {
			chatLoggingEnabled = { type = "boolean", default = false, lastModifiedVersion = 19 },
		},
		internalData = {
			lastCharacter = { type = "string", default = "???", lastModifiedVersion = 90 },
			vendorItems = { type = "table", default = {}, lastModifiedVersion = 10 },
			appMessageId = { type = "number", default = 0, lastModifiedVersion = 10 },
			destroyingHistory = { type = "table", default = {}, lastModifiedVersion = 10 },
			whatsNewVersion = { type = "number", default = 0, lastModifiedVersion = 94 },
			optionalMatBonusIdLookup = { type = "table", default = {}, lastModifiedVersion = 97 },
			optionalMatTextLookup = { type = "table", default = {}, lastModifiedVersion = 97 },
		},
		appearanceOptions = {
			taskListBackgroundLock = { type = "boolean", default = false, lastModifiedVersion = 87 },
			showTotalMoney = { type = "boolean", default = false, lastModifiedVersion = 95 },
			colorSet = { type = "string", default = "midnight", lastModifiedVersion = 75 },
		},
		auctionUIContext = {
			frame = { type = "table", default = { width = 830, height = 587, centerX = -300, centerY = 100, scale = 1, page = 1 }, lastModifiedVersion = 55 },
			showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 },
			auctioningSelectionDividedContainer = { type = "table", default = { leftWidth = 272 }, lastModifiedVersion = 55 },
			auctioningSelectionVerticalDividedContainer = { type = "table", default = { leftWidth = 220 }, lastModifiedVersion = 66 },
			auctioningBagScrollingTable = { type = "table", default = { colWidth = { selected = 16, item = 246, operation = 206 }, colHidden = {} }, lastModifiedVersion = 55 },
			auctioningLogScrollingTable = { type = "table", default = { colWidth = { index = 14, item = 190, buyout = 110, operation = 108, seller = 90, info = 234 }, colHidden = {} }, lastModifiedVersion = 55 },
			auctioningAuctionScrollingTable = { type = "table", default = { colWidth = { item = 226, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, timeLeft = 26, seller = TSM.IsWowClassic() and 88 or 136, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115, bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 70 },
			myAuctionsScrollingTable = { type = "table", default = { colWidth = { item = 248, stackSize = 30, timeLeft = 40, highbidder = TSM.IsWowClassic() and 110 or nil, group = TSM.IsWowClassic() and 110 or 228, currentBid = 100, buyout = 100 }, colHidden = {} }, lastModifiedVersion = 84 },
			shoppingSelectionDividedContainer = { type = "table", default = { leftWidth = 272 }, lastModifiedVersion = 55 },
			shoppingAuctionScrollingTable = { type = "table", default = { colWidth = { item = 226, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, timeLeft = 26, seller = TSM.IsWowClassic() and 88 or 136, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115, bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 70 },
			sniperScrollingTable = { type = "table", default = { colWidth = { icon = 24, item = 230, ilvl = 32, qty = not TSM.IsWowClassic() and 40 or nil, posts = TSM.IsWowClassic() and 40 or nil, stack = TSM.IsWowClassic() and 40 or nil, seller = TSM.IsWowClassic() and 86 or 134, itemBid = 115, bid = 115, itemBuyout = 115, buyout = 115,  bidPct = 40, pct = 40 }, colHidden = { bid = true, buyout = true, bidPct = true } }, lastModifiedVersion = 72 },
			shoppingSearchesTabGroup = { type = "table", default = { pathIndex = 1 }, lastModifiedVersion = 55 },
			auctioningTabGroup = { type = "table", default = { pathIndex = 1 }, lastModifiedVersion = 93 },
		},
		bankingUIContext = {
			frame = { type = "table", default = { width = 325, height = 600, centerX = 500, centerY = 0, scale = 1 }, lastModifiedVersion = 55 },
			isOpen = { type = "boolean", default = true, lastModifiedVersion = 55 },
			tab = { type = "string", default = "Warehousing", lastModifiedVersion = 55 },
		},
		craftingUIContext = {
			frame = { type = "table", default = { width = 820, height = 587, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 },
			showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 },
			craftsScrollingTable = { type = "table", default = { colWidth = { queued = 30, craftName = 218, operation = 80, bags = 28, ah = 24, craftingCost = 100, itemValue = 100, profit = 100, profitPct = 50,  saleRate = 32 }, colHidden = { profitPct = true } }, lastModifiedVersion = 86 },
			matsScrollingTable = { type = "table", default = { colWidth = { name = 242, price = 100, professions = 310, num = 100 }, colHidden = {} }, lastModifiedVersion = 55 },
			gatheringDividedContainer = { type = "table", default = { leftWidth = 284 }, lastModifiedVersion = 55 },
			gatheringScrollingTable = { type = "table", default = { colWidth = { name = 206, sources = 160, have = 50, need = 50 }, colHidden = {} }, lastModifiedVersion = 55 },
			professionScrollingTable = { type = "table", default = { colWidth = { name = not TSM.IsWowClassic() and 240 or 288, qty = 54, rank = not TSM.IsWowClassic() and 40 or nil, craftingCost = 100, itemValue = 100, profit = 100, profitPct = 50, saleRate = 30 }, colHidden = { craftingCost = true, itemValue = true, profitPct = true }, collapsed = {} }, lastModifiedVersion = 91 },
			professionDividedContainer = { type = "table", default = { leftWidth = 520 }, lastModifiedVersion = 55 },
		},
		destroyingUIContext = {
			frame = { type = "table", default = { width = 296, height = 442, centerX = 0, centerY = 0, scale = 1 }, lastModifiedVersion = 55 },
			itemsScrollingTable = { type = "table", default = { colWidth = { item = 214, num = 30 }, colHidden = {} }, lastModifiedVersion = 55 },
		},
		mailingUIContext = {
			frame = { type = "table", default = { width = 620, height = 516, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 },
			showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 },
			mailsScrollingTable = { type = "table", default = { colWidth = { items = 380, sender = 100, expires = 65, money = 115 }, colHidden = { sender = true } }, lastModifiedVersion = 81 },
		},
		mainUIContext = {
			frame = { type = "table", default = { width = 900, height = 700, centerX = 0, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 59 },
			ledgerDetailScrollingTable = { type = "table", default = { colWidth = { activityType = 91, source = 60, buyerSeller = 100, qty = 45, perItem = 120, totalPrice = 120, time = 110 }, colHidden = {} }, lastModifiedVersion = 59 },
			ledgerInventoryScrollingTable = { type = "table", default = { colWidth = { item = 160, totalItems = 50, bags = 50, banks = 50, mail = 50, alts = 50, guildVault = 50, auctionHouse = 50, totalValue = 120 }, colHidden = {} }, lastModifiedVersion = 69 },
			ledgerAuctionsScrollingTable = { type = "table", default = { colWidth = { item = 305, player = 110, stackSize = 55, quantity = 72, time = 120 }, colHidden = {} }, lastModifiedVersion = 59 },
			ledgerOtherScrollingTable = { type = "table", default = { colWidth = { type = 200, character = 110, otherCharacter = 122, amount = 120, time = 110 }, colHidden = {} }, lastModifiedVersion = 59 },
			ledgerTransactionsScrollingTable = { type = "table", default = { colWidth = { item = 156, player = 95, type = 50, stack = 55, auctions = 60, perItem = 120, total = 120, time = 110 }, colHidden = { total = true } }, lastModifiedVersion = 62 },
			ledgerResaleScrollingTable = { type = "table", default = { colWidth = { item = 194, bought = 50, avgBuyPrice = 120, sold = 50, avgSellPrice = 120, avgProfit = 120, totalProfit = 120, profitPct = 80 }, colHidden = { totalProfit = true, profitPct = true } }, lastModifiedVersion = 62 },
			dashboardDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 59 },
			dashboardUnselectedCharacters = { type = "table", default = {}, lastModifiedVersion = 78 },
			dashboardTimeRange = { type = "number", default = -1, lastModifiedVersion = 78 },
			groupsDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 55 },
			operationsDividedContainer = { type = "table", default = { leftWidth = 306 }, lastModifiedVersion = 55 },
			importExportDividedContainer = { type = "table", default = { leftWidth = 300 }, lastModifiedVersion = 55 },
			operationsSummaryScrollingTable = { type = "table", default = { colWidth = { selected = 16, name = 248, groups = 130, items = 130 }, colHidden = {} }, lastModifiedVersion = 76 },
		},
		taskListUIContext = {
			frame = { type = "table", default = { topRightX = -220, topRightY = -10, minimized = false, isOpen = true }, lastModifiedVersion = 55 },
			isOpen = { type = "boolean", default = true, lastModifiedVersion = 55 },
		},
		vendoringUIContext = {
			frame = { type = "table", default = { width = 560, height = 500, centerX = -200, centerY = 0, scale = 1, page = 1 }, lastModifiedVersion = 55 },
			showDefault = { type = "boolean", default = false, lastModifiedVersion = 55 },
			buyScrollingTable = { type = "table", default = { colWidth = { qty = 40, item = 310, ilvl = 32, cost = 150 }, colHidden = { ilvl = true } }, lastModifiedVersion = 92 },
			buybackScrollingTable = { type = "table", default = { colWidth = { qty = 40, item = 360, cost = 100 }, colHidden = {} }, lastModifiedVersion = 55 },
			sellScrollingTable = { type = "table", default = { colWidth = { item = 300, vendorSell = 100, potential = 100 }, colHidden = {} }, lastModifiedVersion = 55 },
		},
		coreOptions = {
			globalOperations = { type = "boolean", default = false, lastModifiedVersion = 10 },
			protectAuctionHouse = { type = "boolean", default = false, lastModifiedVersion = 77 },
			chatFrame = { type = "string", default = "", lastModifiedVersion = 10 },
			auctionSaleSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 },
			minimapIcon = { type = "table", default = { hide = false, minimapPos = 220, radius = 80 }, lastModifiedVersion = 10 },
			destroyValueSource = { type = "string", default = "dbmarket", lastModifiedVersion = 10 },
			groupPriceSource = { type = "string", default = "dbmarket", lastModifiedVersion = 41 },
		},
		accountingOptions = {
			trackTrades = { type = "boolean", default = true, lastModifiedVersion = 10 },
			autoTrackTrades = { type = "boolean", default = false, lastModifiedVersion = 10 },
		},
		auctioningOptions = {
			cancelWithBid = { type = "boolean", default = false, lastModifiedVersion = 10 },
			disableInvalidMsg = { type = "boolean", default = false, lastModifiedVersion = 10 },
			matchWhitelist = { type = "boolean", default = true, lastModifiedVersion = 10 },
			scanCompleteSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 },
			confirmCompleteSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 },
		},
		craftingOptions = {
			defaultMatCostMethod = { type = "string", default = "min(dbmarket, crafting, vendorbuy, convert(dbmarket))", lastModifiedVersion = 10 },
			defaultCraftPriceMethod = { type = "string", default = "first(dbminbuyout, dbmarket)*0.95", lastModifiedVersion = 89 },
			ignoreCharacters = { type = "table", default = {}, lastModifiedVersion = 10 },
			ignoreGuilds = { type = "table", default = {}, lastModifiedVersion = 10 },
		},
		destroyingOptions = {
			autoStack = { type = "boolean", default = true, lastModifiedVersion = 10 },
			includeSoulbound = { type = "boolean", default = false, lastModifiedVersion = 10 },
			autoShow = { type = "boolean", default = true, lastModifiedVersion = 10 },
			deMaxQuality = { type = "number", default = 3, lastModifiedVersion = 10 },
			deAbovePrice = { type = "string", default = "0c", lastModifiedVersion = 10 },
		},
		mailingOptions = {
			sendItemsIndividually = { type = "boolean", default = false, lastModifiedVersion = 10 },
			inboxMessages = { type = "boolean", default = true, lastModifiedVersion = 10 },
			sendMessages = { type = "boolean", default = true, lastModifiedVersion = 10 },
			resendDelay = { type = "number", default = 1, lastModifiedVersion = 10 },
			keepMailSpace = { type = "number", default = 0, lastModifiedVersion = 10 },
			deMaxQuality = { type = "number", default = 2, lastModifiedVersion = 10 },
			openMailSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 },
			recentlyMailedList = { type = "table", default = {}, lastModifiedVersion = 38 },
		},
		shoppingOptions = {
			minDeSearchLvl = { type = "number", default = 1, lastModifiedVersion = 10 },
			maxDeSearchLvl = { type = "number", default = 500, lastModifiedVersion = 79 },
			maxDeSearchPercent = { type = "number", default = 100, lastModifiedVersion = 23 },
			pctSource = { type = "string", default = "dbmarket", lastModifiedVersion = 12 },
			buyoutConfirm  = { type = "boolean", default = false, lastModifiedVersion = 46 },
			buyoutAlertSource  = { type = "string", default = "min(100000g, 200% dbmarket)", lastModifiedVersion = 46 },
			searchAutoFocus = { type = "boolean", default = true, lastModifiedVersion = 88 },
		},
		sniperOptions = {
			sniperSound = { type = "string", default = Sound.GetNoSoundKey(), lastModifiedVersion = 10 },
		},
		vendoringOptions = {
			displayMoneyCollected = { type = "boolean", default = false, lastModifiedVersion = 10 },
			qsMarketValue = { type = "string", default = "dbmarket", lastModifiedVersion = 10 },
		},
		tooltipOptions = {
			enabled = { type = "boolean", default = true, lastModifiedVersion = 20 },
			embeddedTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			customPriceTooltips = { type = "table", default = {}, lastModifiedVersion = 10 },
			moduleTooltips = { type = "table", default = {}, lastModifiedVersion = 10 },
			vendorBuyTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			vendorSellTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			groupNameTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			detailedDestroyTooltip = { type = "boolean", default = false, lastModifiedVersion = 10 },
			millTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			prospectTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			deTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			transformTooltip = { type = "boolean", default = true, lastModifiedVersion = 10 },
			operationTooltips = { type = "table", default = {}, lastModifiedVersion = 10 },
			tooltipShowModifier = { type = "string", default = "none", lastModifiedVersion = 10 },
			inventoryTooltipFormat = { type = "string", default = "full", lastModifiedVersion = 10 },
			tooltipPriceFormat = { type = "string", default = "text", lastModifiedVersion = 10 },
		},
		userData = {
			operations = { type = "table", default = {}, lastModifiedVersion = 10 },
			customPriceSources = { type = "table", default = {}, lastModifiedVersion = 10 },
			destroyingIgnore = { type = "table", default = {}, lastModifiedVersion = 10 },
			savedShoppingSearches = { type = "table", default = { filters = {}, name = {}, isFavorite = {} }, lastModifiedVersion = 96 },
			vendoringIgnore = { type = "table", default = {}, lastModifiedVersion = 10 },
			savedAuctioningSearches = { type = "table", default = { filters = {}, searchTypes = {}, name = {}, isFavorite = {} }, lastModifiedVersion = 96 },
		},
	},
	profile = {
		internalData = {
			createdDefaultOperations = { type = "boolean", default = false, lastModifiedVersion = 11 },
		},
		userData = {
			groups = { type = "table", default = {}, lastModifiedVersion = 10 },
			items = { type = "table", default = {}, lastModifiedVersion = 10 },
			operations = { type = "table", default = {}, lastModifiedVersion = 10 },
		},
		gatheringOptions = {
			sources = { type = "table", default = { "vendor", "guildBank", "alt", "altGuildBank", "craftProfit", "auction", "craftNoProfit" }, lastModifiedVersion = 32 },
		},
	},
	factionrealm = {
		internalData = {
			characterGuilds = { type = "table", default = {}, lastModifiedVersion = 10 },
			guildVaults = { type = "table", default = {}, lastModifiedVersion = 10 },
			pendingMail = { type = "table", default = {}, lastModifiedVersion = 10 },
			expiringMail = { type = "table", default = {}, lastModifiedVersion = 47 },
			expiringAuction = { type = "table", default = {}, lastModifiedVersion = 47 },
			mailDisenchantablesChar = { type = "string", default = "", lastModifiedVersion = 49 },
			mailExcessGoldChar = { type = "string", default = "", lastModifiedVersion = 49 },
			mailExcessGoldLimit = { type = "number", default = 10000000000, lastModifiedVersion = 49 },
			crafts = { type = "table", default = {}, lastModifiedVersion = 53 },
			mats = { type = "table", default = {}, lastModifiedVersion = 10 },
			guildGoldLog = { type = "table", default = {}, lastModifiedVersion = 25 },
			guildGoldLogLastUpdate = { type = "table", default = {}, lastModifiedVersion = 83 },
			csvAuctionDBScan = { type = "string", default = "", lastModifiedVersion = 50 },
			auctionDBScanTime = { type = "number", default = 0, lastModifiedVersion = 50 },
			auctionDBScanHash = { type = "number", default = 0, lastModifiedVersion = 50 },
			isCraftFavorite = { type = "table", default = {}, lastModifiedVersion = 56 },
		},
		coreOptions = {
			ignoreGuilds = { type = "table", default = {}, lastModifiedVersion = 10 },
		},
		auctioningOptions = {
			whitelist = { type = "table", default = {}, lastModifiedVersion = 10 },
		},
		gatheringContext = {
			crafter = { type = "string", default = "", lastModifiedVersion = 32 },
			professions = { type = "table", default = {}, lastModifiedVersion = 32 },
		},
		userData = {
			craftingCooldownIgnore = { type = "table", default = {}, lastModifiedVersion = 35 },
		},
	},
	realm = {
		internalData = {
			csvSales = { type = "string", default = "", lastModifiedVersion = 10 },
			csvBuys = { type = "string", default = "", lastModifiedVersion = 10 },
			csvIncome = { type = "string", default = "", lastModifiedVersion = 10 },
			csvExpense = { type = "string", default = "", lastModifiedVersion = 10 },
			csvExpired = { type = "string", default = "", lastModifiedVersion = 10 },
			csvCancelled = { type = "string", default = "", lastModifiedVersion = 10 },
			saveTimeSales = { type = "string", default = "", lastModifiedVersion = 10 },
			saveTimeBuys = { type = "string", default = "", lastModifiedVersion = 10 },
			saveTimeExpires = { type = "string", default = "", lastModifiedVersion = 10 },
			saveTimeCancels = { type = "string", default = "", lastModifiedVersion = 10 },
			accountingTrimmed = { type = "table", default = {}, lastModifiedVersion = 10 },
		},
	},
	char = {
		internalData = {
			auctionPrices = { type = "table", default = {}, lastModifiedVersion = 10 },
			auctionMessages = { type = "table", default = {}, lastModifiedVersion = 10 },
			craftingCooldowns = { type = "table", default = {}, lastModifiedVersion = 27 },
			auctionSaleHints = { type = "table", default = {}, lastModifiedVersion = 45 },
		},
		auctionUIContext = {
			auctioningGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
			shoppingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
		bankingUIContext = {
			warehousingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
			auctioningGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
			mailingGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
		craftingUIContext = {
			groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
		mailingUIContext = {
			groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
		vendoringUIContext = {
			groupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
		mainUIContext = {
			groupsManagementGroupTree = { type = "table", default = { collapsed = {} }, lastModifiedVersion = 71 },
			importGroupTree = { type = "table", default = { collapsed = {}, selected = {} }, lastModifiedVersion = 80 },
			exportGroupTree = { type = "table", default = { collapsed = {}, unselected = {} }, lastModifiedVersion = 80 },
		},
	},
	sync = {
		-- NOTE: whenever these are changed, the sync version needs to be increased in LibTSM/Services/SyncClasses/Constants.lua
		internalData = {
			money = { type = "number", default = 0, lastModifiedVersion = 74 },
			classKey = { type = "string", default = "", lastModifiedVersion = 19 },
			bagQuantity = { type = "table", default = {}, lastModifiedVersion = 19 },
			bankQuantity = { type = "table", default = {}, lastModifiedVersion = 19 },
			reagentBankQuantity = { type = "table", default = {}, lastModifiedVersion = 19 },
			auctionQuantity = { type = "table", default = {}, lastModifiedVersion = 19 },
			mailQuantity = { type = "table", default = {}, lastModifiedVersion = 19 },
			goldLog = { type = "string", default = "", lastModifiedVersion = 25 },
			goldLogLastUpdate = { type = "number", default = 0, lastModifiedVersion = 83 },
			playerProfessions = { type = "table", default = {}, lastModifiedVersion = 36 },
		},
	},
}



-- ============================================================================
-- Module Loading
-- ============================================================================

Settings:OnSettingsLoad(function()
	local db, upgradeObj = private.Constructor("TradeSkillMasterDB", SETTINGS_INFO)
	private.db = db
	if not upgradeObj then
		return
	end

	-- process DB upgrades
	local prevVersion = upgradeObj:GetPrevVersion()
	if prevVersion < 19 then
		-- migrate inventory data to the sync scope
		local oldInventoryData = TempTable.Acquire()
		local oldSyncMetadata = TempTable.Acquire()
		local oldAccountKey = TempTable.Acquire()
		local oldCharacters = TempTable.Acquire()
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, scopeKey, _, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "factionrealm" then
				if settingKey == "inventory" then
					oldInventoryData[scopeKey] = value
				elseif settingKey == "syncMetadata" then
					oldSyncMetadata[scopeKey] = value
				elseif settingKey == "accountKey" then
					oldAccountKey[scopeKey] = value
				elseif settingKey == "characters" then
					oldCharacters[scopeKey] = value
				end
			end
		end
		for factionrealm, characters in pairs(oldInventoryData) do
			local syncMetadata = oldSyncMetadata[factionrealm] and oldSyncMetadata[factionrealm].TSM_CHARACTERS
			for character, inventoryData in pairs(characters) do
				if not syncMetadata or not syncMetadata[character] or syncMetadata[character].owner == oldAccountKey[factionrealm] then
					db:NewSyncCharacter(character, db:GetSyncAccountKey(factionrealm), factionrealm)
					local syncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm)
					local class = oldCharacters[factionrealm] and oldCharacters[factionrealm][character]
					if type(class) == "string" then
						db:Set("sync", syncScopeKey, "internalData", "classKey", class)
					end
					db:Set("sync", syncScopeKey, "internalData", "bagQuantity", inventoryData.bag)
					db:Set("sync", syncScopeKey, "internalData", "bankQuantity", inventoryData.bank)
					db:Set("sync", syncScopeKey, "internalData", "reagentBankQuantity", inventoryData.reagentBank)
					db:Set("sync", syncScopeKey, "internalData", "auctionQuantity", inventoryData.auction)
					db:Set("sync", syncScopeKey, "internalData", "mailQuantity", inventoryData.mail)
				end
			end
		end
		TempTable.Release(oldInventoryData)
		TempTable.Release(oldSyncMetadata)
		TempTable.Release(oldAccountKey)
		TempTable.Release(oldCharacters)
	end
	if prevVersion < 25 then
		-- migrate gold log info
		local NEW_CSV_COLS = { "minute", "copper" }
		local function ConvertGoldLogFormat(data)
			local decodedData = select(2, CSV.Decode(data))
			if not decodedData then
				return
			end
			for _, entry in ipairs(decodedData) do
				local minute = entry.startMinute
				local copper = entry.copper
				wipe(entry)
				entry.minute = minute
				entry.copper = copper
			end
			return CSV.Encode(NEW_CSV_COLS, decodedData)
		end
		local function ProcessGoldLogData(character, data, scopeKey)
			if type(data) ~= "string" then
				return
			end
			-- check if we know about this character and under what faction
			local syncScopeKey = nil
			for factionrealm in db:FactionrealmByRealmIterator(scopeKey) do
				local testSyncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm)
				if db:Get("sync", testSyncScopeKey, "internalData", "classKey") then
					syncScopeKey = testSyncScopeKey
				end
			end
			if syncScopeKey then
				db:Set("sync", syncScopeKey, "internalData", "goldLog", ConvertGoldLogFormat(data))
			else
				-- check if this is a known guild
				local found = false
				for factionrealm in db:FactionrealmByRealmIterator(scopeKey) do
					local characterGuilds = db:Get("factionrealm", factionrealm, "internalData", "characterGuilds")
					if not found and characterGuilds and Table.KeyByValue(characterGuilds, character) then
						local guildGoldLog = db:Get("factionrealm", factionrealm, "internalData", "guildGoldLog") or {}
						guildGoldLog[character] = ConvertGoldLogFormat(data)
						db:Set("factionrealm", factionrealm, "internalData", "guildGoldLog", guildGoldLog)
						found = true
					end
				end
			end
		end
		if prevVersion >= 10 then
			for key, value in upgradeObj:RemovedSettingIterator() do
				local scopeType, scopeKey, _, settingKey = upgradeObj:GetKeyInfo(key)
				if scopeType == "realm" and settingKey == "goldLog" then
					for character, data in pairs(value) do
						ProcessGoldLogData(character, data, scopeKey)
					end
				end
			end
		end
	end
	if prevVersion < 36 then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, factionrealm, _, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "factionrealm" and settingKey == "playerProfessions" then
				for character, data in pairs(value) do
					-- check if we know about this character
					local syncScopeKey = db:GetSyncScopeKeyByCharacter(character, factionrealm)
					if db:Get("sync", syncScopeKey, "internalData", "classKey") then
						db:Set("sync", syncScopeKey, "internalData", "playerProfessions", data)
					end
				end
			end
		end
	end
	if prevVersion < 53 and not TSM.IsWowClassic() then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, factionrealm, namespace, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "factionrealm" and namespace == "internalData" and settingKey == "crafts" then
				db:Set("factionrealm", factionrealm, "internalData", "crafts", value)
			end
		end
	end
	if prevVersion < 64 then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, _, namespace, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "global" and namespace == "accountingOptions" and settingKey == "smartBuyPrice" and value then
				-- show a dialog to inform the user that this was removed
				StaticPopupDialogs["TSM_ACCOUNTING_SMART_AVG_REMOVED"] = {
					text = L["The 'use smart average for purchase price' setting has been removed from TSM and replaced with a new 'SmartAvgBuy' price source. Please update your custom prices appropriately."],
					button1 = OKAY,
					timeout = 0,
					whileDead = true,
				}
				Wow.ShowStaticPopupDialog("TSM_ACCOUNTING_SMART_AVG_REMOVED")
			end
		end
	end
	if prevVersion < 82 then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "global" and namespace == "craftingUIContext" and settingKey == "professionScrollingTable" then
				-- preserve the previous values
				local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey)
				for col, width in pairs(value.colWidth) do
					newTbl.colWidth[col] = width
				end
				for col, hidden in pairs(value.colHidden) do
					newTbl.colHidden[col] = hidden
				end
				if value.collapsed then
					for col, collapsed in pairs(value.collapsed) do
						newTbl.collapsed[col] = collapsed
					end
				end
			end
		end
	end
	if prevVersion < 89 then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "global" and namespace == "craftingOptions" and settingKey == "defaultCraftPriceMethod" then
				-- preserve the previous value
				db:Set(scopeType, scopeKey, namespace, settingKey, value)
			end
		end
	end
	if prevVersion < 96 then
		for key, value in upgradeObj:RemovedSettingIterator() do
			local scopeType, scopeKey, namespace, settingKey = upgradeObj:GetKeyInfo(key)
			if scopeType == "global" and namespace == "userData" and settingKey == "savedShoppingSearches" then
				-- convert how they are stored
				local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey)
				for i, searchInfo in ipairs(value) do
					local filter = searchInfo.filter
					if searchInfo.name ~= filter then
						newTbl.name[filter] = searchInfo.name
					end
					if searchInfo.isFavorite then
						newTbl.isFavorite[filter] = true
					end
					newTbl.filters[i] = filter
				end
			elseif scopeType == "global" and namespace == "userData" and settingKey == "savedAuctioningSearches" then
				-- convert how they are stored
				local newTbl = db:Get(scopeType, scopeKey, namespace, settingKey)
				for i, searchInfo in ipairs(value) do
					local filter = searchInfo.filter
					if searchInfo.name ~= filter then
						newTbl.name[filter] = searchInfo.name
					end
					if searchInfo.isFavorite then
						newTbl.isFavorite[filter] = true
					end
					newTbl.filters[i] = filter
					newTbl.searchTypes[i] = searchInfo.searchType
				end
			end
		end
	end
end)



-- ============================================================================
-- Module Functions
-- ============================================================================

-- TODO: get rid of this
function Settings.GetDB()
	assert(private.db)
	return private.db
end

function Settings.NewView()
	assert(private.db)
	return private.CreateView(private.db)
end

function Settings.Get(scope, scopeKey, namespace, key)
	return private.db:Get(scope, scopeKey, namespace, key)
end

function Settings.Set(scope, scopeKey, namespace, key, value)
	return private.db:Set(scope, scopeKey, namespace, key, value)
end

function Settings.GetCurrentSyncAccountKey()
	return private.db:GetSyncAccountKey()
end

function Settings.GetSyncScopeKeyByCharacter(character, factionrealm)
	return private.db:GetSyncScopeKeyByCharacter(character, factionrealm)
end

function Settings.GetCharacterSyncAccountKey(character)
	return private.context[private.db].db._syncOwner[private.db:GetSyncScopeKeyByCharacter(character)]
end

function Settings.ShowSyncSVCopyError()
	if time() - private.svCopyErrorTime < 60 then
		return
	end
	private.svCopyErrorTime = time()
	Log.PrintfUser(L["It appears that you've manually copied your saved variables between accounts which will cause TSM's automatic sync'ing to not work. You'll need to undo this, and/or delete the TradeSkillMaster saved variables files on both accounts (with WoW closed) in order to fix this."])
end

function Settings.CharacterByAccountFactionrealmIterator(account, factionrealm)
	factionrealm = factionrealm or SCOPE_KEYS.factionrealm
	account = account or private.db:GetSyncAccountKey(factionrealm)
	local result = TempTable.Acquire()
	for scopeKey, ownerAccount in pairs(private.context[private.db].db._syncOwner) do
		if ownerAccount == account then
			local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm))
			if character then
				tinsert(result, character)
			end
		end
	end
	return TempTable.Iterator(result)
end

function Settings.CharacterByFactionrealmIterator(factionrealm)
	factionrealm = factionrealm or SCOPE_KEYS.factionrealm
	local result = TempTable.Acquire()
	for scopeKey in pairs(private.context[private.db].db._syncOwner) do
		local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm))
		if character then
			tinsert(result, character)
		end
	end
	return TempTable.Iterator(result)
end

function Settings.IsCurrentAccountOwner(character)
	return Settings.GetCharacterSyncAccountKey(character) == Settings.GetCurrentSyncAccountKey()
end

function Settings.ConnectedFactionrealmAltCharacterIterator()
	local result = TempTable.Acquire()
	for factionrealm in private.db:GetConnectedRealmIterator("factionrealm") do
		for scopeKey in pairs(private.context[private.db].db._syncOwner) do
			local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm))
			if character and (factionrealm ~= SCOPE_KEYS.factionrealm or character ~= PLAYER) then
				tinsert(result, factionrealm)
				tinsert(result, character)
				tinsert(result, character..SCOPE_KEY_SEP..factionrealm)
			end
		end
	end
	return TempTable.Iterator(result, 3)
end

function Settings.SyncAccountIterator()
	local result = TempTable.Acquire()
	local used = TempTable.Acquire()
	for _, syncOwner in pairs(private.context[private.db].db._syncOwner) do
		if strmatch(syncOwner, "^"..String.Escape(SCOPE_KEYS.factionrealm..SCOPE_KEY_SEP).."(%d+)$") and not used[syncOwner] and syncOwner ~= Settings.GetCurrentSyncAccountKey() then
			used[syncOwner] = true
			tinsert(result, syncOwner)
		end
	end
	TempTable.Release(used)
	return TempTable.Iterator(result)
end

function Settings.NewSyncCharacter(accountKey, character)
	local factionrealm = SCOPE_KEYS.factionrealm
	assert(strmatch(accountKey, "^"..String.Escape(factionrealm..SCOPE_KEY_SEP).."(%d+)$"), "Invalid account key")
	local scopeKey = private.db:GetSyncScopeKeyByCharacter(character, factionrealm)
	local context = private.context[private.db]
	context.db._syncOwner[scopeKey] = accountKey
	if not tContains(context.db._scopeKeys.sync, scopeKey) then
		tinsert(context.db._scopeKeys.sync, scopeKey)
	end
	private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.sync, String.Escape(scopeKey), ".+", ".+"))
end

function Settings.RemoveSyncAccount(accountKey)
	local settingsDB = private.context[private.db].db
	assert(accountKey ~= private.db:GetSyncAccountKey())
	local scopeKeysToRemove = TempTable.Acquire()
	for scopeKey, ownerAccountKey in pairs(settingsDB._syncOwner) do
		if ownerAccountKey == accountKey then
			tinsert(scopeKeysToRemove, scopeKey)
		end
	end
	for _, scopeKey in ipairs(scopeKeysToRemove) do
		private.db:DeleteScope("sync", scopeKey)
		settingsDB._syncOwner[scopeKey] = nil
	end
	TempTable.Release(scopeKeysToRemove)
end

function Settings.RemoveSyncCharacter(character)
	local settingsDB = private.context[private.db].db
	local scopeKey = private.db:GetSyncScopeKeyByCharacter(character)
	private.db:DeleteScope("sync", scopeKey)
	settingsDB._syncOwner[scopeKey] = nil
end

function Settings.SyncSettingIterator()
	local result = TempTable.Acquire()
	for namespace, settings in pairs(private.context[private.db].settingsInfo.sync) do
		for settingKey in pairs(settings) do
			tinsert(result, namespace)
			tinsert(result, settingKey)
		end
	end
	return TempTable.Iterator(result, 2)
end

function Settings.FactionrealmCharacterIterator()
	return private.db:FactionrealmCharacterIterator()
end



-- ============================================================================
-- Main SettingsDB Class
-- ============================================================================

local PROTECTED_TABLE_MT = {
	__newindex = function(self, key, value)
		assert(private.protectedAccessAllowed[self], "Attempting to modify a protected table")
		rawset(self, key, value)
	end,
	__metatable = false
}

local SETTINGS_MT = {
	-- getter
	__index = function(self, key)
		if private.SettingsDBMethods[key] then
			return private.SettingsDBMethods[key]
		elseif SCOPE_TYPES[key] then
			return private.context[self].scopeProxies[key]
		else
			error("Invalid scope: "..tostring(key))
		end
	end,

	-- setter
	__newindex = function(self, key, value)
		error("You cannot set values in this table! You're probably missing a scope.")
	end,

	__metatable = false,
}

function private.Constructor(name, rawSettingsInfo)
	assert(type(name) == "string")
	assert(type(rawSettingsInfo) == "table")
	local version = rawSettingsInfo.version
	assert(type(version) == "number" and version >= 1)

	-- get (and create if necessary) the global table
	local db = _G[name]
	if not db then
		db = {}
		_G[name] = db
	end

	-- flatten and validate rawSettingsInfo and generate hash data
	local settingsInfo = CopyTable(rawSettingsInfo)
	local hashDataParts = TempTable.Acquire()
	local newLastModifiedVersion = TempTable.Acquire()
	for scope, scopeSettingsInfo in pairs(rawSettingsInfo) do
		if scope ~= "version" then
			assert(SCOPE_TYPES[scope], "Invalid scope: "..tostring(scope))
			for namespace, namespaceSettingsInfo in pairs(scopeSettingsInfo) do
				assert(type(namespace) == "string" and type(namespaceSettingsInfo) == "table")
				assert(not strfind(namespace, KEY_SEP))
				for key, info in pairs(namespaceSettingsInfo) do
					assert(type(key) == "string" and type(info) == "table", "Invalid type for key: "..tostring(key))
					assert(not strfind(key, KEY_SEP))
					for k, v in pairs(info) do
						if k == "type" then
							assert(VALID_TYPES[info.type], "Invalid type for key: "..key)
						elseif k == "default" then
							assert(type(v) == info.type, "Invalid default for key: "..key)
							if type(v) == "table" then
								private.CheckDefaultTable(v)
							end
						elseif k == "lastModifiedVersion" then
							assert(type(v) == "number" and v <= version, "Invalid lastModifiedVersion for key: "..key)
							newLastModifiedVersion[strjoin(KEY_SEP, SCOPE_TYPES[scope], namespace, key)] = v
						else
							error("Unexpected key in settingsInfo for key: "..key)
						end
					end
					tinsert(hashDataParts, strjoin(",", key, scope, namespace, info.type, type(info.default) == "table" and "table" or tostring(info.default)))
				end
			end
		end
	end
	sort(hashDataParts)
	local hash = Math.CalculateHash(table.concat(hashDataParts, ";"))
	TempTable.Release(hashDataParts)

	-- reset the DB if it's not valid
	local isValid = true
	if not next(db) then
		-- new DB
		isValid = false
	elseif not private.ValidateDB(db) then
		-- corrupted DB
		assert(not TSM.IsDevVersion(), "DB is not valid!")
		isValid = false
	elseif db._version == version and db._hash ~= hash then
		-- the hash didn't match
		assert(not TSM.IsDevVersion(), "Invalid settings hash! Did you forget to increase the version?")
		isValid = false
	elseif db._syncOwner and db._syncOwner[SCOPE_KEYS.sync] and db._syncOwner[SCOPE_KEYS.sync] ~= db._syncAccountKey[SCOPE_KEYS.factionrealm] then
		-- we aren't the owner of this character, so wipe the DB and show a manual error
		Settings.ShowSyncSVCopyError()
		assert(not TSM.IsDevVersion(), "Settings are corrupted due to manual copying of saved variables file")
		isValid = false
	end
	if not isValid then
		-- wipe the DB and start over
		wipe(db)
		for key, value in pairs(DEFAULT_DB) do
			db[key] = private.CopyData(value)
		end
	end
	db._hash = hash

	if not db._syncOwner then
		-- we just upgraded to the first version with the sync scope
		db._syncOwner = {}
		db._syncAccountKey = {}
		db._scopeKeys.sync = {}
	end

	-- make sure we have sync account keys for every factionrealm
	for _, factionrealm in ipairs(db._scopeKeys.factionrealm) do
		db._syncAccountKey[factionrealm] = db._syncAccountKey[factionrealm] or strjoin(SCOPE_KEY_SEP, factionrealm, random(time()))
	end
	-- create the sync account key for this factionrealm if necessary
	db._syncAccountKey[SCOPE_KEYS.factionrealm] = db._syncAccountKey[SCOPE_KEYS.factionrealm] or strjoin(SCOPE_KEY_SEP, SCOPE_KEYS.factionrealm, random(time()))
	-- set the sync owner of the current sync scope key to this account
	db._syncOwner[SCOPE_KEYS.sync] = db._syncOwner[SCOPE_KEYS.sync] or db._syncAccountKey[SCOPE_KEYS.factionrealm]

	-- setup current scope keys and set defaults for new keys
	db._currentProfile[SCOPE_KEYS.char] = db._currentProfile[SCOPE_KEYS.char] or DEFAULT_PROFILE_NAME
	local currentScopeKeys = CopyTable(SCOPE_KEYS)
	currentScopeKeys.profile = db._currentProfile[SCOPE_KEYS.char]
	for scopeType, scopeKey in pairs(currentScopeKeys) do
		if scopeType ~= "global" and not tContains(db._scopeKeys[scopeType], scopeKey) then
			tinsert(db._scopeKeys[scopeType], scopeKey)
			private.SetScopeDefaults(db, settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES[scopeType], String.Escape(scopeKey), ".+", ".+"))
		end
	end

	-- set any values which are nil to their default value
	db._scopeKeys = db._scopeKeys or {
		profile = {},
		realm = {},
		factionrealm = {},
		char = {},
		sync = {},
	}
	for scopeType, scopeKeys in pairs(db._scopeKeys) do
		for _, scopeKey in ipairs(scopeKeys) do
			for namespace, namespaceInfo in pairs(settingsInfo[scopeType]) do
				for settingKey, info in pairs(namespaceInfo) do
					local key = strjoin(KEY_SEP, SCOPE_TYPES[scopeType], scopeKey, namespace, settingKey)
					if db[key] == nil then
						private.SetDBKeyValue(db, key, private.CopyData(info.default))
					end
				end
			end
		end
	end
	for namespace, namespaceInfo in pairs(settingsInfo.global) do
		for settingKey, info in pairs(namespaceInfo) do
			local key = strjoin(KEY_SEP, SCOPE_TYPES.global, GLOBAL_SCOPE_KEY, namespace, settingKey)
			if db[key] == nil then
				private.SetDBKeyValue(db, key, private.CopyData(info.default))
			end
		end
	end

	-- do any necessary upgrading or downgrading if the version changed
	db._lastModifiedVersion = db._lastModifiedVersion or {}
	local removedSettings, prevVersion = nil, nil
	if version ~= db._version then
		-- clear any settings which no longer exist, and set new/updated settings to their default values
		removedSettings = {}
		for key in pairs(db) do
			-- ignore metadata (keys starting with "_")
			if strsub(key, 1, 1) ~= "_" then
				local scopeTypeShort, namespace, settingKey = strmatch(key, "^(.+)"..KEY_SEP..".+"..KEY_SEP.."(.+)"..KEY_SEP.."(.+)$")
				local settingLastModifiedVersion = scopeTypeShort and db._lastModifiedVersion[strjoin(KEY_SEP, scopeTypeShort, namespace, settingKey)]
				local scopeType = scopeTypeShort and private.ScopeReverseLookup(scopeTypeShort)
				local info = settingKey and settingsInfo[scopeType] and settingsInfo[scopeType][namespace] and settingsInfo[scopeType][namespace][settingKey]
				if not info then
					-- this setting was removed so remove it from the db
					removedSettings[key] = db[key]
					db[key] = nil
				elseif info.lastModifiedVersion > db._version then
					-- this setting was updated, so we'll reset it to the default value
					removedSettings[key] = db[key]
				elseif not settingLastModifiedVersion and version < db._version then
					-- we don't have lastModifiedVersion info for this setting and the DB is getting downgraded, so we'll reset it to the default value
					removedSettings[key] = db[key]
				elseif (settingLastModifiedVersion or 0) > version then
					-- this setting is being downgraded, so we'll reset it to the default value
					removedSettings[key] = db[key]
				end
			end
		end
		for scope, scopeInfo in pairs(settingsInfo) do
			if scope ~= "version" then
				for namespace, namespaceInfo in pairs(scopeInfo) do
					for settingKey, info in pairs(namespaceInfo) do
						local settingLastModifiedVersion = db._lastModifiedVersion[strjoin(KEY_SEP, SCOPE_TYPES[scope], namespace, settingKey)]
						if info.lastModifiedVersion > db._version or (not settingLastModifiedVersion and version < db._version) or (settingLastModifiedVersion or 0) > version then
							-- this is either a new setting or was changed or this is a downgrade - either way set it to the default value
							private.SetScopeDefaults(db, settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES[scope], ".+", namespace, settingKey))
						end
					end
				end
			end
		end
		if version > db._version then
			prevVersion = db._version
		else
			removedSettings = nil
		end
		db._version = version
	end

	-- populate the new lastModifiedVersion info
	wipe(db._lastModifiedVersion)
	for k, v in pairs(newLastModifiedVersion) do
		db._lastModifiedVersion[k] = v
	end
	TempTable.Release(newLastModifiedVersion)

	-- make the db table protected
	setmetatable(db, PROTECTED_TABLE_MT)

	-- create the new object and return it
	local new = setmetatable({}, SETTINGS_MT)
	private.context[new] = {
		db = db,
		settingsInfo = settingsInfo,
		currentScopeKeys = currentScopeKeys,
		callbacks = {},
		scopeProxies = {},
		namespaceProxies = {},
	}
	for scopeType, scopeInfo in pairs(rawSettingsInfo) do
		if scopeType ~= "version" then
			for namespace in pairs(scopeInfo) do
				private.context[new].namespaceProxies[scopeType..KEY_SEP..namespace] = private.CreateNamespace(new, namespace, scopeType)
			end
			private.context[new].scopeProxies[scopeType] = private.CreateScope(new, scopeType)
		end
	end
	local upgradeObj = nil
	if removedSettings then
		upgradeObj = setmetatable({}, private.SettingsDBUpgradeObjMT)
		assert(prevVersion)
		private.upgradeContext = {
			removedSettings = removedSettings,
			prevVersion = prevVersion,
		}
	end
	return new, upgradeObj
end



-- ============================================================================
-- Class for upgrade object
-- ============================================================================

private.SettingsDBUpgradeObjMT = {
	-- getter
	__index = {
		GetPrevVersion = function(self)
			return private.upgradeContext.prevVersion
		end,

		RemovedSettingIterator = function(self)
			return next, private.upgradeContext.removedSettings, nil
		end,

		GetKeyInfo = function(self, key)
			local scopeType, scopeKey, namespace, settingKey = nil, nil, nil, nil
			local parts = TempTable.Acquire(strsplit(KEY_SEP, key))
			if #parts == 4 then
				scopeType, scopeKey, namespace, settingKey = TempTable.UnpackAndRelease(parts)
				scopeType = private.ScopeReverseLookup(scopeType)
			elseif #parts == 3 then
				scopeType, scopeKey, settingKey = TempTable.UnpackAndRelease(parts)
				scopeType = private.ScopeReverseLookup(scopeType)
			else
				error("Unknown key: "..tostring(key))
			end
			return scopeType, scopeKey, namespace, settingKey
		end,
	},

	-- setter
	__newindex = function(self)
		error("You cannot set values in this table!")
	end,

	__metatable = false,
}



-- ============================================================================
-- SettingsDB Object Methods
-- ============================================================================

private.SettingsDBMethods = {
	Get = function(self, scope, scopeKey, namespace, key)
		assert(SCOPE_TYPES[scope] and type(namespace) == "string" and type(key) == "string", "Invalid parameters!")
		local context = private.context[self]
		assert(context.settingsInfo[scope][namespace][key], "Setting does not exist!")
		scopeKey = scopeKey or context.currentScopeKeys[scope]
		return context.db[strjoin(KEY_SEP, SCOPE_TYPES[scope], scopeKey, namespace, key)]
	end,

	Set = function(self, scope, scopeKey, namespace, key, value)
		assert(SCOPE_TYPES[scope] and type(namespace) == "string" and type(key) == "string", "Invalid parameters!")
		local context = private.context[self]
		local info = context.settingsInfo[scope][namespace][key]
		assert(info, "Setting does not exist!")
		assert(value == nil or type(value) == info.type, "Value is of wrong type.")
		scopeKey = scopeKey or context.currentScopeKeys[scope]
		private.SetDBKeyValue(context.db, strjoin(KEY_SEP, SCOPE_TYPES[scope], scopeKey, namespace, key), value)
	end,

	GetDefaultReadOnly = function(self, scope, namespace, key)
		local context = private.context[self]
		return context.settingsInfo[scope][namespace][key].default
	end,

	GetDefault = function(self, scope, namespace, key)
		return private.CopyData(self:GetDefaultReadOnly(scope, namespace, key))
	end,

	RegisterCallback = function(self, event, callback)
		assert(event == "OnProfileUpdated")
		assert(type(callback) == "function")
		private.context[self].callbacks[event] = callback
	end,

	IsValidProfileName = function(self, name)
		return name ~= "" and not strfind(name, KEY_SEP)
	end,

	ProfileExists = function(self, name)
		return tContains(private.context[self].db._scopeKeys.profile, name) and true or false
	end,

	GetCurrentProfile = function(self)
		return private.context[self].currentScopeKeys.profile
	end,

	GetScopeKeys = function(self, scope)
		return CopyTable(private.context[self].db._scopeKeys[scope])
	end,

	GetProfiles = function(self)
		return self:GetScopeKeys("profile")
	end,

	ProfileIterator = function(self)
		return ipairs(private.context[self].db._scopeKeys.profile)
	end,

	SetProfile = function(self, profileName, noCallback)
		assert(type(profileName) == "string", tostring(profileName))
		assert(not strfind(profileName, KEY_SEP))
		local context = private.context[self]

		-- change the current profile for this character
		context.db._currentProfile[SCOPE_KEYS.char] = profileName
		context.currentScopeKeys.profile = context.db._currentProfile[SCOPE_KEYS.char]

		local isNew = false
		if not tContains(context.db._scopeKeys.profile, profileName) then
			tinsert(context.db._scopeKeys.profile, profileName)
			-- this is a new profile, so set all the settings to their default values
			private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.profile, String.Escape(profileName), ".+", ".+"))
			isNew = true
		end

		if context.callbacks.OnProfileUpdated and not noCallback then
			context.callbacks.OnProfileUpdated(isNew)
		end
	end,

	ResetProfile = function(self)
		local context = private.context[self]
		private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.profile, String.Escape(context.currentScopeKeys.profile), ".+", ".+"))
		if context.callbacks.OnProfileUpdated then
			context.callbacks.OnProfileUpdated(true)
		end
	end,

	CopyProfile = function(self, sourceProfileName)
		assert(type(sourceProfileName) == "string")
		assert(not strfind(sourceProfileName, KEY_SEP))
		local context = private.context[self]
		assert(sourceProfileName ~= context.currentScopeKeys.profile)

		-- copy all the settings from the source profile to the current one
		for namespace, namespaceInfo in pairs(context.settingsInfo.profile) do
			for settingKey in pairs(namespaceInfo) do
				local srcKey = strjoin(KEY_SEP, SCOPE_TYPES.profile, sourceProfileName, namespace, settingKey)
				local destKey = strjoin(KEY_SEP, SCOPE_TYPES.profile, context.currentScopeKeys.profile, namespace, settingKey)
				private.SetDBKeyValue(context.db, destKey, private.CopyData(context.db[srcKey]))
			end
		end

		if context.callbacks.OnProfileUpdated then
			context.callbacks.OnProfileUpdated(false)
		end
	end,

	DeleteScope = function(self, scopeType, scopeKey)
		assert(SCOPE_TYPES[scopeType])
		assert(type(scopeKey) == "string")
		local context = private.context[self]
		assert(scopeKey ~= context.currentScopeKeys[scopeType])

		-- remove all settings for the specified profile
		local searchPattern = strjoin(KEY_SEP, SCOPE_TYPES[scopeType], String.Escape(scopeKey), ".+", ".+")
		for key in pairs(context.db) do
			if strmatch(key, searchPattern) then
				private.SetDBKeyValue(context.db, key, nil)
			end
		end

		-- remove the scope key from the list
		Table.RemoveByValue(context.db._scopeKeys[scopeType], scopeKey)
	end,

	DeleteProfile = function(self, profileName, defaultNewProfileName)
		self:DeleteScope("profile", profileName)
		-- move other characters which were on this profile to another one
		local context = private.context[self]
		if not defaultNewProfileName then
			defaultNewProfileName = context.db._scopeKeys.profile[1]
		end
		assert(defaultNewProfileName and tContains(context.db._scopeKeys.profile, defaultNewProfileName))
		for character, currentProfileName in pairs(context.db._currentProfile) do
			if currentProfileName == profileName then
				assert(character ~= SCOPE_KEYS.char)
				context.db._currentProfile[character] = defaultNewProfileName
			end
		end
	end,

	GetConnectedRealmIterator = function(self, scope)
		assert(scope == "factionrealm" or scope == "realm")
		return private.ConnectedRealmIterator, self, scope
	end,

	GetSyncAccountKey = function(self, factionrealm)
		factionrealm = factionrealm or SCOPE_KEYS.factionrealm
		return private.context[self].db._syncAccountKey[factionrealm]
	end,

	SyncAccountIterator = function(self)
		local result = TempTable.Acquire()
		local used = TempTable.Acquire()
		for _, syncOwner in pairs(private.context[self].db._syncOwner) do
			if strmatch(syncOwner, "^"..String.Escape(SCOPE_KEYS.factionrealm..SCOPE_KEY_SEP).."(%d+)$") and not used[syncOwner] and syncOwner ~= self:GetSyncAccountKey() then
				used[syncOwner] = true
				tinsert(result, syncOwner)
			end
		end
		TempTable.Release(used)
		return TempTable.Iterator(result)
	end,

	NewSyncCharacter = function(self, character, accountKey, factionrealm)
		factionrealm = factionrealm or SCOPE_KEYS.factionrealm
		assert(strmatch(accountKey, "^"..String.Escape(factionrealm..SCOPE_KEY_SEP).."(%d+)$"), "Invalid account key")
		local scopeKey = self:GetSyncScopeKeyByCharacter(character, factionrealm)
		local context = private.context[self]
		context.db._syncOwner[scopeKey] = accountKey
		if not tContains(context.db._scopeKeys.sync, scopeKey) then
			tinsert(context.db._scopeKeys.sync, scopeKey)
		end
		private.SetScopeDefaults(context.db, context.settingsInfo, strjoin(KEY_SEP, SCOPE_TYPES.sync, String.Escape(scopeKey), ".+", ".+"))
	end,

	RemoveSyncAccount = function(self, accountKey)
		local settingsDB = private.context[self].db
		assert(accountKey ~= self:GetSyncAccountKey())
		local scopeKeysToRemove = TempTable.Acquire()
		for scopeKey, ownerAccountKey in pairs(settingsDB._syncOwner) do
			if ownerAccountKey == accountKey then
				tinsert(scopeKeysToRemove, scopeKey)
			end
		end
		for _, scopeKey in ipairs(scopeKeysToRemove) do
			self:DeleteScope("sync", scopeKey)
			settingsDB._syncOwner[scopeKey] = nil
		end
		TempTable.Release(scopeKeysToRemove)
	end,

	RemoveSyncCharacter = function(self, character)
		local settingsDB = private.context[self].db
		local scopeKey = self:GetSyncScopeKeyByCharacter(character)
		self:DeleteScope("sync", scopeKey)
		settingsDB._syncOwner[scopeKey] = nil
	end,

	GetSyncOwnerAccountKey = function(self, character)
		return private.context[self].db._syncOwner[self:GetSyncScopeKeyByCharacter(character)]
	end,

	FactionrealmCharacterIterator = function(self, factionrealm)
		factionrealm = factionrealm or SCOPE_KEYS.factionrealm
		local result = TempTable.Acquire()
		for scopeKey in pairs(private.context[self].db._syncOwner) do
			local character = strmatch(scopeKey, "^(.+)"..String.Escape(SCOPE_KEY_SEP..factionrealm))
			if character then
				tinsert(result, character)
			end
		end
		return TempTable.Iterator(result)
	end,

	GetSyncScopeKeyByCharacter = function(self, character, factionrealm)
		return character..SCOPE_KEY_SEP..(factionrealm or SCOPE_KEYS.factionrealm)
	end,

	FactionrealmByRealmIterator = function(self, realm)
		return private.FactionrealmByRealmIteratorHelper, realm
	end,
}



-- ============================================================================
-- Proxy Class for Scopes (TSM.db.XXXXX)
-- ============================================================================

local SCOPE_MT = {
	-- getter
	__index = function(self, namespace)
		assert(type(namespace) == "string", "Invalid namespace type!")
		local proxyInfo = private.proxies[self]
		local context = private.context[proxyInfo.settingsDB]
		assert(context.settingsInfo[proxyInfo.scope][namespace], "Namespace does not exist!")
		local namespaceProxy = context.namespaceProxies[proxyInfo.scope..KEY_SEP..namespace]
		assert(namespaceProxy)
		return namespaceProxy
	end,

	-- setter
	__newindex = function(self, key, value)
		error("You cannot set values in this table! You're probably missing a namespace.")
	end,

	__metatable = false,
}



-- ============================================================================
-- Proxy Class for Namespaces (TSM.db.<scope>.XXXXX)
-- ============================================================================

local NAMESPACE_MT = {
	-- getter
	__index = function(self, key)
		assert(type(key) == "string", "Invalid setting key type!")
		local proxyInfo = private.proxies[self]
		return proxyInfo.settingsDB:Get(proxyInfo.scope, nil, proxyInfo.namespace, key)
	end,

	-- setter
	__newindex = function(self, key, value)
		local proxyInfo = private.proxies[self]
		proxyInfo.settingsDB:Set(proxyInfo.scope, nil, proxyInfo.namespace, key, value)
	end,

	__metatable = false,
}



-- ============================================================================
-- Setting View Class (see Settings.CreateView(...))
-- ============================================================================

local VIEW_METHODS = {
	AddKey = function(self, scopeType, namespace, key)
		local viewInfo = private.views[self]
		assert(viewInfo and not viewInfo.keyProxies[key])
		viewInfo.scopeNamespace[key] = scopeType..KEY_SEP..namespace
		viewInfo.keyProxies[key] = private.context[viewInfo.settingsDB].namespaceProxies[viewInfo.scopeNamespace[key]]
		return self
	end,
	RegisterCallback = function(self, key, callback)
		local viewInfo = private.views[self]
		assert(callback and not viewInfo.callbacks[key])
		viewInfo.callbacks[key] = callback
		return self
	end,
	GetDefaultReadOnly = function(self, key)
		local viewInfo = private.views[self]
		local scope, namespace = strsplit(KEY_SEP, viewInfo.scopeNamespace[key])
		assert(scope and namespace)
		return viewInfo.settingsDB:GetDefaultReadOnly(scope, namespace, key)
	end,
}

local VIEW_MT = {
	__index = function(self, key)
		if VIEW_METHODS[key] then
			return VIEW_METHODS[key]
		end
		return private.views[self].keyProxies[key][key]
	end,
	__newindex = function(self, key, value)
		private.views[self].keyProxies[key][key] = value
	end,
	__metatable = false,
}



-- ============================================================================
-- Helper Functions
-- ============================================================================

function private.CheckDefaultTable(tbl)
	for k, v in pairs(tbl) do
		assert(type(k) == "string" or type(k) == "number")
		if type(v) == "table" then
			private.CheckDefaultTable(v)
		end
	end
end

function private.CreateScope(settingsDB, scope)
	assert(private.context[settingsDB])
	local new = setmetatable({}, SCOPE_MT)
	private.proxies[new] = {
		settingsDB = settingsDB,
		scope = scope,
	}
	return new
end

function private.CreateNamespace(settingsDB, namespace, scope)
	assert(private.context[settingsDB])
	local new = setmetatable({}, NAMESPACE_MT)
	private.proxies[new] = {
		settingsDB = settingsDB,
		namespace = namespace,
		scope = scope,
	}
	return new
end

function private.CreateView(settingsDB)
	assert(private.context[settingsDB])
	local view = setmetatable({}, VIEW_MT)
	private.views[view] = {
		settingsDB = settingsDB,
		keyProxies = {},
		scopeNamespace = {},
		callbacks = {},
	}
	return view
end

function private.SetDBKeyValue(db, key, value)
	private.protectedAccessAllowed[db] = true
	db[key] = value
	private.protectedAccessAllowed[db] = nil
	local scopeType, _, namespace, settingKey = strsplit(KEY_SEP, key)
	if not settingKey then
		return
	end
	scopeType = private.ScopeReverseLookup(scopeType)
	for _, info in pairs(private.views) do
		if info.callbacks[settingKey] and info.scopeNamespace[settingKey] == scopeType..KEY_SEP..namespace then
			info.callbacks[settingKey]()
		end
	end
end

function private.CopyData(data)
	if type(data) == "table" then
		return CopyTable(data)
	elseif VALID_TYPES[type(data)] or type(data) == nil then
		return data
	end
end

function private.ScopeReverseLookup(scopeTypeShort)
	for key, value in pairs(SCOPE_TYPES) do
		if value == scopeTypeShort then
			return key
		end
	end
end

function private.ValidateDB(db)
	-- make sure the DB we are loading from is valid
	if #db > 0 then return end
	if type(db._version) ~= "number" then return end
	if type(db._hash) ~= "number" then return end
	if db._lastModifiedVersion ~= nil and type(db._lastModifiedVersion) ~= "table" then return end
	if type(db._scopeKeys) ~= "table" then return end
	for scopeType, keys in pairs(db._scopeKeys) do
		if not SCOPE_TYPES[scopeType] then return end
		for i, name in pairs(keys) do
			if type(i) ~= "number" or i > #keys or i <= 0 or type(name) ~= "string" then return end
		end
	end
	if type(db._currentProfile) ~= "table" then return end
	for key, value in pairs(db._currentProfile) do
		if type(key) ~= "string" or type(value) ~= "string" then return end
	end
	return true
end

function private.SetScopeDefaults(db, settingsInfo, searchPattern)
	-- remove any existing entries for matching keys
	for key in pairs(db) do
		if strmatch(key, searchPattern) then
			private.SetDBKeyValue(db, key, nil)
		end
	end

	local scopeTypeShort = strsub(searchPattern, 1, 1)
	local scopeType = private.ScopeReverseLookup(scopeTypeShort)
	assert(scopeType, "Couldn't find scopeType: "..tostring(scopeTypeShort))
	local scopeKeys = nil
	if scopeTypeShort == SCOPE_TYPES.global then
		scopeKeys = {GLOBAL_SCOPE_KEY}
	else
		scopeKeys = db._scopeKeys[scopeType]
		assert(scopeKeys, "Couldn't find scopeKeys for type: "..tostring(scopeTypeShort))
	end

	-- set any matching keys to their default values
	if not settingsInfo[scopeType] then return end
	for namespace, namespaceInfo in pairs(settingsInfo[scopeType]) do
		for settingKey, info in pairs(namespaceInfo) do
			for _, scopeKey in ipairs(scopeKeys) do
				local key = strjoin(KEY_SEP, scopeTypeShort, scopeKey, namespace, settingKey)
				if strmatch(key, searchPattern) then
					private.SetDBKeyValue(db, key, private.CopyData(info.default))
				end
			end
		end
	end
end

function private.ConnectedRealmIterator(self, prevScopeKey)
	if not private.cachedConnectedRealms then
		local connectedRealms = {}
		if not TSM.IsWowClassic() then
			local realmId, _, _, _, _, _, _, _, connectedRealmIds = LibRealmInfo:GetRealmInfo(REALM)
			if connectedRealmIds then
				for _, id in ipairs(connectedRealmIds) do
					if id ~= realmId then
						local _, connectedRealmName = LibRealmInfo:GetRealmInfoByID(id)
						tinsert(connectedRealms, connectedRealmName)
					end
				end
			end
		end
		private.cachedConnectedRealms = connectedRealms
	end
	local scope = nil
	if prevScopeKey == "factionrealm" or prevScopeKey == "realm" then
		-- this is the first time
		scope = prevScopeKey
		prevScopeKey = nil
	else
		scope = strmatch(prevScopeKey, String.Escape(FACTION.." - ")) and "factionrealm" or "realm"
	end
	local foundPrev = prevScopeKey == nil
	local index = 0
	while true do
		local realm = index == 0 and SCOPE_KEYS.realm or private.cachedConnectedRealms[index]
		if not realm then return end
		index = index + 1
		local scopeKey = (scope == "factionrealm") and (FACTION..SCOPE_KEY_SEP..realm) or realm
		if scopeKey == prevScopeKey then
			foundPrev = true
		elseif foundPrev and tContains(private.context[self].db._scopeKeys[scope], scopeKey) then
			return scopeKey
		end
	end
end

function private.FactionrealmByRealmIteratorHelper(realm, prevValue)
	if not prevValue then
		return strjoin(SCOPE_KEY_SEP, "Horde", realm)
	elseif strmatch(prevValue, "^Horde") then
		return strjoin(SCOPE_KEY_SEP, "Alliance", realm)
	elseif strmatch(prevValue, "^Alliance") then
		return strjoin(SCOPE_KEY_SEP, "Neutral", realm)
	end
end
