-- Contains library functions that do not have a logical place.
---@class QuestieLib
local QuestieLib = QuestieLoader:CreateModule("QuestieLib");
-------------------------
--Import modules.
-------------------------
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB");
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer");

--Is set in QuestieLib.lua
QuestieLib.AddonPath = "Interface\\Addons\\QuestieDev-master\\";

local math_abs = math.abs;
local math_sqrt = math.sqrt;
local math_max = math.max;
local tinsert = table.insert

--[[
    Red: 5+ level above player
    Orange: 3 - 4 level above player
    Yellow: max 2 level below/above player
    Green: 3 - GetQuestGreenRange() level below player (GetQuestGreenRange() changes on specific player levels)
    Gray: More than GetQuestGreenRange() below player
--]]
function QuestieLib:PrintDifficultyColor(level, text)

    if level == -1 then
        level = QuestiePlayer:GetPlayerLevel();
    end
    local levelDiff = level - QuestiePlayer:GetPlayerLevel();

    if (levelDiff >= 5) then
        return "|cFFFF1A1A"..text.."|r"; -- Red
    elseif (levelDiff >= 3) then
        return "|cFFFF8040"..text.."|r"; -- Orange
    elseif (levelDiff >= -2) then
        return "|cFFFFFF00"..text.."|r"; -- Yellow
    elseif (-levelDiff <= GetQuestGreenRange()) then
        return "|cFF40C040"..text.."|r"; -- Green
    else
        return "|cFFC0C0C0"..text.."|r"; -- Grey
    end
end

function QuestieLib:GetDifficultyColorPercent(level)

    if level == -1 then
        level = QuestiePlayer:GetPlayerLevel();
    end
    local levelDiff = level - QuestiePlayer:GetPlayerLevel();

    if (levelDiff >= 5) then
        --return "|cFFFF1A1A"..text.."|r"; -- Red
        return 1, 0.102, 0.102
    elseif (levelDiff >= 3) then
        --return "|cFFFF8040"..text.."|r"; -- Orange
        return 1, 0.502, 0.251
    elseif (levelDiff >= -2) then
        --return "|cFFFFFF00"..text.."|r"; -- Yellow
        return 1, 1, 0
    elseif (-levelDiff <= GetQuestGreenRange()) then
        --return "|cFF40C040"..text.."|r"; -- Green
        return 0.251, 0.753, 0.251
    else
        --return "|cFFC0C0C0"..text.."|r"; -- Grey
        return 0.753, 0.753, 0.753
    end
end

-- 1.12 color logic
local function RGBToHex(r, g, b)
    if r > 255 then r = 255; end
    if g > 255 then g = 255; end
    if b > 255 then b = 255; end
    return string.format("|cFF%02x%02x%02x", r, g, b);
end

local function FloatRGBToHex(r, g, b)
    return RGBToHex(r*254, g*254, b*254);
end

function QuestieLib:GetRGBForObjective(Objective)
    if Objective.fulfilled ~= nil and Objective.Collected == nil then
        Objective.Collected = Objective.fulfilled
        Objective.Needed = Objective.required
    end

    if not Objective.Collected or type(Objective.Collected) ~= "number" then return QuestieLib:FloatRGBToHex(0.8, 0.8, 0.8); end
    local float = Objective.Collected / Objective.Needed
    local trackerColor = Questie.db.global.trackerColorObjectives
    
    if not trackerColor or trackerColor == "white" then
        return "|cFFEEEEEE";
    elseif trackerColor == "whiteAndGreen" then
        return Objective.Collected == Objective.Needed and RGBToHex(76, 255, 76) or QuestieLib:FloatRGBToHex(0.8, 0.8, 0.8)
    elseif trackerColor == "whiteToGreen" then
        return QuestieLib:FloatRGBToHex(0.8 - float / 2, 0.8 + float / 3, 0.8 - float / 2);
    else
        if float < .49 then return QuestieLib:FloatRGBToHex(1, 0 + float / .5, 0); end
        if float == .50 then return QuestieLib:FloatRGBToHex(1, 1, 0); end
        if float > .50 then return QuestieLib:FloatRGBToHex(1 - float / 2, 1, 0); end
    end
