Librarian = ZO_SortFilterList:Subclass()
Librarian.defaults = {}

ZO_CreateStringId("SI_BINDING_NAME_TOGGLE_LIBRARIAN", "Toggle Librarian")
ZO_CreateStringId("SI_BINDING_NAME_RELOAD_UI", "Reload UI")
ZO_CreateStringId("SI_WINDOW_TITLE_LIBRARIAN", "Librarian")
ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_UNREAD", "Unread")
ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_FOUND", "Found")
ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_TITLE", "Title")
ZO_CreateStringId("SI_LIBRARIAN_SORT_TYPE_WORD_COUNT", "Words")
ZO_CreateStringId("SI_LIBRARIAN_MARK_UNREAD", "Mark as Unread")
ZO_CreateStringId("SI_LIBRARIAN_MARK_READ", "Mark as Read")
ZO_CreateStringId("SI_LIBRARIAN_BOOK_COUNT", "%d Books")
ZO_CreateStringId("SI_LIBRARIAN_UNREAD_COUNT", "%s (%d Unread)")
ZO_CreateStringId("SI_LIBRARIAN_SHOW_ALL_BOOKS", "Show books for all characters")
ZO_CreateStringId("SI_LIBRARIAN_NEW_BOOK_FOUND", "Book added to librarian")
ZO_CreateStringId("SI_LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE", "Book added to librarian: %s")
ZO_CreateStringId("SI_LIBRARIAN_FULLTEXT_SEARCH", "Full-text Search:")
ZO_CreateStringId("SI_LIBRARIAN_SEARCH_HINT", "Enter text to search for.")
ZO_CreateStringId("SI_LIBRARIAN_RELOAD_REMINDER", "ReloadUI suggested to update Librarian database.")
ZO_CreateStringId("SI_LIBRARIAN_BACKUP_REMINDER", "Remember to backup your Librarian SavedVariables regularly.  Look up Librarian on ESOUI for instructions.")
ZO_CreateStringId("SI_LIBRARIAN_EMPTY_LIBRARY_IMPORT_PROMPT", [[It appears your Library is empty.
Patch 1.3 fixed a bug in the storage of addon data which now needs to be moved back to the correct place.
Please click the "Import from before patch" button in the Librarian setting menu to perform this migration.
It is recommended that you backup your Librarian SavedVariables before performing this step as a precaution.
Librarian settings can be opened by clicking the settings button in the top right of the window.]])

local SORT_ARROW_UP = "EsoUI/Art/Miscellaneous/list_sortUp.dds"
local SORT_ARROW_DOWN = "EsoUI/Art/Miscellaneous/list_sortDown.dds"
local LIBRARIAN_DATA = 1
local LIBRARIAN_SEARCH = 1

local ENTRY_SORT_KEYS =
{
    ["title"] = { },
    ["unread"] = { tiebreaker = "timeStamp" },
    ["timeStamp"] = { tiebreaker = "title" },
    ["wordCount"] = { tiebreaker = "title" }
}

function Librarian:New()
    local librarian = ZO_SortFilterList.New(self, LibrarianFrame)
    librarian:Initialise()
    return librarian
end

