local LAM2 = LibStub("LibAddonMenu-2.0")

local GuildGoldDeposits = {}
GuildGoldDeposits.name = "GuildGoldDeposits"
GuildGoldDeposits.version = 224.1
GuildGoldDeposits.default = {
      enable_guild  = { true, true, true, true, true }
}
GuildGoldDeposits.max_guild_ct = 5

                        -- event_list[guild_index] = { list of event strings }
                        -- loaded from the current "Save Now" run.
                        -- Eventually these become the front part of
                        -- savedVariables.guild_history[guildName]
GuildGoldDeposits.event_list = {}
GuildGoldDeposits.guild_name = {} -- guild_name[guild_index] = "My Aweseome Guild"

                        -- retry_ct[guild_index] = how many retries after
                        -- distrusting "nah, no more history"
GuildGoldDeposits.retry_ct   = { 0, 0, 0, 0, 0 }
GuildGoldDeposits.max_retry_ct = 3
GuildGoldDeposits.max_day_ct = 30 -- how many days to store in SavedVariables

local TIMESTAMPS_CLOSE_SECS = 10

-- Indices into the 3-element "event" split.
local I_TIMESTAMP = 1
local I_AMOUNT    = 2
local I_USER      = 3

-- Init ----------------------------------------------------------------------

function GuildGoldDeposits.OnAddOnLoaded(event, addonName)
    if addonName ~= GuildGoldDeposits.name then return end
    if not GuildGoldDeposits.version then return end
    if not GuildGoldDeposits.default then return end
    GuildGoldDeposits:Initialize()
end

function GuildGoldDeposits:Initialize()
    self.savedVariables = ZO_SavedVars:NewAccountWide(
                              "GuildGoldDepositsVars"
                            , self.version
                            , nil
                            , self.default
                            )
    self:CreateSettingsWindow()
    EVENT_MANAGER:UnregisterForEvent(self.name, EVENT_ADD_ON_LOADED)
end

-- UI ------------------------------------------------------------------------

function GuildGoldDeposits.ref_cb(guild_index)
    return "GuildGoldDeposits_cbg" .. guild_index
end

function GuildGoldDeposits.ref_desc(guild_index)
    return "GuildGoldDeposits_desc" .. guild_index
end

function GuildGoldDeposits:CreateSettingsWindow()
    local panelData = {
        type                = "panel",
        name                = "Guild Gold Deposits",
        displayName         = "Guild Gold Deposits",
        author              = "ziggr",
        version             = self.version,
        slashCommand        = "/gg",
        registerForRefresh  = true,
        registerForDefaults = false,
    }
    local cntrlOptionsPanel = LAM2:RegisterAddonPanel( self.name
                                                     , panelData
                                                     )
    local optionsData = {
        { type      = "button"
        , name      = "Save Data Now"
        , tooltip   = "Save guild gold deposit data to file now."
        , func      = function() self:SaveNow() end
        },
        { type      = "header"
        , name      = "Guilds"
        },
    }

    for guild_index = 1, self.max_guild_ct do
        table.insert(optionsData,
            { type      = "checkbox"
            , name      = "(guild " .. guild_index .. ")"
            , tooltip   = "Save data for guild " .. guild_index .. "?"
            , getFunc   = function()
                            return self.savedVariables.enable_guild[guild_index]
                          end
            , setFunc   = function(e)
                            self.savedVariables.enable_guild[guild_index] = e
                          end
            , reference = self.ref_cb(guild_index)
            })
                        -- HACK: for some reason, I cannot get "description"
                        -- items to dynamically update their text. Color and
                        -- hidden, yes, but text? Nope, it never changes. So
                        -- instead of a desc for static text, I'm going to use
                        -- a "checkbox" with the on/off field hidden. Total
                        -- hack. Sorry.
        table.insert(optionsData,
            { type      = "checkbox"
            , name      = "(desc " .. guild_index .. ")"
            , reference = self.ref_desc(guild_index)
            , getFunc   = function() return false end
            , setFunc   = function() end
            })
    end

    LAM2:RegisterOptionControls("GuildGoldDeposits", optionsData)
    CALLBACK_MANAGER:RegisterCallback("LAM-PanelControlsCreated"
            , self.OnPanelControlsCreated)
