--Show a list of Dreamseed when approaching Emerald Bounty Soil.
--Show checkmark if the Plant's achievement criteria is complete
--10 yd range: Plant Seed

--Mechanism Explained:
--  Fire "VIGNETTE_MINIMAP_UPDATED" when an Emerald Bounty (growing or not growing) enters/leaves the minimap
--  Fire "UPDATE_UI_WIDGET" after planting a seed (Can be triggered by seeds planted across the entire map)
--  Cast hidden spell "Dreamseed (425856)" when you get to 6 yd, where you get dismounted and your cursor becomes "Investigate"
--  Taking off on Dragon with "Skyward Ascent (372610)" doesn't trigger "PLAYER_STARTED_MOVING", so we need to watch "UNIT_SPELLCAST_SUCCEEDED"

local _, addon = ...
local API = addon.API;
local GetPlayerMapCoord = API.GetPlayerMapCoord;
local GetCreatureIDFromGUID = API.GetCreatureIDFromGUID;
local QuickSlot = addon.QuickSlot;

local MAPID_EMRALD_DREAM = 2200;
local VIGID_BOUNTY = 5971;
local RANGE_PLANT_SEED = 10;
local FORMAT_ITEM_COUNT_ICON = "%s|T%s:0:0:0:0:64:64:0:64:0:64|t";
local SEED_ITEM_IDS = {208047, 208067, 208066};     --Gigantic, Plump, Small Dreamseed
local SEED_SPELL_IDS = {417508, 417645, 417642};
local QUICKSLOT_NAME = "dreamseed";

local math = math;
local sqrt = math.sqrt;
local format = string.format;
local pairs = pairs;
local ipairs = ipairs;
local C_VignetteInfo = C_VignetteInfo;
local GetVignetteInfo = C_VignetteInfo.GetVignetteInfo;
local GetVignettes = C_VignetteInfo.GetVignettes
local GetStatusBarWidgetVisualizationInfo = C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo;
local GetItemDisplayVisualizationInfo = C_UIWidgetManager.GetItemDisplayVisualizationInfo;
local GetAllWidgetsBySetID = C_UIWidgetManager.GetAllWidgetsBySetID;
local IsFlying = IsFlying;
local IsMounted = IsMounted;
local GetAchievementCriteriaInfoByID = GetAchievementCriteriaInfoByID;
local UIParent = UIParent;
local GetItemCount = C_Item.GetItemCount;
local GetItemIconByID = C_Item.GetItemIconByID;
local GetBestMapForUnit = C_Map.GetBestMapForUnit;
local time = time;
local GetTime = GetTime;
local UnitChannelInfo = UnitChannelInfo;

local IS_SEED_SPELL = {};
do
    for _, spellID in ipairs(SEED_SPELL_IDS) do
        IS_SEED_SPELL[spellID] = true;
    end
end

local function GetVisibleEmeraldBountyGUID()
    local vignetteGUIDs = GetVignettes();
    local info;

    for i, vignetteGUID in ipairs(vignetteGUIDs) do
        info = GetVignetteInfo(vignetteGUID);
        if info and info.vignetteID == VIGID_BOUNTY then
            if info.onMinimap then
                return vignetteGUID, info.objectGUID
            end
        end
    end
end

local DataProvider = {};
local EL = CreateFrame("Frame", nil, UIParent);

local QUICKSLOT_DATA = {
    buttons = {
        {actionType = "item", itemID = 208047, spellID = 417508},   --Gigantic
        {actionType = "item", itemID = 208067, spellID = 417645},   --Plump
        {actionType = "item", itemID = 208066, spellID = 417642},   --Small Dreamseed
    },
    systemName = QUICKSLOT_NAME,
    spellcastType = 2,      --Channel
};

function EL:IsTrackedPlantGrowing()
    return self.trackedObjectGUID and DataProvider:IsPlantGrowing(self.trackedObjectGUID);
end

function EL:AttemptShowUI()
    if self:IsTrackedPlantGrowing() then
        return
    end

    self:RegisterEvent("UPDATE_UI_WIDGET");
    self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player");
    self:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player");

    self.isChanneling = nil;

    QuickSlot:SetButtonData(QUICKSLOT_DATA);
    QuickSlot:ShowUI();

    if self.trackedObjectGUID then
        local plantName, criteriaComplete = DataProvider:GetPlantNameAndProgress(self.trackedObjectGUID);
        if plantName then
            --plantName = "|cff808080"..plantName.."|r";  --DISABLED_FONT_COLOR
            if criteriaComplete then
                plantName = "|TInterface/AddOns/Plumber/Art/Button/Checkmark-Green-Shadow:16:16:-4:-2|t"..plantName;  --"|A:common-icon-checkmark:0:0:-4:-2|a" |TInterface/AddOns/Plumber/Art/Button/Checkmark-Green:0:0:-4:-2|t
            end
            QuickSlot:SetHeaderText(plantName, true);
            QuickSlot:SetDefaultHeaderText(plantName);
        end
    else
        QuickSlot:SetDefaultHeaderText(nil);
    end

    return true
end

function EL:CloseUI()
    self:UnregisterEvent("UPDATE_UI_WIDGET");
    self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START");
    self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP");

    QuickSlot:RequestCloseUI(QUICKSLOT_NAME);
end