function Librarian:Initialise()
    self.masterList = {}
    self.newBookCount = 0
    self.sortHeaderGroup:SelectHeaderByKey("timeStamp")

    ZO_ScrollList_AddDataType(self.list, LIBRARIAN_DATA, "LibrarianBookRow", 30, function(control, data) self:SetupBookRow(control, data) end)
    ZO_ScrollList_EnableHighlight(self.list, "ZO_ThinListHighlight")

    self.localSavedVars = ZO_SavedVars:New("Librarian_SavedVariables", 1, nil, self.defaults, nil)
    self.globalSavedVars = ZO_SavedVars:NewAccountWide("Librarian_SavedVariables", 1, nil, self.defaults, nil)

    if not self.globalSavedVars.settings then self.globalSavedVars.settings = {} end
    self.settings = self.globalSavedVars.settings

    if not self.globalSavedVars.books then self.globalSavedVars.books = {} end
    self.books = self.globalSavedVars.books

    if not self.localSavedVars.characterBooks then self.localSavedVars.characterBooks = {} end
    self.characterBooks = self.localSavedVars.characterBooks

    self.searchBox = GetControl(LibrarianFrame, "SearchBox")
    self.searchBox:SetHandler("OnTextChanged", function() self:OnSearchTextChanged() end)
    self.search = ZO_StringSearch:New()
    self.search:AddProcessor(LIBRARIAN_SEARCH, function(stringSearch, data, searchTerm, cache) return self:ProcessBookEntry(stringSearch, data, searchTerm, cache) end)

    self.sortFunction = function(listEntry1, listEntry2) return ZO_TableOrderingFunction(listEntry1.data, listEntry2.data, self.currentSortKey, ENTRY_SORT_KEYS, self.currentSortOrder) end

    self:UpdateSavedVariables()

    local settings = LibrarianSettings:New(self.settings)

    local function OnShowAllBooksClicked(checkButton, isChecked)
        self.settings.showAllBooks = isChecked
        self:RefreshFilters()
    end

    local function GetShowAllBooks()
        return self.settings.showAllBooks
    end

    local showAllBooks = LibrarianFrameShowAllBooks
    ZO_CheckButton_SetToggleFunction(showAllBooks, OnShowAllBooksClicked)
    ZO_CheckButton_SetCheckState(showAllBooks, GetShowAllBooks())

    --self:ImportFromLoreLibrary()
    self:RefreshData()
    self:InitializeKeybindStripDescriptors()
    self:InitializeScene()
    self:AddLoreReaderUnreadToggle()
end

function Librarian:AddLoreReaderUnreadToggle()
    local readerKeybinds
    if LORE_READER.keybindStripDescriptor then
        readerKeybinds = LORE_READER.keybindStripDescriptor
    else
        readerKeybinds = LORE_READER.PCKeybindStripDescriptor
    end

    if readerKeybinds then
        -- Special exit button
        local exitKeybind =
        {
            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
            name = GetString(SI_EXIT_BUTTON),
            keybind = "UI_SHORTCUT_EXIT",
            callback = function() SCENE_MANAGER:HideCurrentScene() end,
        }
        table.insert(readerKeybinds, exitKeybind)

        local toggleKeybind =
        {
            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
            name = function()
                local book = self:FindBook(LORE_READER.titleText)
                if not book or book.unread then
                    if self.settings.showUnreadIndicatorInReader then
                        self.unreadIndicator:SetHidden(false)
                    else
                        self.unreadIndicator:SetHidden(true)
                    end
                    return GetString(SI_LIBRARIAN_MARK_READ)
                else
                    self.unreadIndicator:SetHidden(true)
                    return GetString(SI_LIBRARIAN_MARK_UNREAD)
                end
            end,
            keybind = "UI_SHORTCUT_SECONDARY",
            callback = function()
                local book = self:FindBook(LORE_READER.titleText)
                if not book then return end
                book.unread = not book.unread
                KEYBIND_STRIP:UpdateKeybindButtonGroup(readerKeybinds)
                self:RefreshData()
            end
        }
        table.insert(readerKeybinds, toggleKeybind)
    end

    self.unreadIndicator = WINDOW_MANAGER:CreateControl("LibrarianUnreadIndicator", ZO_LoreReaderBookContainer, CT_TEXTURE)
    self.unreadIndicator:SetAnchor(TOPLEFT, ZO_LoreReaderBookContainerFirstPage, TOPLEFT, -32, 3)
    self.unreadIndicator:SetDimensions(32, 32)
    self.unreadIndicator:SetHidden(true)
    self.unreadIndicator:SetTexture([[EsoUI/Art/Inventory/newitem_icon.dds]])

    local function OnSceneStateChange(scene, oldState, newState)
        if(newState == SCENE_SHOWING) then
            --if KEYBIND_STRIP.defaultExit then
                --KEYBIND_STRIP:RemoveKeybindButton(KEYBIND_STRIP.defaultExit)
            --else
              --  KEYBIND_STRIP:RemoveDefaultExit()
            --end
            KEYBIND_STRIP:AddKeybindButtonGroup(readerKeybinds)
        elseif(newState == SCENE_HIDDEN) then
            KEYBIND_STRIP:RemoveKeybindButtonGroup(readerKeybinds)
            --if KEYBIND_STRIP.defaultExit then
                KEYBIND_STRIP:AddKeybindButton(KEYBIND_STRIP.defaultExit)
           -- else
               -- KEYBIND_STRIP:RestoreDefaultExit()
            --end
        end
    end

    LORE_READER_INTERACTION_SCENE.callbackRegistry.StateChange[1][3] = true
    LORE_READER_INTERACTION_SCENE:Clean("StateChange")
    LORE_READER_INTERACTION_SCENE:RegisterCallback("StateChange", OnSceneStateChange)

    LORE_READER_INVENTORY_SCENE.callbackRegistry.StateChange[1][3] = true
    LORE_READER_INVENTORY_SCENE:Clean("StateChange")
    LORE_READER_INVENTORY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)

    LORE_READER_LORE_LIBRARY_SCENE.callbackRegistry.StateChange[1][3] = true
    LORE_READER_LORE_LIBRARY_SCENE:Clean("StateChange")
    LORE_READER_LORE_LIBRARY_SCENE:RegisterCallback("StateChange", OnSceneStateChange)