end

function QuestieLib:IsResponseCorrect(questId)
    local count = 0;
    local objectiveList = nil;
    local good = true;
    while (true and count < 50) do
        good = true;
        objectiveList = C_QuestLog.GetQuestObjectives(questId);
        if(objectiveList == nil) then
          good = false;
        else
          for objectiveIndex, objective in pairs(objectiveList) do
              if(objective.text == nil or objective.text == "" or QuestieLib:Levenshtein(": 0/1", objective.text) < 5) then
                  Questie:Debug(DEBUG_SPAM, count, " : Objective text is strange!", "'", objective.text, "'", " distance", QuestieLib:Levenshtein(": 0/1", objective.text));
                  good = false;
                  break;
              end
          end
        end
        if(good) then
            break;
        end
        count = count + 1;
    end
    return good;
end

function QuestieLib:GetQuestObjectives(questId)
    local count = 0;
    local objectiveList = nil;
    while (true and count < 50) do
        local good = true;
        objectiveList = C_QuestLog.GetQuestObjectives(questId);
        if(objectiveList == nil) then
            good = false;
        else
            for objectiveIndex, objective in pairs(objectiveList) do
                if(objective.text == nil or objective.text == "" or QuestieLib:Levenshtein(": 0/1", objective.text) < 5) then
                    Questie:Debug(DEBUG_SPAM, count, " : Objective text is strange!", "'", objective.text, "'", " distance", QuestieLib:Levenshtein(": 0/1", objective.text));
                    good = false;
                    break;
                end
            end
        end
        if(good) then
            break;
        end
        count = count + 1;
    end
    return objectiveList;
end

---@param id integer @The quest ID
---@param name string @The (localized) name of the quest
---@param level integer @The quest level
---@param showLevel integer @Wheather the quest level should be included
---@param isComplete boolean @Wheather the quest is complete
---@param blizzLike boolean @True = [40+], false/nil = [40D/R]
function QuestieLib:GetColoredQuestName(id, name, level, showLevel, isComplete, blizzLike)
    if showLevel then
        local questType, questTag = GetQuestTagInfo(id)

        if questType and questTag then
            local char = "+"
            if(not blizzLike) then
                char = string.sub(questTag, 1, 1);
            end

            local langCode = QuestieLocale:GetUILocale() -- the string.sub above doesn't work for multi byte characters in Chinese
            if questType == 1 then
                name = "[" .. level .. "+" .. "] " .. name -- Elite quest
            elseif questType == 81 then
                if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
                    char = "D"
                end
                name = "[" .. level .. char .. "] " .. name -- Dungeon quest
            elseif questType == 62 then
                if langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU" then
                    char = "R"
                end
                name = "[" .. level .. char .. "] " .. name -- Raid quest
            elseif questType == 41 then
                name = "[" .. level .. "] " .. name -- Which one? This is just default.
                --name = "[" .. level .. questTag .. "] " .. name -- PvP quest
            elseif questType == 83 then
                name = "[" .. level .. "++" .. "] " .. name -- Legendary quest
            else
                name = "[" .. level .. "] " .. name -- Some other irrelevant type
            end
        else
            name = "[" .. level .. "] " .. name
        end
    end
    if Questie.db.global.enableTooltipsQuestID then
        name = name .. " (" .. id .. ")"
    end
    if isComplete then
        if isComplete == -1 then
            name = name .. " (" .. _G['FAILED'] .. ")"
        else
            name = name .. " (" .. _G['COMPLETE'] .. ")"
        end
    end

    return QuestieLib:PrintDifficultyColor(level, name)
end