function EL:GetMapPointsDistance(x1, y1, x2, y2)
    local x = self.mapWidth * (x1 - x2);
    local y = self.mapHeight * (y1 - y2);

    return sqrt(x*x + y*y)
end

function EL:OnEvent(event, ...)
    if event == "VIGNETTE_MINIMAP_UPDATED" then
        local vignetteGUID, onMinimap = ...
        if vignetteGUID == self.trackedVignetteGUID then
            self:StopTrackingPosition();
        elseif onMinimap then
            local info = GetVignetteInfo(vignetteGUID);
            if info and info.vignetteID == VIGID_BOUNTY then
                self.trackedObjectGUID = info.objectGUID;
                self:UpdateTargetLocation(vignetteGUID);
            end
        end

    elseif event == "PLAYER_STARTED_MOVING" then
        --Fires like crazy when channeling seed (Repeat START/STOP Moving)
        --Doesn't fire when taking off on a dragon
        self.isPlayerMoving = true;
    elseif event == "PLAYER_STOPPED_MOVING" then
        self.isPlayerMoving = false;
        if not self.isChanneling then
            self:CalculatePlayerToTargetDistance();
        end
    elseif event == "PLAYER_MOUNT_DISPLAY_CHANGED" or event == "UNIT_SPELLCAST_SUCCEEDED" then
        self:CalculatePlayerToTargetDistance();
        if IsMounted() then
            self.isPlayerMoving = true;
        end
        if event == "UNIT_SPELLCAST_SUCCEEDED" then
            local _, _, spellID = ...
            if spellID and IS_SEED_SPELL[spellID] then
                --This event fires when start Channeling (and we don't have a SPELLCAST_CHANNEL equivalent, it doesn't make sense)
                --UNIT_SPELLCAST_CHANNEL_START fires before this event
                --UNIT_SPELLCAST_CHANNEL_STOP fires regardless of finishing cast or not
                --So we check if the the channel stops after endTime
                local name, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo("player");
                self.channelEndTime = endTime;
            else
                self.channelEndTime = nil;
            end
        end
    elseif event == "UPDATE_UI_WIDGET" then
        local widgetInfo = ...
        if DataProvider:IsValuableWidget(widgetInfo.widgetID) then
            if self:IsTrackedPlantGrowing() then
                self:StopTrackingPosition();
            end
        end
    elseif event == "UNIT_SPELLCAST_CHANNEL_START" then
        self.isChanneling = true;

    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
        self.isChanneling = nil;
        if self.channelEndTime then
            local t = GetTime();
            t = t * 1000;
            local diff = t - self.channelEndTime;
            if diff < 200 and diff > -200 then
                --Natural Complete
                C_Timer.After(0.2, function()
                    DataProvider:MarkNearestPlantContributed();
                end);
            else
                --Interrupted
            end
        end
    end
end
EL:SetScript("OnEvent", EL.OnEvent);

function EL:UpdateTargetLocation(vignetteGUID)
    local position, facing = C_VignetteInfo.GetVignettePosition(vignetteGUID, MAPID_EMRALD_DREAM);
    self.trackedVignetteGUID = vignetteGUID;
    if position and not self:IsTrackedPlantGrowing() then
        self.targetX, self.targetY = position.x, position.y;
        self:StartTrackingPosition();
    else
        self:StopTrackingPosition();
    end
end

function EL:UpdateTrackedVignetteInfo()
    local vignetteGUID, objectGUID = GetVisibleEmeraldBountyGUID();
    self.trackedObjectGUID = objectGUID;

    if vignetteGUID then
        if vignetteGUID ~= self.trackedVignetteGUID  then
            self:UpdateTargetLocation(vignetteGUID);
        end
    else
        self:StopTrackingPosition();
    end
end

function EL:OnUpdate(elapsed)
    self.t = self.t + elapsed;
    if self.t > self.t0 then
        self.t = 0;
        if self.isPlayerMoving then
            self:CalculatePlayerToTargetDistance();
        end
    end
end

function EL:StartTrackingPosition()
    self:RegisterEvent("PLAYER_STARTED_MOVING");
    self:RegisterEvent("PLAYER_STOPPED_MOVING");
    self:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED");     --In case player landing right on the soil
    self:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player");
    self.t = 0;
    self:CalculatePlayerToTargetDistance();
    self:SetScript("OnUpdate", self.OnUpdate);
end

function EL:StopTrackingPosition()
    if self.trackedVignetteGUID then
        self.trackedVignetteGUID = nil;
        self.trackedObjectGUID = nil;
        self.isPlayerMoving = nil;
        self.isChanneling = nil;
        self.isInRange = nil;
        self:UnregisterEvent("PLAYER_STARTED_MOVING");
        self:UnregisterEvent("PLAYER_STOPPED_MOVING");
        self:UnregisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED");
        self:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED");
        self:SetScript("OnUpdate", nil);
        self:OnLeavingSoil();
    end
end

function EL:UpdateMap()
    self.mapWidth, self.mapHeight = C_Map.GetMapWorldSize(MAPID_EMRALD_DREAM);
end
EL.mapWidth = 7477.1201171875;
EL.mapHeight = 4983.330078125;