end

function Librarian:UpdateSavedVariables()
    -- Version 1.0.4 - Settings moved to global variables.
    if self.localSavedVars.setting_time_format then
        self.globalSavedVars.settings.time_format = self.localSavedVars.setting_time_format
        self.localSavedVars.setting_time_format = nil
    end

    -- Version 1.0.4 - Book data moved to global variables
    if self.localSavedVars.books then
        for _,book in ipairs(self.localSavedVars.books) do
            local timeStamp = book.timeStamp
            local unread = book.unread
            self:AddBook(book)
            local characterBook = self:FindCharacterBook(book.title)
            if characterBook then characterBook.timeStamp = timeStamp end
            local globalBook = self:FindBook(book.title)
            if globalBook then
                globalBook.timeStamp = timeStamp
                globalBook.unread = unread
            end
        end
        self.localSavedVars.books = nil
        self:RefreshData()
    end

    -- Version 1.0.16 - Fixed a couple of settings names.
    if self.globalSavedVars.settings.alert_style then
        self.globalSavedVars.settings.alertStyle = self.globalSavedVars.settings.alert_style
        self.globalSavedVars.settings.alert_style = nil
    end

    if self.globalSavedVars.settings.time_format then
        self.globalSavedVars.settings.timeFormat = self.globalSavedVars.settings.time_format
        self.globalSavedVars.settings.time_format = nil
    end

    -- Version 1.1.3 - SavedVariable hell!
    for _,account in pairs(Librarian_SavedVariables["Default"]) do
        for _,book in pairs(account["$AccountWide"].books) do
            if type(book.body) == "string" then
                local newBody = book.body
                book.body = {}
                while string.len(newBody) > 1024 do
                    table.insert(book.body, string.sub(newBody, 0, 1024))
                    newBody = string.sub(newBody, 1025)
                end
                table.insert(book.body, newBody)
            end
        end
    end
end