---@param waypointTable table<integer, Point> @A table containing waypoints {{X, Y}, ...}
---@return integer @X coordinate, 0-100
---@return integer @Y coordinate, 0-100
function QuestieLib:CalculateWaypointMidPoint(waypointTable)
    if(waypointTable) then
        local x = nil;
        local y = nil;
        local distanceList = {}
        local lastPos = nil
        local totalDistance = 0;
        for index, waypoint in pairs(waypointTable) do
            if(lastPos == nil) then
                lastPos = waypoint;
            else
                local distance = QuestieLib:Euclid(lastPos[1], lastPos[2], waypoint[1], waypoint[2]);
                totalDistance = totalDistance + distance;
                distanceList[distance] = index;
            end
        end

        --reset the last pos
        local ranDistance = 0;
        lastPos = nil
        for distance, index in pairs(distanceList) do
            if(lastPos == nil) then
                lastPos = index;
            else
                ranDistance = ranDistance + distance;
                if(ranDistance > totalDistance/2) then
                    local firstMiddle = waypointTable[lastPos];
                    local secondMiddle = waypointTable[index];
                    x = firstMiddle[1];--(firstMiddle[1] + secondMiddle[1])/2
                    y = firstMiddle[2]--(firstMiddle[2] + secondMiddle[2])/2
                    break;
                end
            end
        end
        return x, y;
    end
    return nil, nil;
end

function QuestieLib:ProfileFunction(functionReference, includeSubroutine)
    --Optional var
    if(not includeSubroutine) then includeSubroutine = true; end
    local now, count = GetFunctionCPUUsage(functionReference, includeSubroutine);
    --Questie:Print("[QuestieLib]", "Profiling Avg:", round(time/count, 6));
    return now, count;
end

--To try and create a fix for errors regarding items that do not exist in our DB,
--this function tries to prefetch all the items on startup and accept.
function QuestieLib:CacheAllItemNames()
    --[[
        1 name
        2 for quest
        3 dropped by
        [4103]={"Shackle Key",{630},{1559},{}},
    ]]
    local numEntries, numQuests = GetNumQuestLogEntries();
    for index = 1, numEntries do
        local title, level, _, isHeader, _, isComplete, _, questId, _, displayQuestId, _, _, _, _, _, _, _ = GetQuestLogTitle(index)
        if(not isHeader) then
            QuestieLib:CacheItemNames(questId);
        end
    end
end

function QuestieLib:CacheItemNames(questId)
    local quest = QuestieDB:GetQuest(questId);
    if(quest and quest.ObjectiveData) then
        for objectiveIndexDB, objectiveDB in pairs(quest.ObjectiveData) do
            if objectiveDB.Type == "item" then
                if not QuestieDB.itemData[objectiveDB.Id] then
                    Questie:Debug(DEBUG_DEVELOP, "Requesting item information for missing itemId:", objectiveDB.Id)
                    local item = Item:CreateFromItemID(objectiveDB.Id)
                    item:ContinueOnItemLoad(function()
                        local itemName = item:GetItemName();
                        --local itemName = GetItemInfo(objectiveDB.Id)
                        --Create an empty item with the name itself but no drops.
                        QuestieDB.itemData[objectiveDB.Id] = {itemName,{questId},{},{}};
                        Questie:Debug(DEBUG_DEVELOP, "Created item information for item:", itemName, ":", objectiveDB.Id);
                    end)
                end
            end
        end
    end
end

function QuestieLib:Euclid(x, y, i, e)
    local xd = math_abs(x - i);
    local yd = math_abs(y - e);
    return math_sqrt(xd * xd + yd * yd);
end

function QuestieLib:Maxdist(x, y, i, e)
    return math_max(math_abs(x - i), math_abs(y - e))
end

function QuestieLib:Remap(value, low1, high1, low2, high2)
    return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
end

local cachedTitle = nil;
--Move to Questie.lua after QuestieOptions move.
function QuestieLib:GetAddonVersionInfo()  -- todo: better place
    if(not cachedTitle) then
        local name, title, _, _, reason = GetAddOnInfo("QuestieDev-master");
        if(reason == "MISSING") then
            _, title = GetAddOnInfo("Questie");
        end
        cachedTitle = title;
    end
    --%d = digit, %p = punctuation character, %x = hexadecimal digits.
    local major, minor, patch, commit = string.match(cachedTitle, "(%d+)%p(%d+)%p(%d+)");
    return tonumber(major), tonumber(minor), tonumber(patch);