function EL:CalculatePlayerToTargetDistance()
    self.playerX, self.playerY = GetPlayerMapCoord(MAPID_EMRALD_DREAM);
    if self.playerX and self.playerY then
        local d = self:GetMapPointsDistance(self.playerX, self.playerY, self.targetX, self.targetY);
        --print(format("Distance: %.1f yd", d));

        --Change update frequency dynamically
        if d <= 10 then
            self.t0 = 0.2;
        elseif d < 50 then
            self.t0 = 0.5;
        else
            self.t0 = 1;
        end

        if d <= RANGE_PLANT_SEED and not IsFlying() then
            if not self.isInRange then
                self.isInRange = true;
                self:OnApproachingSoil();
            end
        elseif self.isInRange then
            self.isInRange = false;
            self:OnLeavingSoil();
        end
    end
end

function EL:OnApproachingSoil()
    local success = self:AttemptShowUI();
    --Frame not shown if Growth Cycle has already begun
end

function EL:OnLeavingSoil()
    self:CloseUI();
end


local ZoneTriggerModule;

local function EnableModule(state)
    if state then
        if not ZoneTriggerModule then
            local module = API.CreateZoneTriggeredModule("quickslotseed");
            ZoneTriggerModule = module;
            module:SetValidZones(MAPID_EMRALD_DREAM);
            EL:UpdateMap();

            local function OnEnterZoneCallback()
                EL:RegisterEvent("VIGNETTE_MINIMAP_UPDATED");
                EL:UpdateTrackedVignetteInfo();
            end

            local function OnLeaveZoneCallback()
                EL:UnregisterEvent("VIGNETTE_MINIMAP_UPDATED");
                EL:StopTrackingPosition();
            end

            module:SetEnterZoneCallback(OnEnterZoneCallback);
            module:SetLeaveZoneCallback(OnLeaveZoneCallback);
        end
        ZoneTriggerModule:SetEnabled(true);
        ZoneTriggerModule:Update();
    else
        if ZoneTriggerModule then
            ZoneTriggerModule:SetEnabled(false);
        end
        EL:UnregisterEvent("VIGNETTE_MINIMAP_UPDATED");
        EL:StopTrackingPosition();
        EL:CloseUI();
    end
end

do
    local moduleData = {
        name = addon.L["ModuleName EmeraldBountySeedList"],
        dbKey = "EmeraldBountySeedList",
        description = addon.L["ModuleDescription EmeraldBountySeedList"],
        toggleFunc = EnableModule,
        categoryID = 1002,
        uiOrder = 1,
    };

    addon.ControlCenter:AddModule(moduleData);
end


local PLANT_DATA = {
    --17 types of Plant + 1 Tutorial
    --Kudos to @patf0rd on Twitter!

    --from VigInfo.ObjectGUID
    --/dump C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo(5136) (bag max 180(3 min))
    --C_UIWidgetManager.GetItemDisplayVisualizationInfo()
    --C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo()
    --shownState = 1, itemInfo = {showAsEarned = true}
    --AchievementID = 19013;    --I Dream of Seeds
    --https://warcraft.wiki.gg/wiki/UPDATE_UI_WIDGET
    --C_UIWidgetManager.GetAllWidgetsBySetID
    --DBC: Vignette - VisibleTrackingQuestID to find 3 chest for each plant

    --[creatureID] = {criteriaID, widgetID(Growth Cycle),   3 Nurture Widgets,  6 Reward Widgets(There are actually 12 item widgets but only need 6)(Small, Plump, Gigantic Bounty, Small, Medium, Large Bloom(Shared by 3 rarities)),   3 DreamseedChest[VigID] (based on quality)} --Purple/Blue/Green, 3 widgetSetIDs
    --          1      2        3     4     5        6     7     8     9     10    11       12    13    14       15   16   17
    [208443] = {62028, 5084,    4994, 5087, 5088,    5183, 5181, 5182, 5184, 5245, 5247,    5769, 5856, 5857,    869, 918, 919}, --"Ysera's Clover"
    [208511] = {62029, 5122,    4995, 5089, 5090,    5186, 5188, 5187, 5185, 5248, 5249,    5772, 5854, 5855,    870, 920, 921}, --"Chiming Foxglove"
    [208556] = {62030, 5123,    4996, 5091, 5092,    5179, 5172, 5180, 5173, 5250, 5251,    5773, 5853, 5853,    871, 922, 923}, --"Dragon's Daffodil"
    [208563] = {62031, 5125,    5000, 5095, 5096,    5194, 5195, 5196, 5193, 5254, 5255,    5775, 5848, 5849,    873, 926, 927}, --"Singing Weedling"
    [208605] = {62032, 5126,    5001, 5097, 5098,    5198, 5200, 5199, 5197, 5256, 5257,    5776, 5846, 5847,    879, 928, 929}, --"Fuzzy Licorice"
    [208606] = {62039, 5127,    5002, 5099, 5100,    5202, 5204, 5203, 5201, 5258, 5259,    5777, 5844, 5845,    875, 930, 931}, --"Lofty Lupin"
    [208607] = {62038, 5128,    5003, 5101, 5102,    5206, 5208, 5207, 5205, 5260, 5261,    5778, 5842, 5843,    876, 932, 933}, --"Ringing Rose"  (ok)
    [208615] = {62037, 5129,    5004, 5103, 5104,    5210, 5212, 5211, 5209, 5262, 5263,    5779, 5840, 5841,    877, 934, 935}, --"Dreamer's Daisy"  (ok)
    [208616] = {62035, 5130,    5005, 5105, 5106,    5214, 5216, 5215, 5213, 5264, 5265,    5780, 5838, 5839,    878, 936, 937}, --"Viridescent Sprout"
    [208617] = {62041, 5124,    4999, 5093, 5094,    5191, 5189, 5190, 5192, 5252, 5253,    5774, 5850, 5851,    872, 924, 925}, --"Belligerent Begonias"  --Sometimes not visible due to phasing?
    [209583] = {62027, 5131,    5075, 5108, 5109,    5218, 5220, 5219, 5217, 5266, 5267,    5782, 5783, 5784,    897, 938, 939}, --"Lavatouched Lilies" (ok)
    [209599] = {62040, 5132,    5076, 5107, 5110,    5222, 5224, 5223, 5221, 5268, 5269,    5787, 5788, 5789,    941, 898, 940}, --"Lullaby Lavender" (ok)
    [209880] = {62036, 5133,    5077, 5111, 5112,    5226, 5228, 5227, 5225, 5270, 5271,    5790, 5791, 5792,    899, 942, 943}, --"Glade Goldenrod"  (ok)
    [210723] = {62185, 5134,    5113, 5114, 5115,    5230, 5232, 5231, 5229, 5272, 5273,    5793, 5862, 5863,    944, 946, 945}, --"Comfy Chamomile"
    [210724] = {62186, 5135,    5116, 5117, 5118,    5234, 5236, 5235, 5233, 5274, 5275,    5864, 5865, 5866,    947, 948, 949}, --"Moon Tulip"
    [210725] = {62189, 5136,    5119, 5120, 5121,    5238, 5240, 5239, 5237, 5276, 5277,    5867, 5868, 5869,    950, 951, 952}, --"Flourishing Scurfpea"
    [211059] = {62397, 5149,    5146, 5147, 5148,    5242, 5244, 5243, 5241, 5278, 5279,    5876, 5877, 5878,    970, 971, 972}, --"Whisperbloom Sapling" ! (not spawning due to Superbloom phasing)

    --Ageless Blossom (criteriaID: 62396)
};