function Librarian:InitializeKeybindStripDescriptors()
    self.keybindStripDescriptor =
    {
        {
            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
            name = GetString(SI_LORE_LIBRARY_READ),
            keybind = "UI_SHORTCUT_PRIMARY",
            visible = function()
                return self.mouseOverRow
            end,
            callback = function()
                self:ReadBook(self.mouseOverRow.data.title)
            end,
        },
        {
            alignment = KEYBIND_STRIP_ALIGN_RIGHT,
            name = function()
                if not self.mouseOverRow then return nil end
                local book = self:FindBook(self.mouseOverRow.data.title)
                if book.unread then
                    return GetString(SI_LIBRARIAN_MARK_READ)
                else
                    return GetString(SI_LIBRARIAN_MARK_UNREAD)
                end
            end,
            keybind = "UI_SHORTCUT_SECONDARY",
            visible = function()
                return self.mouseOverRow
            end,
            callback = function()
                local book = self:FindBook(self.mouseOverRow.data.title)
                book.unread = not book.unread
                self:RefreshData()
            end,
        }
    }
end

function Librarian:InitializeScene()
    if not LIBRARIAN_SCENE then
        LIBRARIAN_TITLE_FRAGMENT = ZO_SetTitleFragment:New(SI_WINDOW_TITLE_LIBRARIAN)
        LIBRARIAN_SCENE = ZO_Scene:New("librarian", SCENE_MANAGER)
        LIBRARIAN_SCENE:AddFragmentGroup(FRAGMENT_GROUP.MOUSE_DRIVEN_UI_WINDOW)
        if self.settings.enableCharacterSpin then
            LIBRARIAN_SCENE:AddFragmentGroup(FRAGMENT_GROUP.FRAME_TARGET_STANDARD_RIGHT_PANEL)
        end
        LIBRARIAN_SCENE:AddFragment(ZO_FadeSceneFragment:New(LibrarianFrame))
        LIBRARIAN_SCENE:AddFragment(RIGHT_BG_FRAGMENT)
        LIBRARIAN_SCENE:AddFragment(TITLE_FRAGMENT)
        LIBRARIAN_SCENE:AddFragment(LIBRARIAN_TITLE_FRAGMENT)
        LIBRARIAN_SCENE:AddFragment(CODEX_WINDOW_SOUNDS)

        LIBRARIAN_SCENE:RegisterCallback("StateChange",
            function(oldState, newState)
                if(newState == SCENE_SHOWING) then
                    KEYBIND_STRIP:AddKeybindButtonGroup(self.keybindStripDescriptor)
                elseif(newState == SCENE_HIDDEN) then
                    KEYBIND_STRIP:RemoveKeybindButtonGroup(self.keybindStripDescriptor)
                end
            end)
    end
end

function Librarian:ImportFromLoreLibrary()
    local hasImportedBooks = false
    local chatEnabled = self.settings.chatEnabled
    local alertEnabled = self.settings.alertEnabled
        self.settings.chatEnabled = true
        self.settings.alertEnabled = false
    local identifier = "LibrarianLoreImport"
    local GetLoreBookInfo, ReadLoreBook = GetLoreBookInfo, ReadLoreBook
    local em = GetEventManager()
    local function endSearch()
        em:UnregisterForUpdate(identifier)
        self.settings.chatEnabled = chatEnabled
        self.settings.alertEnabled = alertEnabled
    end

    local categoryIndex, collectionIndex, bookIndex = 0, 0, 0
    local numCollections, totalBooks = 0, 0
    local function step()
        if bookIndex >= totalBooks then
            if collectionIndex >= numCollections then
                if categoryIndex >= GetNumLoreCategories() then
                    endSearch()
                    return false
                else
                    categoryIndex = categoryIndex + 1
                    numCollections = select(2, GetLoreCategoryInfo(categoryIndex))
                    collectionIndex = 0
                end
            end
            collectionIndex = collectionIndex + 1
            totalBooks = select(4, GetLoreCollectionInfo(categoryIndex, collectionIndex))
            bookIndex = 0
        end
        bookIndex = bookIndex + 1
        local title, icon, known = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
        if known then
            if not self:FindCharacterBook(title) then
                local body, medium, showTitle = ReadLoreBook(categoryIndex, collectionIndex, bookIndex)
                local book = { title = title, body = body, medium = medium, showTitle = showTitle }
                self:AddBook(book)
            end
        end
        return true
    end

    local function steps()
    -- do as much as possible in 5ms
        local gettime = GetGameTimeMilliseconds
        local start = gettime()
            while true do
            if ((gettime() - start) >= 5) or not step() then break end
        end
    end
    em:RegisterForUpdate(identifier, 0, steps)
