local L = Gargul_L;

local _, GL = ...;

GL.AceGUI = GL.AceGUI or LibStub("AceGUI-3.0");
GL.ScrollingTable = GL.ScrollingTable or LibStub("ScrollingTable");

local AceGUI = GL.AceGUI;
local ScrollingTable = GL.ScrollingTable;

---@type Data
local Constants = GL.Data.Constants;

---@type DB
local DB = GL.DB;

---@class Exporter
GL.Exporter = {
    ---@type boolean
    visible = false,

    ---@type string
    disenchantedItemIdentifier = "||de||",

    ---@type table
    SelectedDates = {},
};

---@type Exporter
local Exporter = GL.Exporter;

--- Show the export window
---
---@return void
function Exporter:draw()
    if (self.visible) then
        return;
    end

    self.visible = true;

    -- Fetch award history per date
    local AwardHistoryByDate = {};
    local latestEntry = 0;
    for _, AwardEntry in pairs(DB:get("AwardHistory") or {}) do
        local dateString = date(L["%Y-%m-%d"], AwardEntry.timestamp);
        local Entries = GL:tableGet(AwardHistoryByDate, dateString, {});

        tinsert(Entries, AwardEntry);

        AwardHistoryByDate[dateString] = Entries;

        if (AwardEntry.timestamp > latestEntry) then
            latestEntry = AwardEntry.timestamp;
        end
    end

    if (GL:gt(latestEntry, 0)) then
        tinsert(self.SelectedDates, date(L["%Y-%m-%d"], latestEntry));
    end

    -- Create a container/parent frame
    local Window = AceGUI:Create("Frame");
    Window:SetTitle((L["Gargul v%s"]):format(GL.version));
    Window:SetStatusText(L["v"] ..GL.version);
    Window:SetLayout("Flow");
    Window:SetWidth(600);
    Window:SetHeight(500);
    Window:EnableResize(false);
    Window:SetCallback("OnClose", function()
        self:close();
    end);
    Window:SetPoint(GL.Interface:getPosition("Exporter"));
    Window.statustext:GetParent():Hide(); -- Hide the statustext bar

    GL.Interface:set(self, "Window", Window);

    -- Make sure the window can be closed by pressing the escape button
    _G["GARGUL_EXPORTER_WINDOW"] = Window.frame;
    tinsert(UISpecialFrames, "GARGUL_EXPORTER_WINDOW");

    --[[ DON'T EDIT STOOPID! ]]
    local DontEditNotification = AceGUI:Create("Label");
    DontEditNotification:SetFullWidth(true);
    DontEditNotification:SetJustifyH("CENTER");
    DontEditNotification:SetText(("|c00FF0000%s|r"):format(L[ [[
This is an export feature ONLY, there is no point editing any of the values: THEY WON'T BE SAVED!

]]]));
    Window:AddChild(DontEditNotification);

    --[[
        DATES FRAME
    ]]
    local DateFrame = AceGUI:Create("SimpleGroup");
    DateFrame:SetLayout("FILL")
    DateFrame:SetWidth(200);
    DateFrame:SetHeight(350);
    Window:AddChild(DateFrame);

    -- Generate the characters table and add it to DateFrame.frame
    Exporter:drawDatesTable(Window.frame, GL:tableFlip(AwardHistoryByDate));

    -- Large edit box
    local ExportBox = AceGUI:Create("MultiLineEditBox");
    ExportBox:SetText("");
    ExportBox:SetWidth(360);
    ExportBox:SetHeight(350);
    ExportBox:DisableButton(true);
    ExportBox:SetLabel("");
    ExportBox:SetNumLines(22);
    ExportBox:SetMaxLetters(999999999);
    Window:AddChild(ExportBox);
    GL.Interface:set(self, "Export", ExportBox);

    --[[
        FOOTER BUTTON PARENT FRAME
    ]]
    local FooterFrame = AceGUI:Create("SimpleGroup");
    FooterFrame:SetLayout("Flow");
    FooterFrame:SetFullWidth(true);
    FooterFrame:SetHeight(50);
    Window:AddChild(FooterFrame);

    local ClearButton = AceGUI:Create("Button");
    ClearButton:SetText(L["Clear"]);
    ClearButton:SetWidth(140);
    ClearButton:SetCallback("OnClick", function()
        Exporter:clearData();
    end);
    FooterFrame:AddChild(ClearButton);

    local SettingsButton = AceGUI:Create("Button");
    SettingsButton:SetText(L["Settings"]);
    SettingsButton:SetWidth(140);
    SettingsButton:SetCallback("OnClick", function()
        GL.Settings:draw("ExportingLoot");
    end);
    FooterFrame:AddChild(SettingsButton);

    Exporter:refreshExportString();
end

--- Clear export data, either for a specific date or everything
---
---@return void
function Exporter:clearData()
    local warning;
    local onConfirm;

    -- No date is selected, delete everything!
    if (GL:empty(self.SelectedDates)) then
        warning = L["Are you sure you want to remove your complete reward history table? This deletes ALL loot data and cannot be undone!"];
        onConfirm = function()
            GL.Events:fire("GL.ITEM_UNAWARDED");
            DB:set("AwardHistory", {});

            Exporter:close();
            Exporter:draw();
        end;

    else -- Only delete entries on the selected date
        warning = (L["Are you sure you want to remove all data for %s? This cannot be undone!"]):format(table.concat(self.SelectedDates, ", "));
        onConfirm = function()
            for key, AwardEntry in pairs(DB:get("AwardHistory")) do
                local dateString = date(L["%Y-%m-%d"], AwardEntry.timestamp);

                if (GL:inTable(self.SelectedDates, dateString)) then
                    AwardEntry = nil;
                    DB:set("AwardHistory." .. key, nil);
                end
            end

            Exporter:close();
            Exporter:draw();
        end
    end

    -- Show a confirmation dialog before clearing entries
    GL.Interface.Dialogs.PopupDialog:open{
        question = warning,
        OnYes = function ()
            onConfirm();

            -- Let the application know that an item was unawarded (deleted)
            GL.Events:fire("GL.ITEM_UNAWARDED");
        end,
    };
end

--- Show the export data (either all or for the selected date)
---
---@return void
function Exporter:refreshExportString()
    local exportFormat = GL.Settings:get("ExportingLoot.format");

    if (exportFormat == Constants.ExportFormats.JSON) then
        local exportString = GL.JSON:encode(self:getLootEntries(true));
        GL.Interface:get(self, "MultiLineEditBox.Export"):SetText(exportString);

        return;
    end

    local LootEntries = self:getLootEntries();
    if (exportFormat == Constants.ExportFormats.Custom) then
        local exportString = self:transformEntriesToCustomFormat(LootEntries);
        GL.Interface:get(self, "MultiLineEditBox.Export"):SetText(exportString);

    elseif (exportFormat == Constants.ExportFormats.RRobin) then
        local exportString = self:transformEntriesToCustomFormat(LootEntries, "@ID,@DATE @TIME,@WINNER-@REALM");
        GL.Interface:get(self, "MultiLineEditBox.Export"):SetText(exportString);

    elseif (GL:inTable({ Constants.ExportFormats.TMB, Constants.ExportFormats.TMBWithRealm }, exportFormat)) then
        local exportString = self:transformEntriesToTMBFormat(LootEntries);
        GL.Interface:get(self, "MultiLineEditBox.Export"):SetText(exportString);

    elseif (GL:inTable({ Constants.ExportFormats.DFTUS, Constants.ExportFormats.DFTEU }, exportFormat)) then
        GL.Interface:get(self, "MultiLineEditBox.Export"):SetText(self:transformEntriesToDFTFormat(LootEntries));
    end
end

--- Fetch export entries (either all or for the selected date)
---
---@return table
function Exporter:getLootEntries(raw)
    local noDatesSelected = GL:empty(self.SelectedDates);
    local Entries = {};
    raw = raw == true;

    for _, AwardEntry in pairs(DB:get("AwardHistory")) do
        (function()
            local concernsDisenchantedItem = AwardEntry.awardedTo == self.disenchantedItemIdentifier;

            local dateString = date(L["%Y-%m-%d"], AwardEntry.timestamp);
            if (
                (not concernsDisenchantedItem or GL.Settings:get("ExportingLoot.includeDisenchantedItems"))
                and (not AwardEntry.OS or GL.Settings:get("ExportingLoot.includeOffspecItems"))
                and (not AwardEntry.isBonusLoot or GL.Settings:get("ExportingLoot.includeBonusRollItems"))
                and (noDatesSelected or GL:inTable(self.SelectedDates, dateString))
                and not GL:empty(AwardEntry.timestamp)
                and not GL:empty(AwardEntry.itemLink)
                and not GL:empty(AwardEntry.awardedTo)
            ) then
                if (raw) then
                    tinsert(Entries, AwardEntry);
                    return;
                end

                local awardedTo = AwardEntry.awardedTo;

                if (concernsDisenchantedItem) then
                    awardedTo = GL.Settings:get("ExportingLoot.disenchanterIdentifier");
                end

                -- Just in case an itemID is not available we will extract it from the item link
                local itemID = AwardEntry.itemID or GL:getItemIDFromLink(AwardEntry.itemLink);

                local checksum = AwardEntry.checksum; -- Old entries may not possess a checksum yet
                if (not checksum) then
                    checksum = GL:strPadRight(GL:strLimit(GL:stringHash(AwardEntry.timestamp .. itemID) .. GL:stringHash(awardedTo), 20, ""), "0", 20);
                end

                tinsert(Entries, {
                    timestamp = AwardEntry.timestamp,
                    awardedTo = awardedTo,
                    itemID = itemID,
                    itemLink = AwardEntry.itemLink,
                    OS = AwardEntry.OS and 1 or 0,
                    SR = AwardEntry.SR and 1 or 0,
                    WL = AwardEntry.WL and 1 or 0,
                    PL = AwardEntry.PL and 1 or 0,
                    TMB = AwardEntry.TMB and 1 or 0,
                    winningRollType = AwardEntry.winningRollType or "-",
                    checksum = checksum,
                });
            end
        end)();
    end

    -- Sort the entries by award time
    table.sort(Entries, function (a, b)
        if (not a.timestamp or not b.timestamp) then
            return false;
        end

        return a.timestamp < b.timestamp;
    end);

    return Entries;
end

--- Transform the table of entries to the user specified (custom) format
---
---@return string
function Exporter:transformEntriesToCustomFormat(Entries, format)
    format = format or GL.Settings:get("ExportingLoot.customFormat");
    local exportString = "";

    -- Make sure that all relevant item data is cached
    GL:onItemLoadDo(GL:tableColumn(Entries, "itemLink"), function (Result)
        local Details = {};
        for _, ItemDetails in pairs(Result or {}) do
            Details[ItemDetails.id] = ItemDetails;
        end
        Result = nil;

        for _, AwardEntry in pairs(Entries) do
            local exportEntry = format;
            local ItemDetails = Details[AwardEntry.itemID];
            local wowheadLink;

            if (GL.isEra) then
                wowheadLink = ("https://classic.wowhead.com/item=%s"):format(AwardEntry.itemID);
            elseif (GL.isRetail) then
                wowheadLink = ("https://www.wowhead.com/item=%s"):format(AwardEntry.itemID);
            else
                wowheadLink = ("https://www.wowhead.com/mop/item=%s"):format(AwardEntry.itemID);
            end

            if (ItemDetails) then
                local Values = {
                    ["@ID"] = AwardEntry.itemID,
                    ["@LINK"] = ItemDetails.link:gsub('\124','\124\124'),
                    ["@ITEM"] = ItemDetails.name,
                    ["@ILVL"] = ItemDetails.level,
                    ["@QUALITY"] = ItemDetails.quality,
                    ["@WINNER"] = GL:nameFormat{ name = AwardEntry.awardedTo, stripRealm = true },
                    ["@REALM"] = GL:getRealmFromName(AwardEntry.awardedTo),
                    ["@OS"] = GL:toboolean(AwardEntry.OS),
                    ["@SR"] = GL:toboolean(AwardEntry.SR),
                    ["@WL"] = GL:toboolean(AwardEntry.WL),
                    ["@PL"] = GL:toboolean(AwardEntry.PL),
                    ["@TMB"] = GL:toboolean(AwardEntry.TMB),
                    ["@ROLLTYPE"] = AwardEntry.winningRollType,
                    ["@CHECKSUM"] = AwardEntry.checksum,
                    ["@YEAR"] = date('%Y', AwardEntry.timestamp),
                    ["@YY"] = date('%y', AwardEntry.timestamp),
                    ["@MONTH"] = date('%m', AwardEntry.timestamp),
                    ["@DAY"] = date('%d', AwardEntry.timestamp),
                    ["@HOUR"] = date('%H', AwardEntry.timestamp),
                    ["@MINUTE"] = date('%M', AwardEntry.timestamp),
                    ["@DATE"] = date(L["%Y-%m-%d"], AwardEntry.timestamp),
                    ["@TIME"] = date('%H:%M', AwardEntry.timestamp),
                    ["@WOWHEAD"] = wowheadLink,
                    ["\\t"] = "\t",
                };

                for find, replace in pairs(Values) do
                    -- We transform booleans to 0 or 1
                    if (type(replace) == "boolean") then
                        replace = replace and 1 or 0;
                    end

                    exportEntry = exportEntry:gsub(find, replace);
                end

                exportString = exportString .. "\n" .. exportEntry;
            end

        end
    end);

    return strtrim(exportString);
end

--- Transform the table of entries to the TMB CSV format
---
---@return string
function Exporter:transformEntriesToTMBFormat(Entries)
    local ItemIDConversions = {
        [18423] = 18422, -- Head of Onyxia
        [19003] = 19002, -- Head of Nefarian
        [32386] = 32385, -- Magtheridon's Head
        [43959] = 44083, -- Reins of the Grand Black War Mammoth
        [209693] = 211452, -- Perfect Blackfathom Pearl
        [217350] = 217351, -- Thermaplugg's Engineering Notes
        [221346] = 221363, -- Scapula of the Fallen Avatar
    };

    local exportFormat = GL.Settings:get("ExportingLoot.format");
    local exportString = "dateTime,character,itemID,offspec,id";

    for _, AwardEntry in pairs(Entries) do
        exportString = string.format("%s\n%s,%s,%s,%s,%s",
            exportString,
            date(L["%Y-%m-%d"], AwardEntry.timestamp),
            GL:nameFormat{
                name = AwardEntry.awardedTo,
                stripRealm = exportFormat == Constants.ExportFormats.TMB,
                forceRealm = exportFormat == Constants.ExportFormats.TMBWithRealm
            },
            ItemIDConversions[AwardEntry.itemID] and ItemIDConversions[AwardEntry.itemID] or AwardEntry.itemID, -- Convert specific item IDs for TMB compatibility
            AwardEntry.OS,
            AwardEntry.checksum
        );
    end

    return exportString;
end

--- Transform the table of entries to the DFT sheet format
---
---@return string
function Exporter:transformEntriesToDFTFormat(Entries)
    -- Get the desired export format
    local exportFormat = GL.Settings:get("ExportingLoot.format");

    -- We need to load all items first to make sure the item names are available
    local exportString = "";
    for _, AwardEntry in pairs(Entries) do
        local dateString = "";

        -- Check whether the player wants an EU or US date string
        if (exportFormat == Constants.ExportFormats.DFTEU) then
            dateString = date('%d/%m/%Y', AwardEntry.timestamp);
        else
            dateString = date('%m/%d/%Y', AwardEntry.timestamp);
        end

        exportString = string.format("%s%s;[%s];%s\n",
            exportString,
            dateString,
            AwardEntry.itemID,
            GL:nameFormat(AwardEntry.awardedTo)
        );
    end;

    return exportString;
end

--- Close the export window
---
---@return void
function Exporter:close()
    if (not self.visible) then
        return;
    end

    self.visible = false;
    local Window = GL.Interface:get(self, "Window");

    if (not Window) then
        return;
    end

    -- Store the frame's last position for future play sessions
    GL.Interface:storePosition(Window, "Exporter");
    Window:Hide();

    -- Clean up the Dates table separately
    local Dates = GL.Interface:get(self, "Table.Dates");
    if (Dates) then
        GL.Interface:get(self, "Table.Dates"):SetData({}, true);
        GL.Interface:get(self, "Table.Dates"):Hide();
    end
end

--- Draw the dates table shown on the left hand side of the window
---
---@return void
function Exporter:drawDatesTable(Parent, Dates)
    local Columns = {
        {
            name = "Date",
            width = 120,
            align = "LEFT",
            color = {
                r = 0.5,
                g = 0.5,
                b = 1.0,
                a = 1.0
            },
            colorargs = nil,
            sort = GL.Data.Constants.ScrollingTable.descending,
        },
    };

    local Table = ScrollingTable:CreateST(Columns, 21, 15, nil, Parent, true);
    Table:EnableSelection(true);
    Table:SetWidth(120);
    Table.frame:SetPoint("TOPLEFT", Parent, "TOPLEFT", 50, -80);

    Table:RegisterEvents{
        OnClick = function(rowFrame, cellFrame, data, cols, row, realrow, column, table, button, ...)
            -- Even if we're still missing an answer from some of the group members
            -- we still want to make sure our inspection end after a set amount of time
            GL.Ace:ScheduleTimer(function()
                self.SelectedDates = {};
                for _, row in pairs(Table:GetSelection() or {}) do
                    local Row = Table:GetRow(row) or {};

                    if (Row["cols"]) then
                        local date = GL:tableGet(Row, "cols.1.value");

                        if (date) then
                            tinsert(self.SelectedDates, date);
                        end
                    end
                end

                self:refreshExportString();
            end, .1);
        end
    };

    local TableData = {};
    local selectedRow = nil;
    local i = 1;
    for _, date in pairs(Dates) do
        local Row = {
            cols = {
                {
                    value = date,
                },
            },
        };

        if (date == self.SelectedDates[1]) then
            selectedRow = i;
        end

        --- Can't use simple format because it's not supported by multiselection
        tinsert(TableData, Row);

        i = i + 1;
    end

    -- The second argument refers to "isMinimalDataformat"
    -- For the full format see https://www.wowace.com/projects/lib-st/pages/set-data
    Table:SetData(TableData);

    if (selectedRow) then
        Table:SetSelection(selectedRow);
    end

    GL.Interface:set(self, "Dates", Table);
end