function DataProvider:IsValuableWidget(widgetID)
    if not self.valuableWidgets then
        self.valuableWidgets = {};
        for _, data in pairs(PLANT_DATA) do
            if data[2] then
                self.valuableWidgets[ data[2] ] = true
            end
        end
    end

    return widgetID and self.valuableWidgets[widgetID]
end

function DataProvider:GetGrowthTimesByCreatureID(creatureID)
    if creatureID and PLANT_DATA[creatureID] then
        local widgetID = PLANT_DATA[creatureID][2];
        local info = widgetID and GetStatusBarWidgetVisualizationInfo(widgetID);
        if info then
            return info.barValue, info.barMax
        end
    end
end

function DataProvider:GetGrowthTimes(objectGUID)
    local creatureID = GetCreatureIDFromGUID(objectGUID);
    return self:GetGrowthTimesByCreatureID(creatureID)
end

function DataProvider:IsPlantGrowingByCreatureID(creatureID)
    local remainingTime = self:GetGrowthTimesByCreatureID(creatureID)
    return remainingTime and remainingTime > 0
end

function DataProvider:IsPlantGrowing(objectGUID)
    local remainingTime = self:GetGrowthTimes(objectGUID);
    return remainingTime and remainingTime > 0
end

function DataProvider:GetPlantNameByCreatureID(creatureID)
    if creatureID and PLANT_DATA[creatureID] then
        local criteriaString = GetAchievementCriteriaInfoByID(19013, PLANT_DATA[creatureID][1]);
        return criteriaString
    end
end

function DataProvider:GetPlantNameAndProgress(objectGUID, isCreatureID)
    local id;
    if isCreatureID then
        id = objectGUID;
    else
        id = GetCreatureIDFromGUID(objectGUID);
    end
    if id and PLANT_DATA[id] then
        local criteriaString, criteriaType, completed = GetAchievementCriteriaInfoByID(19013, PLANT_DATA[id][1]);
        return criteriaString, completed
    end
end

function DataProvider.GetActiveDreamseedGrowthTimes()
    --This function shares between modules. (additional "Growth Cycle Timer" on PlayerChoiceFrame)
    --If this module is disabled "trackedVignetteGUID" will be nil and we to obtain it

    local vignetteGUID = EL.trackedVignetteGUID or DataProvider.lastVignetteGUID;

    if not vignetteGUID then
        vignetteGUID = GetVisibleEmeraldBountyGUID();
        DataProvider.lastVignetteGUID = vignetteGUID;
    end

    if vignetteGUID then
        local info = GetVignetteInfo(vignetteGUID);
        if info and info.vignetteID == VIGID_BOUNTY then
            if info.onMinimap then
                return DataProvider:GetGrowthTimes(info.objectGUID)
            end
        end
    end
end

function DataProvider:GetNurtureProgress(objectGUID, convertToString)
    local creatureID = GetCreatureIDFromGUID(objectGUID);
    if creatureID and PLANT_DATA[creatureID] then
        local widgetID = PLANT_DATA[creatureID][3];
        local info = widgetID and GetStatusBarWidgetVisualizationInfo(widgetID);
        if info and info.barValue and info.barMax then
            local barValue, barMax = info.barValue, info.barMax;
            if not (barValue and barMax) then return end;

            if barValue > barMax then
                barValue = barMax;
            end

            if convertToString then
                local str = info.text;
                if str then
                    str = str..": ".. barValue .. "/" ..barMax
                else
                    str = barValue .. "/" ..barMax
                end
                return str
            else
                return barValue, barMax
            end
        end
    end