end

function Librarian:CanImportFromEmptyAccount()
    return Librarian_SavedVariables["Default"][""] ~= nil
end

function Librarian:ImportFromEmptyAccount()
    if Librarian_SavedVariables["Default"][""] ~= nil then
        Librarian_SavedVariables["Default"][GetDisplayName()] = Librarian_SavedVariables["Default"][""]
    end
    Librarian_SavedVariables["Default"][""] = nil
    SLASH_COMMANDS["/reloadui"]()
end

function Librarian:BuildMasterList()
    if self:CanImportFromEmptyAccount() and #self.books == 0 then LibrarianFrameMessage:SetHidden(false) end

    for i, book in ipairs(self.books) do
        local data = {}
        for k,v in pairs(book) do
            if k == "body" then
                data[k] = table.concat(book.body)
            else
                data[k] = v
            end
        end
        data.type = LIBRARIAN_SEARCH
        local characterBook = self:FindCharacterBook(book.title)
        if characterBook then
            data.seenByCurrentCharacter = true
            data.timeStamp = characterBook.timeStamp
        else
            data.seenByCurrentCharacter = false
            data.timeStamp = book.timeStamp
        end
        self.masterList[i] = data
    end
end

function Librarian:FilterScrollList()
    local scrollData = ZO_ScrollList_GetDataList(self.list)
    ZO_ClearNumericallyIndexedTable(scrollData)

    local bookCount = 0
    local unreadCount = 0
    local searchTerm = self.searchBox:GetText()
    for i = 1, #self.masterList do
        local data = self.masterList[i]
        if self.settings.showAllBooks or data.seenByCurrentCharacter then
            if(searchTerm == "" or self.search:IsMatch(searchTerm, data)) then
                table.insert(scrollData, ZO_ScrollList_CreateDataEntry(LIBRARIAN_DATA, data))
                bookCount = bookCount + 1
                if data.unread then unreadCount = unreadCount + 1 end
            end
        end
    end

    local message = string.format(GetString(SI_LIBRARIAN_BOOK_COUNT), bookCount)
    if unreadCount > 0 then message = string.format(GetString(SI_LIBRARIAN_UNREAD_COUNT), message, unreadCount) end
    LibrarianFrameBookCount:SetText(message)
end

function Librarian:SortScrollList()
    local scrollData = ZO_ScrollList_GetDataList(self.list)
    table.sort(scrollData, self.sortFunction)
end

function Librarian:SetupBookRow(control, data)
    control.data = data
    control.unread = GetControl(control, "Unread")
    control.found = GetControl(control, "Found")
    control.title = GetControl(control, "Title")
    control.wordCount = GetControl(control, "WordCount")

    control.unread.nonRecolorable = true
    if data.unread then control.unread:SetAlpha(1) else control.unread:SetAlpha(0) end

    control.found.normalColor = ZO_NORMAL_TEXT
    control.found:SetText(self:FormatClockTime(data.timeStamp))

    control.title.normalColor = ZO_NORMAL_TEXT
    control.title:SetText(data.title)

    control.wordCount.normalColor = ZO_NORMAL_TEXT
    control.wordCount:SetText(data.wordCount)

    ZO_SortFilterList.SetupRow(self, control, data)
end

function Librarian:ProcessBookEntry(stringSearch, data, searchTerm, cache)
    local lowerSearchTerm = searchTerm:lower()

    if data.title and zo_plainstrfind(data.title:lower(), lowerSearchTerm) then
        return true
    end

    if data.body and zo_plainstrfind(data.body:lower(), lowerSearchTerm) then
        return true
    end

    return false
end

