local _, addonTable = ...

local Serialization = {}

-- Upvalues
local R = Rarity

-- Externals
local compress = LibStub("LibCompress")
local L = LibStub("AceLocale-3.0"):GetLocale("Rarity")

-- Lua APIs
local tonumber = tonumber
local pairs = pairs
local type = type
local CONSTANTS = addonTable.constants
local FormatTime = Rarity.Utils.PrettyPrint.FormatTime -- Utils are loaded before Core modules, so this should be fine

--- Sets some default values for items (used before importing them)?
--- TODO: LuaDoc
function Serialization:CleanItemForImport(item)
	item.attempts = 0
	item.lastAttempts = 0
	item.enabled = true
	item.found = false
	item.enableAnnouncements = true
	item.holidayReminder = true
end

--- Returns true if an item has a valid format ?
-- TODO: LuaDoc
function Serialization:CanItemBeExportedImported(item)
	if not item then
		return false
	end
	if not item.method then
		return false
	end
	if not item.type then
		return false
	end

	if not item.itemId or tonumber(item.itemId) == nil or item.itemId <= 0 then
		return false
	end
	if not item.chance or tonumber(item.chance) == nil or item.chance <= 0 then
		return false
	end

	if item.method == CONSTANTS.DETECTION_METHODS.COLLECTION then
		if not item.collectedItemId or tonumber(item.collectedItemId) == nil or item.collectedItemId <= 0 then
			return false
		end
	end

	if item.type ~= CONSTANTS.ITEM_TYPES.ITEM then
		if not item.spellId or tonumber(item.spellId) == nil or item.spellId <= 0 then
			return false
		end
	end

	if item.type == CONSTANTS.ITEM_TYPES.PET then
		if not item.creatureId or tonumber(item.creatureId) == nil or item.creatureId <= 0 then
			return false
		end
	end

	if item.method == CONSTANTS.DETECTION_METHODS.ARCH then
		if not item.raceId or tonumber(item.raceId) == nil or item.raceId <= 0 then
			return false
		end
	end

	if item.method == CONSTANTS.DETECTION_METHODS.ZONE or item.method == CONSTANTS.DETECTION_METHODS.FISHING then
		if not item.zones or #item.zones <= 0 then
			return false
		end
	end

	if item.method == CONSTANTS.DETECTION_METHODS.USE then
		if not item.items or #item.items <= 0 then
			return false
		end
	end

	if item.method == CONSTANTS.DETECTION_METHODS.NPC or item.method == CONSTANTS.DETECTION_METHODS.BOSS then
		if (not item.npcs or #item.npcs <= 0) and (not item.statisticId or #item.statisticId <= 0) then
			return false
		end
	end

	return true
end

--- Attempts to import data from the BunnyHunter addon?
-- This must be ancient, I've never even heard of it before...
-- TODO: LuaDoc
function Serialization:ImportFromBunnyHunter()
	self = Rarity

	if self.db.profile.importedFromBunnyHunter then
		return
	end

	if BunnyHunterDB then
		StaticPopupDialogs["RARITY_IMPORT_FROM_BUNNYHUNTER"] = {
			text = L["Bunny Hunter is running. Would you like Rarity to import data from Bunny Hunter now? Disable Bunny Hunter or click Yes if you don't want to be asked again."],
			button1 = YES,
			button2 = NO,
			hideOnEscape = 1,
			timeout = 0,
			exclusive = 1,
			whileDead = 1,
			OnAccept = function()
				self = Rarity
				-- Do the import
				if BunnyHunterDB.loots and type(BunnyHunterDB.loots) == "table" then
					for k, v in pairs(BunnyHunterDB.loots) do
						for groupkey, group in pairs(self.db.profile.groups) do
							for itemkey, item in pairs(group) do
								if item.itemId == tonumber(k) then
									self:Debug("Resetting found record for %s", itemkey)
									item.finds = nil
									item.totalFinds = nil
								end
							end
						end
					end
					for k, v in pairs(BunnyHunterDB.loots) do
						for groupkey, group in pairs(self.db.profile.groups) do
							for itemkey, item in pairs(group) do
								if item.itemId == tonumber(k) then
									for kk, vv in pairs(v) do
										self:Debug(
											"%s: adding a kill after %d attempts, %s time",
											itemkey,
											vv.loots,
											FormatTime(vv.time)
										)
										if not item.finds then
											item.finds = {}
										end
										local count = 0
										for x, y in pairs(item.finds) do
											count = count + 1
										end
										table.insert(item.finds, {
											num = count + 1,
											totalAttempts = vv.loots,
											totalTime = vv.time,
											attempts = vv.loots,
											time = vv.time,
										})
										item.totalFinds = (item.totalFinds or 0) + 1
									end
								end
							end
						end
					end
				end
				if BunnyHunterDB.kills_by_id and type(BunnyHunterDB.kills_by_id) == "table" then
					for k, v in pairs(BunnyHunterDB.kills_by_id) do
						for groupkey, group in pairs(self.db.profile.groups) do
							for itemkey, item in pairs(group) do
								if item.npcs then
									for npckey, npcid in pairs(item.npcs) do
										if npcid == tonumber(k) then
											self:Debug("Resetting attempts for %s", itemkey)
											item.attempts = 0
											item.lastAttempts = nil
										end
									end
								end
							end
						end
					end
					for k, v in pairs(BunnyHunterDB.kills_by_id) do
						for groupkey, group in pairs(self.db.profile.groups) do
							for itemkey, item in pairs(group) do
								if item.npcs then
									for npckey, npcid in pairs(item.npcs) do
										if npcid == tonumber(k) then
											self:Debug("%s: adding %d attempt(s)", itemkey, v)
											item.attempts = (item.attempts or 0) + v
										end
									end
								end
							end
						end
					end
				end
				if BunnyHunterDB.times and type(BunnyHunterDB.times) == "table" then
					for k, v in pairs(BunnyHunterDB.times) do
						for groupkey, group in pairs(self.db.profile.groups) do
							for itemkey, item in pairs(group) do
								if item.itemId == tonumber(k) then
									self:Debug("%s: updating time to %f", itemkey, v)
									item.time = v
									item.lastTime = nil
								end
							end
						end
					end
				end
				self:UpdateInterestingThings()
				Rarity.GUI:UpdateText()
				-- if self:InTooltip() then self:ShowTooltip() end
				self.db.profile.importedFromBunnyHunter = true
				self:Print(L["Data has been imported from Bunny Hunter"])
			end,
		}
		StaticPopup_Show("RARITY_IMPORT_FROM_BUNNYHUNTER")
	end
end

-- Compression encoding
local encode_translate = { [255] = "\255\001", [0] = "\255\002" }

local function encode_helper(char)
	return encode_translate[char:byte()]
end

local decode_translate = { ["\001"] = "\255", ["\002"] = "\000" }

local function decode_helper(text)
	return decode_translate[text]
end

function R:Encode(data)
	return data:gsub("([\255%z])", encode_helper)
end

function R:Decode(data)
	return data:gsub("\255([\001\002])", decode_helper)
end

function R:Compress(data)
	return self:Encode(compress:Compress(data))
end

function R:Decompress(data)
	return compress:Decompress(self:Decode(data))
end

local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

function Serialization:DecodeBase64(encodedString)
	if not encodedString then
		return nil
	end

	encodedString = string.gsub(encodedString, "[^" .. b .. "=]", "")
	return (
		encodedString
			:gsub(".", function(x)
				if x == "=" then
					return ""
				end
				local r, f = "", (b:find(x) - 1)
				for i = 6, 1, -1 do
					r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and "1" or "0")
				end
				return r
			end)
			:gsub("%d%d%d?%d?%d?%d?%d?%d?", function(x)
				if #x ~= 8 then
					return ""
				end
				local c = 0
				for i = 1, 8 do
					c = c + (x:sub(i, i) == "1" and 2 ^ (8 - i) or 0)
				end
				return string.char(c)
			end)
	)
end

function Serialization:EncodeBase64(input)
	return (
		(input:gsub(".", function(x)
			local r, byte = "", x:byte()
			for i = 8, 1, -1 do
				r = r .. (byte % 2 ^ i - byte % 2 ^ (i - 1) > 0 and "1" or "0")
			end
			return r
		end) .. "0000"):gsub("%d%d%d?%d?%d?%d?", function(x)
			if #x < 6 then
				return ""
			end
			local c = 0
			for i = 1, 6 do
				c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0)
			end
			return b:sub(c + 1, c + 1)
		end) .. ({ "", "==", "=" })[#input % 3 + 1]
	)
end

function Serialization:DeserializeItemString(compressedEncodedItemString)
	local decodedItemString = self:DecodeBase64(compressedEncodedItemString)
	local decompressedItemString, errorMessage = self:Decompress(decodedItemString)
	assert(decompressedItemString, tostring(errorMessage))
	local success, resultOrError = R:Deserialize(decompressedItemString)
	assert(success, tostring(resultOrError))
	return resultOrError
end

Serialization.Decode = R.Decode
Serialization.Decompress = R.Decompress

Rarity.Serialization = Serialization
return Serialization