end

function DataProvider:GetRewardTierByCreatureID(creatureID)
    local seedTier, bloomTier = 0, 0;
    if creatureID and PLANT_DATA[creatureID] then
        local info, widgetID, itemID;
        for i = 6, 8 do
            widgetID = PLANT_DATA[creatureID][i];
            info = widgetID and GetItemDisplayVisualizationInfo(widgetID);
            if info and info.shownState == 1 and info.itemInfo and info.itemInfo.showAsEarned then
                itemID = info.itemInfo.itemID;
                if itemID == 210217 then
                    seedTier = 1;   --Small Dreamy Bounty
                elseif itemID == 210218 then
                    seedTier = 2;   --Plump Dreamy Bounty
                elseif itemID == 210219 then
                    seedTier = 3;   --Gigantic Dreamy Bounty
                end
                break
            end
        end
        for i = 9, 11 do
            widgetID = PLANT_DATA[creatureID][i];
            info = widgetID and GetItemDisplayVisualizationInfo(widgetID);
            if info and info.shownState == 1 and info.itemInfo and info.itemInfo.showAsEarned then
                itemID = info.itemInfo.itemID;
                if itemID == 210224 then
                    bloomTier = 1;  --Small     <50
                elseif itemID == 210225 then
                    bloomTier = 2;  --Medium    <100
                elseif itemID == 210226 then
                    bloomTier = 3;  --Large     =100
                end
                break
            end
        end
    end
    return seedTier, bloomTier
end

function DataProvider:GetRewardTier(objectGUID)
    local creatureID = GetCreatureIDFromGUID(objectGUID);
    return self:GetRewardTierByCreatureID(creatureID)
end

function DataProvider:GetGrowthStateChanged(creatureID, growthRemainingSeconds)
    if not self.growthEndTimes then
        self.growthEndTimes = {};
    end

    if growthRemainingSeconds <= 0 then
        if self.growthEndTimes[creatureID] then
            return true
        else
            return false
        end
    end

    local isChanged = false;
    local currentTime = time();

    if self.growthEndTimes[creatureID] then
        if currentTime > self.growthEndTimes[creatureID] then
            isChanged = true;
        end
    else
        isChanged = true;
    end

    self.growthEndTimes[creatureID] = currentTime + growthRemainingSeconds;

    return isChanged
end

function DataProvider:Debug_ConstructWidgetID()
    --3 Bounty, 3 Blooms
    local REWARD_ITEMS = {210217, 210218, 210219, 210224, 210225, 210226};

    for creatureID in pairs(PLANT_DATA) do
        local info, widgetSetID, setWidgets;
        local itemIDxWidgetID = {};

        for i = 15, 17 do
            widgetSetID = PLANT_DATA[creatureID][i];
            setWidgets = GetAllWidgetsBySetID(widgetSetID);
            if setWidgets then
                local numItems = 0;
                for _, widgetInfo in ipairs(setWidgets) do
                    if widgetInfo.widgetType == 27 then
                        numItems = numItems + 1;
                        local widgetID = widgetInfo.widgetID;
                        info = GetItemDisplayVisualizationInfo(widgetID);
                        local itemID = info.itemInfo.itemID;
                        if not itemIDxWidgetID[itemID] then
                            itemIDxWidgetID[itemID] = widgetID;
                        end
                    end
                end
            end
        end

        local output;
        for i, itemID in ipairs(REWARD_ITEMS) do
            local widgetID = itemIDxWidgetID[itemID];
            if output then
                output = output..", "..widgetID;
            else
                output = widgetID;
            end
        end
        API.SaveDataUnderKey(creatureID, output);
    end
end

--[[
function DataProvider:HasAnyRewardByCreatureID(creatureID)
    --We now use a more RAM friendly (not getting all the widgets in a wigetSet), but less robust approach
    if creatureID and PLANT_DATA[creatureID] then
        if self:IsDreamseedChestAvailableByCreatureID(creatureID) then
            return true, true
        end
        local creatureData = PLANT_DATA[creatureID];
        local info;
        for i = 6, 11 do
            info = GetItemDisplayVisualizationInfo(creatureData[i]);
            if info and info.shownState == 1 and info.itemInfo and info.itemInfo.showAsEarned then
                return true, false
            end
        end
    end
    return false, false
end
--]]

function DataProvider:HasAnyReward(objectGUID)
    local creatureID = GetCreatureIDFromGUID(objectGUID);
    return self:HasAnyRewardByCreatureID(creatureID);
end

function DataProvider:GetResourcesText()
    --208066, 208067, 208047
    local info = C_CurrencyInfo.GetCurrencyInfo(2650);  --Emerald Dewdrop
    local anyNonZero = false;
    local text;

    if info then
        local quantity = info.quantity;
        if quantity > 0 then
            anyNonZero = true;
            if quantity > 9999 then
                quantity = "9999+";
            end
        else
            quantity = "|cff8080800|r";
        end
        text = format(FORMAT_ITEM_COUNT_ICON, quantity, info.iconFileID).."  ";
    else
        return
    end

    local count, icon;

    for _, itemID in ipairs(SEED_ITEM_IDS) do
        count = GetItemCount(itemID);
        icon = GetItemIconByID(itemID);
        if count == 0 then
            count = "|cff8080800|r";
        else
            anyNonZero = true;
        end
        text = text.."  "..format(FORMAT_ITEM_COUNT_ICON, count, icon);
    end

    if anyNonZero then
        return text
    else
        --we don't show this text if player has none of the required resources
    end