function Librarian:FindCharacterBook(title)
    if not self.characterBooks then return nil end
    for _,book in pairs(self.characterBooks) do
        if book.title == title then return book end
    end
end

function Librarian:FindBook(title)
    for _,book in pairs(self.books) do
        if book.title == title then return book end
    end
end

function Librarian:AddBook(book)
    if not self:FindCharacterBook(book.title) then
        if not self:FindBook(book.title) then
            book.timeStamp = GetTimeStamp()
            book.unread = true
            local wordCount = 0
            for w in book.body:gmatch("%S+") do wordCount = wordCount + 1 end
            book.wordCount = wordCount

            local newBody = book.body
            book.body = {}
            while string.len(newBody) > 1024 do
                table.insert(book.body, string.sub(newBody, 0, 1024))
                newBody = string.sub(newBody, 1025)
            end
            table.insert(book.body, newBody)

            table.insert(self.books, book)
        end

        local characterBook = { title = book.title, timeStamp = GetTimeStamp() }
        table.insert(self.characterBooks, characterBook)

        self:RefreshData()
        if self.settings.alertEnabled then
            CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_LARGE_TEXT, SOUNDS.BOOK_ACQUIRED, GetString(SI_LIBRARIAN_NEW_BOOK_FOUND))
        end
        if self.settings.chatEnabled then
            d(string.format(GetString(SI_LIBRARIAN_NEW_BOOK_FOUND_WITH_TITLE), book.title))
        end

        self.newBookCount = self.newBookCount + 1
        if self.settings.reloadReminderBookCount and self.settings.reloadReminderBookCount > 0 and self.settings.reloadReminderBookCount <= self.newBookCount then
            d(GetString(SI_LIBRARIAN_RELOAD_REMINDER))
        end
    end
end

function Librarian:Toggle()
    if LibrarianFrame:IsControlHidden() then
        SCENE_MANAGER:Show("librarian")
    else
        SCENE_MANAGER:Hide("librarian")
    end
end

function Librarian:ReadBook(title)
    local book = self:FindBook(title)
    LORE_READER:SetupBook(book.title, book.body and table.concat(book.body), book.medium, book.showTitle)
        SCENE_MANAGER:Push("loreReaderInteraction")

    -- PlaySound(LORE_READER.OpenSound)
end

function Librarian:FormatClockTime(time)
    local midnightSeconds = GetSecondsSinceMidnight()
    local utcSeconds = GetTimeStamp() % 86400
    local offset = midnightSeconds - utcSeconds
    if offset < -43200 then
        offset = offset + 86400
    end

    local dateString = GetDateStringFromTimestamp(time)
    local timeString = ZO_FormatTime((time + offset) % 86400, TIME_FORMAT_STYLE_CLOCK_TIME, self.settings.timeFormat)
    return string.format("%s %s", dateString, timeString)
end

function Librarian:OnSearchTextChanged()
    ZO_EditDefaultText_OnTextChanged(self.searchBox)
    self:RefreshFilters()
end

local function SlashCommand(args)
    Librarian:Toggle()
end

local function OnAddonLoaded(event, addon)
    if addon == "Librarian" then
        LIBRARIAN = Librarian:New()
    end
end

local function OnShowBook(eventCode, title, body, medium, showTitle)
    local book = { title = title, body = body, medium = medium, showTitle = showTitle }
    LIBRARIAN:AddBook(book)
end

function LibrarianRow_OnMouseEnter(control)
    LIBRARIAN:Row_OnMouseEnter(control)
end

function LibrarianRow_OnMouseExit(control)
    LIBRARIAN:Row_OnMouseExit(control)
end

function LibrarianRow_OnMouseUp(control, button, upInside)
    LIBRARIAN:ReadBook(control.data.title)
end

SLASH_COMMANDS["/librarian"] = SlashCommand

EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_ADD_ON_LOADED, OnAddonLoaded)
EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_SHOW_BOOK, OnShowBook)