end

function QuestieLib:GetAddonVersionString()
    local major, minor, patch = QuestieLib:GetAddonVersionInfo()
    return "v" .. tostring(major) .. "." .. tostring(minor) .. "." .. tostring(patch)
end

--Search for just Addon\\ at the front since the interface part often gets trimmed
--Code Credit Author(s): Cryect (cryect@gmail.com), Xinhuan and their LibGraph-2.0 
do
    local path = string.match(debugstack(1, 1, 0), "AddOns\\(.+)Modules\\Libs\\QuestieLib.lua")
    if path then
        QuestieLib.AddonPath = "Interface\\AddOns\\"..path
  else
    local major, minor, patch, commit = QuestieLib:GetAddonVersionInfo();
        error("v"..major.."."..minor.."."..patch.."_"..commit.." cannot determine the folder it is located in because the path is too long and got truncated in the debugstack(1, 1, 0) function call")
  end
end

function QuestieLib:Count(table) -- according to stack overflow, # and table.getn arent reliable (I've experienced this? not sure whats up)
    local count = 0
    for k, v in pairs(table) do count = count + 1; end
    return count
end

-- Credits to Shagu and pfQuest, why reinvent the wheel.
-- https://gitlab.com/shagu/pfQuest/blob/master/compat/pfUI.lua
local sanitize_cache = {}
function QuestieLib:SanitizePattern(pattern)
  if not sanitize_cache[pattern] then
    local ret = pattern
    -- escape magic characters
    ret = gsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
    -- remove capture indexes
    ret = gsub(ret, "%d%$","")
    -- catch all characters
    ret = gsub(ret, "(%%%a)","%(%1+%)")
    -- convert all %s to .+
    ret = gsub(ret, "%%s%+",".+")
    -- set priority to numbers over strings
    ret = gsub(ret, "%(.%+%)%(%%d%+%)","%(.-%)%(%%d%+%)")
    -- cache it
    sanitize_cache[pattern] = ret
  end

  return sanitize_cache[pattern]
end

function QuestieLib:SortQuestsByLevel(quests)
    local sortedQuestsByLevel = {}

    local function compareTablesByIndex(a, b)
        return a[1] < b[1]
    end

    for _, q in pairs(quests) do
        tinsert(sortedQuestsByLevel, {q.questLevel, q})
    end
    table.sort(sortedQuestsByLevel, compareTablesByIndex)

    return sortedQuestsByLevel
end

---------------------------------------------------------------------------------------------------
-- Returns the Levenshtein distance between the two given strings
-- credit to https://gist.github.com/Badgerati/3261142
function QuestieLib:Levenshtein(str1, str2)
    local len1 = string.len(str1)
    local len2 = string.len(str2)
    local matrix = {}
    local cost = 0
    -- quick cut-offs to save time
    if (len1 == 0) then
        return len2
    elseif (len2 == 0) then
        return len1
    elseif (str1 == str2) then
        return 0
    end
    -- initialise the base matrix values
    for i = 0, len1, 1 do
        matrix[i] = {}
        matrix[i][0] = i
    end
    for j = 0, len2, 1 do
        matrix[0][j] = j
    end
    -- actual Levenshtein algorithm
    for i = 1, len1, 1 do
        for j = 1, len2, 1 do
            if (string.byte(str1,i) == string.byte(str2,j)) then
                cost = 0
            else
                cost = 1
            end
            matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost)
        end
    end
    -- return the last value - this is the Levenshtein distance
    return matrix[len1][len2]
end

-- 1.12 color logic
local function RGBToHex(r, g, b)
    if r > 255 then r = 255; end
    if g > 255 then g = 255; end
    if b > 255 then b = 255; end
    return string.format("|cFF%02x%02x%02x", r, g, b);
end

function QuestieLib:FloatRGBToHex(r, g, b)
    return RGBToHex(r*254, g*254, b*254);
end