end

function DataProvider:GetChestOwnerCreatureIDs()
    local tbl = {};
    local vignetteID;
    for creatureID, data in pairs(PLANT_DATA) do
        for i = 12, 14 do
            vignetteID = data[i];
            tbl[vignetteID] = creatureID;
        end
    end
    return tbl
end

function DataProvider:SetChestStateByCreatureID(creatureID, state, objectGUID, x, y)
    if not creatureID then return end;

    if not self.dreamseedChestStates then
        self.dreamseedChestStates = {};
    end

    --local plantName = self:GetPlantNameAndProgress(creatureID, true);
    if state and not self.dreamseedChestStates[creatureID] then
        self.dreamseedChestStates[creatureID] = {objectGUID, x, y};
        if self.BackupCreaturePositions[creatureID] then
            self.BackupCreaturePositions[creatureID] = {x, y};  --overwrite our database in case Blizzard moves things
        end
    else
        self.dreamseedChestStates[creatureID] = nil;
    end

    if not state then
        --Player looted the chest
        self:SetPlantContributedByCreatureID(creatureID, nil);
    end
end

function DataProvider:SetChestState(objectGUID, state, x, y)
    local creatureID = GetCreatureIDFromGUID(objectGUID);
    self:SetChestStateByCreatureID(creatureID, state, objectGUID, x, y)
end

function DataProvider:TryGetChestInfoByCreatureID(creatureID)
    if self.dreamseedChestStates then
        return self.dreamseedChestStates[creatureID]
    end
end

function DataProvider:GetPlantCreatureIDs()
    local tbl = {};
    for creatureID in pairs(PLANT_DATA) do
        tbl[creatureID] = false;
    end
    return tbl
end

function DataProvider:GetBackupLocation(creatureID)
    return creatureID and self.BackupCreaturePositions[creatureID]
end

function DataProvider:EnumerateSpawnLocations()
    return pairs(self.BackupCreaturePositions)
end

function DataProvider:GetNearbyPlantInfo()
    local uiMapID = GetBestMapForUnit("player");
    if uiMapID ~= MAPID_EMRALD_DREAM then return end;

    local x, y = GetPlayerMapCoord(MAPID_EMRALD_DREAM);
    if x and y then
        EL:UpdateMap();
        local distance;
        for creatureID, position in self:EnumerateSpawnLocations() do
            distance = EL:GetMapPointsDistance(x, y, position[1], position[2]);
            if distance <= 15 then
                return creatureID, position[1], position[2]
            end
        end
    end
end

function DataProvider:BuildPlantIndexTable()
    local function SortByCriteriaID(a, b)
        return PLANT_DATA[a] < PLANT_DATA[b]
    end

    local plants = {};
    local n = 0;

    for creatureID, data in pairs(PLANT_DATA) do
        n = n + 1;
        plants[n] = creatureID;
    end

    table.sort(plants, SortByCriteriaID);

    local plantXIndex = {};
    for index, creatureID in ipairs(plants) do
        plantXIndex[creatureID] = index;
    end

    self.indexXPlant = plants;
    self.plantXIndex = plantXIndex;
end

function DataProvider:GetPlantIndexByCreatureID(creatureID)
    if not self.plantXIndex then
        self:BuildPlantIndexTable();
    end

    return self.plantXIndex[creatureID]
end

function DataProvider:GetPlantCreatureIDByIndex(index)
    if not self.indexXPlant then
        self:BuildPlantIndexTable();
    end

    return self.indexXPlant[index]
end

function DataProvider:SetPlantContributedByCreatureID(creatureID, chestFlag)
    --chestFlag:
    --nil (no chest)
    --1 Potential chest (data from WidgetInfo)
    --2 Chest (Player made contribution during current game session)
    if not self.plantContributed then
        self.plantContributed = {};
    end
    self.plantContributed[creatureID] = chestFlag
end

function DataProvider:HasAnyRewardByCreatureID(creatureID)
    --Alternative approach due to inconsistent data between actual spawn and widgetInfo
    --Check if player has successfully cast Dreamseed / made contributions using PlayerChoiceUI
    if creatureID and PLANT_DATA[creatureID] then
        if not self.plantContributed then
            self.plantContributed = {};
        end
        return self.plantContributed[creatureID] ~= nil
    end
    return false, false
end

function DataProvider:MarkNearestPlantContributed()
    local creatureID = self:GetNearbyPlantInfo();
    if creatureID then
        self:SetPlantContributedByCreatureID(creatureID, 2);
    end
end

function DataProvider:GetPlayerDistanceToTarget(playerX, playerY, targetX, targetY)
    return EL:GetMapPointsDistance(playerX, playerY, targetX, targetY);
end


---- Workaround for inconsistent Chest flag:
---- 1. When player logs in, query UIWidgetManager to get chests from previous game session
---- 2. We check if the chests actually exist when player gets to their approximate locations