end

-- Delay initialization of options panel: don't waste time fetching
-- guild names until a human actually opens our panel.
function GuildGoldDeposits.OnPanelControlsCreated(panel)
    self = GuildGoldDeposits
    guild_ct = GetNumGuilds()
    for guild_index = 1,self.max_guild_ct do
        cb = _G[self.ref_cb(guild_index)]
        if guild_index <= guild_ct then
            guildId   = GetGuildId(guild_index)
            guildName = GetGuildName(guildId)
            cb.label:SetText(guildName)
            cb:SetHidden(false)
            self.guild_name[guild_index] = guildName
        else
                        -- If no guild #N, hide and disable it.
            cb:SetHidden(true)
            self.savedVariables.enable_guild[guild_index] = false
        end

        desc = _G[self.ref_desc(guild_index)]
        self.ConvertCheckboxToText(desc)
        self:SetStatusNewest(guild_index)
    end
end

-- Coerce a checkbox to act like a text label.
--
-- I cannot get LibAddonMenu-2.0 "description" items to dynamically update
-- their text. SetText() has no effect. But SetText() works on "checkbox"
-- items, so beat those into a text-like UI element.
function GuildGoldDeposits.ConvertCheckboxToText(cb)
    desc:SetHandler("OnMouseEnter", nil)
    desc:SetHandler("OnMouseExit",  nil)
    desc:SetHandler("OnMouseUp",    nil)
    desc.label:SetFont("ZoFontGame")
    desc.label:SetText("-")
    desc.checkbox:SetHidden(true)
end

-- Display Status ------------------------------------------------------------

-- Update the per-guild text label with what's going on with that guild data.
function GuildGoldDeposits:SetStatus(guild_index, msg)
    desc = _G[self.ref_desc(guild_index)].label
    desc:SetText("  " .. msg)
end

-- Set status to "Newest: @user 100,000g  11 hours ago"
function GuildGoldDeposits:SetStatusNewest(guild_index)
    line = self:SavedHistoryNewest(guild_index)
    if not line then return end
    event = self:StringToEvent(line)
    now_ts = GetTimeStamp()
    ago_secs = GetDiffBetweenTimeStamps(now_ts, event.time)
    ago_str  = FormatTimeSeconds(ago_secs
                    , TIME_FORMAT_STYLE_SHOW_LARGEST_UNIT_DESCRIPTIVE -- "22 hours"
                    , TIME_FORMAT_PRECISION_SECONDS
                    , TIME_FORMAT_DIRECTION_DESCENDING
                    )

    self:SetStatus(guild_index, "Newest deposit: " .. event.user
                     .. " " .. event.amount .. "g  " .. ago_str .. " ago")
end

-- Parse/Format SavedVariables history ----------------------------------------

-- Lua lacks a split() function. Here's a cheesy hardwired one that works
-- for our specific need.
function GuildGoldDeposits:split(str)
    t1 = string.find(str, '\t')
    t2 = string.find(str, '\t', 1 + t1)
    return   string.sub(str, 1,      t1 - 1)
           , string.sub(str, 1 + t1, t2 - 1)
           , string.sub(str, 1 + t2)
end

-- Convert an event to a compact string that a line-parser can easily consume.
function GuildGoldDeposits:EventToString(event)
                        -- tab-delimited fields
                        -- date     seconds since the epoch
                        -- amount
                        -- user     unquoted, can contain all sorts of
                        --          noise but unlikely to contian a
                        --          tab character.
                        --
                        -- using tostring() here so that this function can work
                        -- when debugging nil event elements.
    return tostring(event.time)
            .. '\t' .. tostring(event.amount)
            .. '\t' .. tostring(event.user)
end

function GuildGoldDeposits:StringToEvent(str)
    ts, amt, user = self:split(str)
    return { time   = ts
           , amount = amt
           , user   = user
           }
end

