local _, addon = ...
local API = addon.API;
local L = addon.L;
local SetDBValue = addon.SetDBValue;
local GetDBValue = addon.GetDBValue;
local Mixin = API.Mixin;

local MapFrame = WorldMapFrame;
local TooltipFrame = GameTooltip;
local InCombatLockdown = InCombatLockdown;
local C_UIWidgetManager = C_UIWidgetManager;

local PIN_TEMPLATE_NAME = "PlumberWorldMapPinTemplate";

local PinController = CreateFrame("Frame");     --Events driven, updates Pin
addon.MapPinController = PinController;
local MapTracker = CreateFrame("Frame");        --Attach to WorldMapFrame to monitor changes to map, scale...
MapTracker:Hide();
local WorldMapDataProvider = CreateFromMixins(MapCanvasDataProviderMixin);
local BlizzardUIUtil = {};


do  --BlizzardUIUtil
    local QuestDetailsFrame = QuestMapFrame and QuestMapFrame.DetailsFrame;

    function BlizzardUIUtil:IsViewingQuestDetails()
        return QuestDetailsFrame and QuestDetailsFrame:IsVisible();
    end

    --Map Filter Menu
    function BlizzardUIUtil:HookIntoMenu()
        if not self.isMenuHooked then
            self.isMenuHooked = true;
        else
            return
        end

        self.isMenuHooked = true;

        if Menu and Menu.ModifyMenu then
            Menu.ModifyMenu("MENU_WORLD_MAP_TRACKING", function(owner, rootDescription, contextData)
                if not (PinController.isEnabled and PinController.pinEnabled) then return end;

                local options = PinController:GetFilterOptionsForCurrentMap();
                if options then
                    rootDescription:CreateDivider();
                    rootDescription:CreateTitle("Plumber");
                    for _, option in ipairs(options) do
                        --rootDescription:CreateButton("Button", function() print("Text here!" end);
                        local function IsSelected()
                            return GetDBValue(option.dbKey);
                        end

                        local function SetSelected()
                            local newState = not GetDBValue(option.dbKey);
                            SetDBValue(option.dbKey, newState);
                            PinController:EnableMapDataProvider(option.dbKey, newState);
                            PinController:RequestUpdate();
                        end

                        local checkbox = rootDescription:CreateCheckbox(option.name, IsSelected, SetSelected);

                        checkbox:AddInitializer(function(button, description, menu)
                            local rightTexture = button:AttachTexture();
                            rightTexture:SetPoint("RIGHT");
                            option.iconSetupFunc(rightTexture);

                            local fontString = button.fontString;
                            fontString:SetPoint("RIGHT", rightTexture, "LEFT");
                            fontString:SetTextColor(1, 1, 1);

                            local pad = 20;
                            local width = pad + fontString:GetUnboundedStringWidth() + rightTexture:GetWidth();
                            local height = 20;

                            return width, height;
                        end);
                    end

                    local submenu = rootDescription:CreateButton(L["Pin Size"]);

                    local function IsSelected(index)
                        return index == PinController:GetPinSizeIndex()
                    end

                    local function SetSelected(index)
                        SetDBValue("WorldMapPin_Size", index, true);
                        return (MenuResponse and MenuResponse.Refresh) or 2
                    end

                    for index, text in ipairs(PinController:GetPinSizeChoices()) do
                        submenu:CreateRadio(text, IsSelected, SetSelected, index);
                    end
                end
            end);
        end
    end
end


do  --PinController
    PinController.pins = {};
    PinController.mapDataProviders = {};
    PinController.dbKeyXdataprovider = {};

    local tinsert = table.insert;
    local ipairs = ipairs;

    function PinController:AddPin(pin)
        tinsert(self.pins, pin);
    end

    function PinController:AddMapDataProvider(mapID, dataProvider)
        if not self.mapDataProviders[mapID] then
            self.mapDataProviders[mapID] = {};
        end
        tinsert(self.mapDataProviders[mapID], dataProvider);

        if not self.dbKeyXdataprovider[dataProvider.OptionData.dbKey] then
            self.dbKeyXdataprovider[dataProvider.OptionData.dbKey] = dataProvider;
        else
            print(string.format("Plumber: Duplicated MapDataProvider Key %s", dataProvider.OptionData.dbKey));
        end
    end

    function PinController:InitializeSubModules()
        local anyEnabled;

        if self.mapDataProviders then
            local masterState = GetDBValue("WorldMapPin_TWW");
            self.pinEnabled = masterState;
            for mapID, dataProviders in pairs(self.mapDataProviders) do
                for _, dataProvider in ipairs(dataProviders) do
                    dataProvider.enabled = masterState and GetDBValue(dataProvider.OptionData.dbKey);
                    if dataProvider.enabled then
                        anyEnabled = true;
                    end
                end
            end
        end

        if self.mapProcessors then
            for processors in pairs(self.mapProcessors) do
                processors.enabled = GetDBValue(processors.dbKey);
                if processors.enabled then
                    anyEnabled = true;
                end
            end
        end

        if anyEnabled then
            MapTracker:Attach();
            self.isEnabled = true;
            BlizzardUIUtil:HookIntoMenu();
        else
            MapTracker:Detach();
            self.isEnabled = false;
            self:ListenEvents(false);
        end
    end

    function PinController:EnableMapDataProvider(dbKey, state)
        local dataProvider = self.dbKeyXdataprovider[dbKey];
        if dataProvider then
            dataProvider.enabled = state == true or state == nil;
        end
    end

    function PinController:DoesMapHaveData(mapID)
        if mapID and self.mapDataProviders[mapID] then
            for _, dataProvider in ipairs(self.mapDataProviders[mapID]) do
                if dataProvider.enabled then
                    return true
                end
            end
        end
    end

    function PinController:UpdatePinForMap(mapID)
        local pin, pinData;
        local anyPins = false;
        local pinSizeScale = self:GetPinSizeScale();

        for i, dataProvider in ipairs(self.mapDataProviders[mapID]) do
            if dataProvider.enabled then
                pinData = dataProvider:GetPinDataForMap(mapID);
                if pinData then
                    for _, data in ipairs(pinData) do
                        pin = MapFrame:AcquirePin(PIN_TEMPLATE_NAME, data);
                        if pin then
                            anyPins = true;
                            pin:SetSizeScale(pinSizeScale);
                        end
                    end
                end
            end
        end

        return anyPins
    end

    function PinController:GetFilterOptionsForCurrentMap()
        local mapID = MapTracker:GetMapID();
        if mapID and self.mapDataProviders[mapID] then
            local options;
            local n = 0;
            for _, dataProvider in ipairs(self.mapDataProviders[mapID]) do
                if not options then
                    options = {};
                end
                n = n + 1;
                options[n] = dataProvider.OptionData;
                --option = {name=, iconSetupFunc=, enabled=}
            end
            return options
        end
    end

    function PinController:OnUpdate(elapsed)
        self.t = self.t + elapsed;
        if self.t >= 0.2 then
            self.t = nil;
            self:SetScript("OnUpdate", nil);
            MapTracker:OnMapChanged();
        end
    end

    function PinController:RequestUpdate()
        --Called by sub module when data is recieved
        self.t = 0;
        self:SetScript("OnUpdate", self.OnUpdate);
    end

    function PinController:ListenEvents(state)
        if state and not self.listening then
            self.listening = true;
            self:RegisterEvent("AREA_POIS_UPDATED");
            self:SetScript("OnEvent", self.OnEvent);
        elseif (not state) and self.listening then
            self.listening = false;
            self:UnregisterEvent("AREA_POIS_UPDATED");
            self:SetScript("OnEvent", nil);
        end
    end

    function PinController:OnEvent(event, ...)
        if event == "AREA_POIS_UPDATED" then
            self:RequestUpdate();
        end
    end

    function PinController:UpdatePinScale()

    end


    --Custom Pin Size
    local PinSizeIndexSizeScale = {
        --[pinSizeIndex] = scale,
        [1] = 1.0,
        [2] = 1.25,
        [3] = 1.5,
    };

    function PinController:GetPinSizeChoices()
        local tbl = {};
        for i, scale in ipairs(PinSizeIndexSizeScale) do
            tbl[i] = string.format("%.0f%%", scale * 100);
        end
        return tbl
    end

    function PinController:GetPinSizeIndex()
        return self.pinSizeIndex or 1
    end

    function PinController:GetPinSizeScale()
        return self.pinSizeScale or 1
    end

    function PinController:SetPinSizeIndex(index, useInput)
        if not (index and PinSizeIndexSizeScale[index]) then
            index = 1;
        end
        self.pinSizeIndex = index;
        self.pinSizeScale = PinSizeIndexSizeScale[index];
        if useInput then
            MapTracker:OnMapChanged();
        end
    end
    addon.CallbackRegistry:Register("SettingChanged.WorldMapPin_Size", PinController.SetPinSizeIndex, PinController);


    --Generic Map Triggers
    function PinController:AddMapProcessor(processor)
        if not self.mapProcessors then
            self.mapProcessors = {};
        end
        self.mapProcessors[processor] = true;
    end

    function PinController:TriggerMapProcessor(uiMapID)
        if self.mapProcessors then
            for processor in pairs(self.mapProcessors) do
                processor:OnMapChanged(uiMapID, MapTracker.mapScale);
            end
        end
    end

    function PinController:TriggerModifierStateChanged()
        if self.mapProcessors then
            for processor in pairs(self.mapProcessors) do
                if processor.OnModifiderStateChanged then
                    processor:OnModifiderStateChanged();
                end
            end
        end
    end

    function PinController:TriggerCanvasScaleChanged(scale)
        if self.mapProcessors then
            for processor in pairs(self.mapProcessors) do
                if processor.OnCanvasScaleChanged then
                    processor:OnCanvasScaleChanged(scale);
                end
            end
        end
    end

    function PinController:RequestLoadSettings()
        if not self.pauseUpdate then
            self.pauseUpdate = true;
            C_Timer.After(0, function()
                self.pauseUpdate = nil;
                self:InitializeSubModules();
            end);
        end
    end
end


do  --WorldMapDataProvider
    function WorldMapDataProvider:GetPinTemplate()
        return PIN_TEMPLATE_NAME
    end

    function WorldMapDataProvider:OnShow()

    end

    function WorldMapDataProvider:OnHide()
        PinController:ListenEvents(false);
    end

    function WorldMapDataProvider:OnEvent(event, ...)
        --This significantly increase RAM usage count
        --So we monitor Events using another frame (PinController) 
    end

    function WorldMapDataProvider:RemoveAllIfNeeded()
        if self.anyPins then
            self:RemoveAllData();
        end
    end

    function WorldMapDataProvider:RemoveAllData()
        self.anyPins = false;
        MapFrame:RemoveAllPinsByTemplate(PIN_TEMPLATE_NAME);
    end

    function WorldMapDataProvider:RefreshAllData(fromOnShow)
        self:RemoveAllIfNeeded();
        self:ShowAllPins();
    end

    function WorldMapDataProvider:RefreshAllDataIfPossible()
        self:RemoveAllIfNeeded();
        if not BlizzardUIUtil:IsViewingQuestDetails() then
            self:ShowAllPins();
        end
    end

    function WorldMapDataProvider:ShowAllPins()
        local mapID = MapTracker:GetMapID();
        self.anyPins = PinController:UpdatePinForMap(mapID);
    end

    function WorldMapDataProvider:OnCanvasScaleChanged()
        --Fires multiple times when opening WorldMapFrame
        PinController:UpdatePinScale();
    end
end


do
    local MapScrollContainer = WorldMapFrame.ScrollContainer;

    function MapTracker:Attach()
        if not self.attached then
            self.attached = true;
            self:SetParent(MapFrame);
            self:EnableScripts();
            self:Show();
        end
    end

    function MapTracker:Detach()
        if self.attached then
            self.attached = nil;
            self.mapID = nil;
            self:SetParent(nil);
            self:DisableScripts();
            self:Hide();
            WorldMapDataProvider:RemoveAllIfNeeded();
            WorldMapDataProvider:OnHide();
        end
    end

    function MapTracker:OnUpdate(elapsed)
        self.t1 = self.t1 + elapsed;
        self.t2 = self.t2 + elapsed;

        if self.t1 > 0.016 then
            self.t1 = 0;

            self.newMapID = MapFrame.mapID;
            if self.newMapID ~= self.mapID then
                self.mapID = self.newMapID;
                self:OnMapChanged(self.mapID);
                self.anyChange = true;
            end

            self.newScale = MapScrollContainer.targetScale;
            if self.newScale ~= self.mapScale then
                self.mapScale = self.newScale;
                self:OnCanvasScaleChanged(self.mapScale);
            end

            self.newMaximized = MapFrame.isMaximized;
            if self.newMaximized ~= self.maximized then
                self.maximized = self.newMaximized;
                self.anyChange = true;
            end
        end

        if self.t2 > 0.1 then
            self.t2 = 0;
            self.detailsVisiblity = BlizzardUIUtil:IsViewingQuestDetails();
            if self.detailsVisiblity ~= self.isViewingDetails then
                self.isViewingDetails = self.detailsVisiblity;
                self:OnViewingQuestDetailsChanged();
            end
        end

        if self.anyChange then
            self.anyChange = nil;
            PinController:TriggerMapProcessor(self.mapID);
        end
    end

    function MapTracker:OnShow()
        self.mapID = nil;
        self.mapScale = nil;
        self.isViewingDetails = BlizzardUIUtil:IsViewingQuestDetails();
        self.t1 = 1;
        self:RegisterEvent("MODIFIER_STATE_CHANGED");
    end

    function MapTracker:OnHide()
        WorldMapDataProvider:OnHide();
        self:UnregisterEvent("MODIFIER_STATE_CHANGED");
    end

    function MapTracker:OnEvent(event, ...)
        if event == "MODIFIER_STATE_CHANGED" then
            local key, down = ...
            if down == 1 and (key == "LALT" or key == "RALT") then
                PinController:TriggerModifierStateChanged();
            end
        end
    end

    function MapTracker:EnableScripts()
        self.t1 = 0;
        self.t2 = 0;
        self:SetScript("OnShow", self.OnShow);
        self:SetScript("OnHide", self.OnHide);
        self:SetScript("OnUpdate", self.OnUpdate);
        self:SetScript("OnEvent", self.OnEvent);
    end

    function MapTracker:DisableScripts()
        self:SetScript("OnShow", nil);
        self:SetScript("OnHide", nil);
        self:SetScript("OnUpdate", nil);
        self:SetScript("OnEvent", nil);
        self:UnregisterEvent("MODIFIER_STATE_CHANGED");
    end

    function MapTracker:GetMapID()
        return self.mapID
    end

    function MapTracker:OnMapChanged(mapID)
        if not mapID then
            mapID = self:GetMapID();
        end

        if not mapID then return end;

        if PinController:DoesMapHaveData(mapID) then
            PinController:ListenEvents(true);
            WorldMapDataProvider:RefreshAllDataIfPossible();
        else
            PinController:ListenEvents(false);
            WorldMapDataProvider:RemoveAllIfNeeded();
        end
    end

    function MapTracker:OnCanvasScaleChanged(scale)
        WorldMapDataProvider:OnCanvasScaleChanged();
        PinController:TriggerCanvasScaleChanged(scale);
    end

    function MapTracker:OnViewingQuestDetailsChanged()
        if self.isViewingDetails then

        else
            self:OnMapChanged();
        end
    end
end


do
    PlumberWorldMapPinMixin = CreateFromMixins(MapCanvasPinMixin);

    local function Dummy_SetPassThroughButtons()
    end

    function PlumberWorldMapPinMixin:OnCreated()
        --When frame being created
        self.originalSetPassThroughButtons = self.SetPassThroughButtons;
        self.SetPassThroughButtons = Dummy_SetPassThroughButtons;
        self:AllowPassThroughRightButton(true);
        PinController:AddPin(self);
    end

    function PlumberWorldMapPinMixin:OnLoad()
        --newPin (see MapCanvasMixin:AcquirePin)
        self:SetScalingLimits(1, 1.0, 1.2);
        self.pinFrameLevelType = "PIN_FRAME_LEVEL_GROUP_MEMBER";    --PIN_FRAME_LEVEL_VIGNETTE  PIN_FRAME_LEVEL_AREA_POI   PIN_FRAME_LEVEL_WAYPOINT_LOCATION  PIN_FRAME_LEVEL_GROUP_MEMBER
        self.pinFrameLevelIndex = 1;
        self:SetTexture("Interface/AddOns/Plumber/Art/MapPin/SeedPlanting-Empty-Distant");
    end

    function PlumberWorldMapPinMixin:AllowPassThroughRightButton(unpackedPrimitiveType)
        --Original "SetPassThroughButtons" (see SimpleScriptRegionAPI for details) has chance to taint when called
        --So we overwrite it
        if (not self.isRightButtonAllowed) and (not InCombatLockdown()) then
            self.isRightButtonAllowed = true;
            if self.originalSetPassThroughButtons then
                self.originalSetPassThroughButtons(self, "RightButton");
            end
        end
    end

    function PlumberWorldMapPinMixin:SetTexture(texture, sampling)
        sampling = sampling or "LINEAR";
        self.Texture:SetTexture(texture, nil, nil, sampling);
        self.HighlightTexture:SetTexture(texture, nil, nil, sampling);
        self.HighlightTexture:SetVertexColor(0.4, 0.4, 0.4);
    end

    function PlumberWorldMapPinMixin:SetTexCoord(l, r, t, b)
        self.Texture:SetTexCoord(l, r, t, b);
        self.HighlightTexture:SetTexCoord(l, r, t, b);
    end

    function PlumberWorldMapPinMixin:OnMouseLeave()
        --BaseMapPoiPinMixin.OnMouseLeave(self);
        TooltipFrame:Hide();
    end

    function PlumberWorldMapPinMixin:SetClickable(state)
        if state ~= self.isClickable then
            self.isClickable = state;
        end
        self:SetMouseClickEnabled(state);
    end

    function PlumberWorldMapPinMixin:OnAcquired(data)
        if self.mixin ~= data.mixin then
            self.mixin = data.mixin;
            Mixin(self, data.mixin);
        end

        self:SetClickable(data.clickable);

        self.data = data;
        self:UpdatePosition();
        self:Update();
    end

    function PlumberWorldMapPinMixin:UpdatePosition()
        if self.data then
            self:SetPosition(self.data.x, self.data.y);
        end
    end

    function PlumberWorldMapPinMixin:OnMouseEnter()
        self.UpdateTooltip = nil;
        self:AllowPassThroughRightButton(true);
        self:PostMouseEnter();
    end

    function PlumberWorldMapPinMixin:TriggerMouseReEnter()
        self.UpdateTooltip = self.OnMouseEnter;
    end

    local function GetTextColorForEnabledState(enabledState)
        local Stats = Enum.WidgetEnabledState;

        if enabledState == Stats.Disabled then
            return DISABLED_FONT_COLOR;
        elseif enabledState == Stats.Red then
            return RED_FONT_COLOR;
        elseif enabledState == Stats.White then
            return HIGHLIGHT_FONT_COLOR;
        elseif enabledState == Stats.Green then
            return GREEN_FONT_COLOR;
        elseif enabledState == Stats.Artifact then
            return ARTIFACT_GOLD_COLOR;
        elseif enabledState == Stats.Black then
            return BLACK_FONT_COLOR;
        elseif enabledState == Stats.BrightBlue then
            return BRIGHTBLUE_FONT_COLOR;
        else
            return NORMAL_FONT_COLOR;
        end
    end

    function PlumberWorldMapPinMixin:AttachWidgetSetToTooltip(tooltip, widgetSetID, textRule)
        local widgets = C_UIWidgetManager.GetAllWidgetsBySetID(widgetSetID);

        --We only show TextWithState
        if widgets then
            for _, info in ipairs(widgets) do
                if info.widgetType == 8 then
                    local widgetInfo = C_UIWidgetManager.GetTextWithStateWidgetVisualizationInfo(info.widgetID);
                    if widgetInfo and widgetInfo.shownState ~= 0 and widgetInfo.text then
                        if (not textRule) or (textRule(widgetInfo.text)) then
                            local color = GetTextColorForEnabledState(widgetInfo.enabledState);
                            local r, g, b = color:GetRGB();
                            tooltip:AddLine(widgetInfo.text, r, g, b, true);
                        end
                    end
                end
            end
        end
    end

    function PlumberWorldMapPinMixin:AddQuestTimeToTooltip(tooltip, questID)
        if not questID then return end;

        local formattedTime, color, secondsRemaining = WorldMap_GetQuestTimeForTooltip(questID);
        if formattedTime and color then
            GameTooltip_AddColoredLine(tooltip, formattedTime, color);
        end
    end


    --Overwrite by module
    function PlumberWorldMapPinMixin:PostMouseEnter()

    end

    function PlumberWorldMapPinMixin:IsMouseClickEnabled()
        return true
    end

    function PlumberWorldMapPinMixin:OnMouseClickAction(mouseButton)

    end

    function PlumberWorldMapPinMixin:Update()

    end
end


do  --Master Switch
    local function EnableMapPinSystem(state)
        PinController:RequestLoadSettings();
    end

    local moduleData = {
        name = addon.L["ModuleName WorldMapPin_TWW"],
        dbKey = "WorldMapPin_TWW",
        description = string.format(L["ModuleDescription WorldMapPin_TWW"], L["Bountiful Delve"], L["Special Assignment"]);
        toggleFunc = EnableMapPinSystem,
        categoryID = 1,
        uiOrder = 1101,
        moduleAddedTime = 1721730000,
    };

    addon.ControlCenter:AddModule(moduleData);
end


do  --Dev Tool
    local DevTool;

    local function SavePOIPosition(poiData)
        if not DevTool then
            DevTool = CreateFrame("Frame");
        end

        DevTool.poiData = poiData;
        DevTool.index = 0;
        DevTool.t = 0;

        DevTool:SetScript("OnUpdate", function(self, elapsed)
            self.t = self.t + elapsed;
            if self.t > 0.25 then
                self.t = 0;
            else
                return
            end

            self.index = self.index + 1;
            local data = self.poiData[self.index];
            if not data then
                self:SetScript("OnUpdate", nil);
                return
            end

            local mapID = data[1];
            local poiID = data[2];
            local poiInfo = C_AreaPoiInfo.GetAreaPOIInfo(mapID, poiID);

            if poiInfo then
                if not data.name then
                    data.name = poiInfo.name;
                end

                local x, y = poiInfo.position:GetXY();
                API.ConvertMapPositionToContinentPosition(mapID, x, y, poiID);
            else
                print("Missing POI", poiID);
            end
        end);
    end
    addon.SavePOIPosition = SavePOIPosition;
end