function DataProvider:HasAnyPotentialRewardByCreatureID(creatureID)
    --We now use a more RAM friendly (not getting all the widgets in a wigetSet), but less robust approach
    if creatureID and PLANT_DATA[creatureID] then
        local creatureData = PLANT_DATA[creatureID];
        local info;
        for i = 6, 11 do
            info = GetItemDisplayVisualizationInfo(creatureData[i]);
            if info and info.shownState == 1 and info.itemInfo and info.itemInfo.showAsEarned then
                return true
            end
        end
    end
    return false
end

function DataProvider:GetChestOwnerCreatureID(vignetteID)
    if not self.chestOwnerCreatureIDs then
        self.chestOwnerCreatureIDs = self:GetChestOwnerCreatureIDs();
    end
    return vignetteID and self.chestOwnerCreatureIDs[vignetteID]
end

function DataProvider:IsDreamseedChest(vignetteID)
    return self:GetChestOwnerCreatureID(vignetteID) ~= nil
end

function DataProvider:RequestScanChests()
    if self.scanComplete then return end;

    if self.scanner then
        self.scanner.t = 0;
        self.scanner:Show();
        return
    end

    local anyPotentionReward;
    local targetList = {};

    local chestFlagAuto = 1;
    local numTargets = 0;

    for creatureID in pairs(PLANT_DATA) do
        if self:HasAnyPotentialRewardByCreatureID(creatureID) then
            anyPotentionReward = true;
            if self.BackupCreaturePositions[creatureID] then
                targetList[creatureID] = true;
                self:SetPlantContributedByCreatureID(creatureID, chestFlagAuto);
                numTargets = numTargets + 1;
            end
        end
    end

    --debugprint("numTargets", numTargets)


    --[[
        --Disable AB Testing (Dreamseed Chests Icon can still be stuck on the map, so we have to enable on-set scanning every game session)
    if PlumberDB then
        if PlumberDB.DreamseedChestABTesting == nil then
            PlumberDB.DreamseedChestABTesting = math.random(100) >= 50;
        end
        if not PlumberDB.DreamseedChestABTesting then
            self.scanComplete = true;
            return
        end
    end
    --]]

    if anyPotentionReward then
        if not self.scanner then
            self.scanner = CreateFrame("Frame");
            self.scanner.t = 0;
            self.scanner:SetScript("OnUpdate", function(f, elapsed)
                f.t = f.t + elapsed;
                if f.t > 3 then
                    f.t = 0;
                    local playerX, playerY = GetPlayerMapCoord(MAPID_EMRALD_DREAM);
                    local distance;
                    if playerX and playerY then
                        local anyTarget, nearbyCreatureID, location;

                        for creatureID in pairs(targetList) do
                            anyTarget = true;
                            location = self.BackupCreaturePositions[creatureID];
                            if location then
                                distance = self:GetPlayerDistanceToTarget(playerX, playerY, location[1], location[2]);
                                if distance <= 200 then
                                    nearbyCreatureID = creatureID;
                                    break
                                end
                            end
                        end

                        if not anyTarget then
                            f:SetScript("OnUpdate", nil);
                            f.t = nil;
                            self.scanComplete = true;
                            --debugprint("Scan Complete");
                        end

                        if nearbyCreatureID then
                            local vignetteGUIDs = GetVignettes();
                            local info, ownerCreatureID;
                            for i, vignetteGUID in ipairs(vignetteGUIDs) do
                                info = GetVignetteInfo(vignetteGUID);
                                if info then
                                    ownerCreatureID = self:GetChestOwnerCreatureID(info.vignetteID);
                                    if ownerCreatureID then
                                        --debugprint(ownerCreatureID, nearbyCreatureID)
                                    end
                                    if ownerCreatureID == nearbyCreatureID then
                                        targetList[ownerCreatureID] = nil;
                                        self:SetPlantContributedByCreatureID(nearbyCreatureID, 2);
                                        --debugprint("Found One");
                                        break
                                    end
                                end
                            end
                            if self.plantContributed[nearbyCreatureID] == chestFlagAuto then
                                if DataProvider:IsPlantGrowingByCreatureID(nearbyCreatureID) then
                                    --debugprint("Still Growing");
                                else
                                    self:SetPlantContributedByCreatureID(nearbyCreatureID, nil);
                                    --debugprint("Doesn't Exist");
                                end
                            end
                        end
                    end
                end
            end);
        end
    else
        self.scanComplete = true;
    end
end

function DataProvider:PauseScanner()
    if self.scanComplete then return end;

    if self.scanner then
        self.scanner:Hide();
    end
end


API.GetActiveDreamseedGrowthTimes = DataProvider.GetActiveDreamseedGrowthTimes;
API.DreamseedUtil = DataProvider;




DataProvider.BackupCreaturePositions = {
    [208605] = {
        0.634965717792511,
        0.4709928631782532,
    },
    [209880] = {
        0.4074325561523438,
        0.4348400235176086,
    },
    [210725] = {
        0.4873448014259338,
        0.8045594692230225,
    },
    [208563] = {
        0.6302892565727234,
        0.5284437537193298,
    },
    [208443] = {
        0.5924164056777954,
        0.5876181125640869,
    },
    [208606] = {
        0.5665966272354126,
        0.4488694071769714,
    },
    [211059] = {
        0.5114631652832031,
        0.5863984823226929,
    },
    [208556] = {
        0.6395775675773621,
        0.6483616232872009,
    },
    [209583] = {
        0.4067537784576416,
        0.2478460669517517,
    },
    [209599] = {
        0.5651239156723022,
        0.3766665458679199,
    },
    [208615] = {
        0.4638132452964783,
        0.4048779606819153,
    },
    [208511] = {
        0.5459136962890625,
        0.6763055324554443,
    },
    [208617] = {
        0.4990068674087524,
        0.3544299006462097,
    },
    [208616] = {
        0.400239109992981,
        0.5268844366073608,
    },
    [210724] = {
        0.4264156222343445,
        0.740414023399353,
    },
    [208607] = {
        0.4916412830352783,
        0.4806915521621704,
    },
    [210723] = {
        0.3845012784004211,
        0.5920345783233643,
    },
};