-- Return the one newest history line, if any, from our previous save.
-- Return nil if not.
function GuildGoldDeposits:SavedHistoryNewest(guild_index)
    guildName = GetGuildName(guildId)
    if not self.savedVariables then return nil end
    if not self.savedVariables.history then return nil end
    history = self.savedVariables.history[guildName]
    if not history then return nil end
    if not (1 <= #history) then return nil end
    return history[1]
end

-- Fetch Guild Data from the server ------------------------------------------
--
-- Fetch _all_ events for each guild. Server holds no more than 10 days, no
-- more than 500 events.
--
-- Defer per-event iteration until fetch is complete. This might help reduce
-- the clock skew caused by the items using relative time, but relative
-- to _what_?

function GuildGoldDeposits:SaveNow()
    self.event_list = {}
    for guild_index = 1, self.max_guild_ct do
        if self.savedVariables.enable_guild[guild_index] then
            self:SaveGuildIndex(guild_index)
        else
            self:SkipGuildIndex(guild_index)
        end
    end
end

-- User doesn't want this guild. Respond with "okay, skipping"
function GuildGoldDeposits:SkipGuildIndex(guild_index)
    self:SetStatus(guild_index, "skipped")
end

-- Download one guild's history
function GuildGoldDeposits:SaveGuildIndex(guild_index)
    guildId = GetGuildId(guild_index)
    self:SetStatus(guild_index, "downloading history...")
    event_ct = 0
    found_ct = 0
    loop_ct = 0
    loop_max = 100
    RequestGuildHistoryCategoryNewest(guildId, GUILD_HISTORY_BANK)

                        -- Start an asynchronous callback chain to slowly
                        -- poll ESO servers for all history. Chain will
                        -- callback itself until done, then callback
                        -- into the actual processing of that data.
    self:ServerDataPoll(guild_index)
end

-- Async poll to fetch ALL guild bank history data from the ESO server
-- Calls ServerDataComplete() once all data is loaded.
function GuildGoldDeposits:ServerDataPoll(guild_index)
    guildId = GetGuildId(guild_index)
    more = DoesGuildHistoryCategoryHaveMoreEvents(guildId, GUILD_HISTORY_BANK)
    event_ct = GetNumGuildEvents(guildId, GUILD_HISTORY_BANK)
    self:SetStatus(guild_index, "fetching events: " .. event_ct .. " ...")
    can_retry =    (not self.retry_ct[guild_index])
                or (self.retry_ct[guild_index] < self.max_retry_ct)
    if more or can_retry then
        RequestGuildHistoryCategoryOlder(guildId, GUILD_HISTORY_BANK)
        delay_ms = 0.5 * 1000
        zo_callLater(function() self:ServerDataPoll(guild_index) end, delay_ms)
        if not more then
            self.retry_ct[guild_index] = 1 + self.retry_ct[guild_index]
        end
    else
        self:ServerDataComplete(guild_index)
    end
end

-- Now that all data from the ESO server is loaded into the ESO client,
-- extract gold deposits and write to savedVars.
function GuildGoldDeposits:ServerDataComplete(guild_index)
    guildId = GetGuildId(guild_index)
    guild_name = self.guild_name[guild_index]
    event_ct = GetNumGuildEvents(guildId, GUILD_HISTORY_BANK)
    --self:SetStatus(guild_index, "scanning events: " .. event_ct .. " ...")
    for i = 1, event_ct do
        t, s, u, a = GetGuildEventInfo(guildId, GUILD_HISTORY_BANK, i)
        if t == GUILD_EVENT_BANKGOLD_ADDED then
            event = { type = t
                    , time = GetTimeStamp() - s
                    , user = u
                    , amount = a
                    }
            self:RecordEvent(guild_index, event)
        end
    end
    found_ct = 0
    if self.event_list[guild_index] then
        found_ct = #self.event_list[guild_index]
    end
    self:SetStatus(guild_index, "scanned events: " .. event_ct
                   .. "  gold deposits: " .. found_ct)
    self.savedVariables.history[guild_name]
        = self:MergeHistories( self.event_list[guild_index]
                             , self.savedVariables.history[guild_name])
end

function GuildGoldDeposits:RecordEvent(guild_index, event)
    if not self.event_list[guild_index] then
        self.event_list[guild_index] = {}
    end
    t = self.event_list[guild_index]
    table.insert(t, self:EventToString(event))
end

-- Merging saved and fetched history -----------------------------------------

-- Return a new list composed of all of "fetched", and the latter portion of
-- "saved" that comes after "fetched", but not older than max_day_ct
function GuildGoldDeposits:MergeHistories(fetched, saved)
    -- Where in "saved" does "fetch" end?

                        -- No saved events? Just use whatever we fecthed.
    if 0 == #saved then
        return fetched
    end
                        -- Create a short list of the last few fetched events.
                        -- We'll scan saved for these events to match up the
                        -- two lists.
    last_rows = self:LastRows(fetched, 5)
    f_events = {}
    for i,f_row in ipairs(last_rows) do
        f_event = self:StringToEvent(f_row)
        table.insert(f_events, f_event)
        d("f_event["..#f_events.."]: " .. f_row)
    end
                        -- If we fetched nothing at all, retain saved
                        -- unchanged. Don't even bother to strip older events.
                        -- Something's probably gone wrong (or the guild has
                        -- gone very, very, quiet).
    if 0 == #f_events then return fetched end

    s_i_found = self:Find(f_events, saved)
    if not s_i_found then
        d("Not Found, retaining all of saved")
        s_overlap_end_i = 0
    else
        s_overlap_end_i = s_i_found + #f_events - 1
        d("Found, ending in s_i:"..s_overlap_end_i)
    end
    for s_i = s_overlap_end_i + 1,#saved do
        table.insert(fetched, saved[s_i])
    end
    return fetched
end

-- Return the index into saved that matches f_events.
-- Return nil if not found.
function GuildGoldDeposits:Find(f_events, saved)
    if (0 == #f_events) or (0 == #saved) then return nil end
    --for i = 1,#saved
    do
        i=#saved
        if self:PatternMatch(i, f_events, saved) then
            return i - #f_events + 1
        end
    end
    return nil
end

-- If saved[s_i] and its precursors match f_events, return true.
-- If not, return false.
function GuildGoldDeposits:PatternMatch(s_i, f_events, saved)
    s_event = {}
    for i = 0, math.min(s_i, #f_events) - 1 do
        s_ii    = s_i - i
        f_ii    = #f_events - i
        s_row   = saved[s_ii]
        s_event = self:StringToEvent(saved[s_i - i])
        f_event = f_events[f_ii]
        match   = self:EventsMatch(f_event, s_event)
        d("pm " ..tostring(match)
            .. " s_i:" .. s_i
            .." i:"..i
            .." f_ii:"..f_ii.." "..self:EventToString(f_event)
            .." s_ii:"..s_ii.." "..s_row
            )
        if not match then return false end
    end
    return true
end

-- Return the last "ct" rows from "list".
-- Or fewer if list doesn't have that many rows.
function GuildGoldDeposits:LastRows(list, ct)
    r = {}
    for i = math.min(ct, #list),1,-1 do
        list_i = #list-i+1
        d("lr list[" .. list_i .."] " ..tostring(list[list_i]))
        table.insert(r, list[list_i])
    end
    return r
end

-- Do these rows "match"?
--
-- User and amount must be exact.
-- Time must be within N seconds.
function GuildGoldDeposits:EventsMatch(f_event, s_event)
    d("f=" .. self:EventToString(f_event))
    d("s=" .. self:EventToString(s_event))
    m1 =  math.abs(f_event.time - s_event.time) < TIMESTAMPS_CLOSE_SECS
    m2 = f_event.amount == s_event.amount
    m3 = f_event.user   == s_event.user
    d("m=" .. tostring(m1) .. " " .. tostring(m2) .. " " .. tostring(m3))
    return m1 and m2 and m3
end

-- Postamble -----------------------------------------------------------------

EVENT_MANAGER:RegisterForEvent( GuildGoldDeposits.name
                              , EVENT_ADD_ON_LOADED
                              , GuildGoldDeposits.OnAddOnLoaded
                              )