---- Dev Tool
--[[
do
    function YeetWidget_StatusBar()
        local GetStatusBarWidgetVisualizationInfo = C_UIWidgetManager.GetStatusBarWidgetVisualizationInfo;
        local info;
        local n = 0;
        for widgetID = 5000, 5200 do
            info = GetStatusBarWidgetVisualizationInfo(widgetID);
            if info and info.barMax ~= 180 and info.barValue > 0 then
                n = n + 1;
                print("#"..n, widgetID, info.text);
            end
        end
    end

    function YeetWidgetInfo()
        for widgetID, widgetType in pairs(EL.widgetData) do
            print("ID:", widgetID, "  Type:", widgetType)
        end
    end

    function YeetVignette()
        local vignetteGUIDs = C_VignetteInfo.GetVignettes();
        local info, position;

        local vignettesGUIDs = {};
        local total = 0;

        for i, guid in ipairs(vignetteGUIDs) do
            info = C_VignetteInfo.GetVignetteInfo(guid);
            if info and info.name then
                total = total + 1;
                vignettesGUIDs[total] = info.vignetteGUID;
                if info.name == "Dreamseed Chest" and true then
                    print(total, format("#%s  type:%s  %s  WorldMap %s  Minimap %s  Unique %s  %s", info.vignetteID, info.type, info.name, tostring(info.onWorldMap), tostring(info.onMinimap), tostring(info.isUnique), info.vignetteGUID));
                end
            end
        end

        local bestUniqueVignetteIndex = C_VignetteInfo.FindBestUniqueVignette(vignettesGUIDs);
        print("Show ",bestUniqueVignetteIndex);
        if bestUniqueVignetteIndex then
            info = C_VignetteInfo.GetVignetteInfo( vignettesGUIDs[bestUniqueVignetteIndex] );
            print(info.atlasName);
        end
    end

    function YeetDistance()
        local uiMapID = C_Map.GetBestMapForUnit("player");
        local trueDistance = C_Navigation.GetDistance();

        local waypoint = C_Map.GetUserWaypoint();
        local x0, y0 = waypoint.position.x, waypoint.position.y;

        local playerPosition = C_Map.GetPlayerMapPosition(uiMapID, "player");
        local x1, y1 = playerPosition:GetXY();
        local width, height = C_Map.GetMapWorldSize(uiMapID);

        local distance = math.sqrt( ((x1 -x0)*width)^2 + ((y1 -y0)*height)^2 );
        print(trueDistance, distance);
    end

    function TestStatusBar()
        if not PlumberTestStatusBar then
            PlumberTestStatusBar = addon.CreateTimerFrame(UIParent);
            PlumberTestStatusBar:SetPoint("CENTER", UIParent, "CENTER", 0, 0);
            PlumberTestStatusBar:SetStyle(2);
            PlumberTestStatusBar:SetWidth(192);
            PlumberTestStatusBar:UpdateMaxBarFillWidth();
            PlumberTestStatusBar:SetReverse(true);
            PlumberTestStatusBar:SetContinuous(false);
        end
        PlumberTestStatusBar:SetDuration(180);
    end

    function TestTinyBar()
        if not PlumberTestStatusBar then
            PlumberTestStatusBar = addon.CreateTinyStatusBar(UIParent);
            PlumberTestStatusBar:SetPoint("CENTER", UIParent, "CENTER", 0, 0);
            PlumberTestStatusBar:UpdateMaxBarFillWidth();
            PlumberTestStatusBar:SetReverse(true);
        end
        PlumberTestStatusBar:SetDuration(10);
    end
end
--]]

--[[
local UiWidgets_Reward_Bounty = {
    --{Small, Plump, Gigantic Bounty, Small, Medium, Large Bloom}
    {5179, 5172, 5180, 5173, 5245, 5247},
    {5183, 5181, 5182, 5184, 5248, 5249},
    {5186, 5188, 5187, 5185, 5250, 5251},
    {5191, 5189, 5190, 5192, 5252, 5253},
    {5194, 5195, 5196, 5193, 5254, 5255},
    {5198, 5200, 5199, 5197, 5256, 5257},
    {5202, 5204, 5203, 5201, 5258, 5259},
    {5206, 5208, 5207, 5205, 5260, 5261},
    {5210, 5212, 5211, 5209, 5262, 5263},
    {5214, 5216, 5215, 5213, 5264, 5265},
    {5218, 5220, 5219, 5217, 5266, 5267},
    {5222, 5224, 5223, 5221, 5268, 5269},
    {5226, 5228, 5227, 5225, 5270, 5271},
    {5230, 5232, 5231, 5229, 5272, 5273},
    {5234, 5236, 5235, 5233, 5274, 5275},
    {5238, 5240, 5239, 5237, 5276, 5277},
    {5242, 5244, 5243, 5241, 5278, 5279},
};
--]]