Librarian = ZO_SortFilterList:Subclass()
Librarian.defaults = {}
Librarian.menuSettings = {
    name = "LibrarianOptions",
    authors = "Orionik, |c4EFFF6Calia1120|r, Flamage",
    version = "3.0",
    optionSlashCommandText = "/librarianOptions"
}
Librarian.slashCommandText = "/librarian"

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()
    return ZO_SortFilterList.New(self, LibrarianFrame)
end

function Librarian:Initialize(...)
    ZO_SortFilterList.Initialize(self, ...)

    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.characterBooksCache = {}

    self.unreadPerCollections = self.localSavedVars.unreadPerCollections

    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 librarianSettings = LibrarianSettings:New(self.settings, self.menuSettings)

    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:RefreshData()
    self:InitializeKeybindStripDescriptors()
    self:InitializeScene()
    self:AddLoreReaderUnreadToggle()
    self:AddLoreLibraryIcons()

    self:ImportFromLoreLibrary()
end

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

    if readerKeybinds then
        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.loreReaderUnreadIndicator:SetHidden(false)
                    else
                        self.loreReaderUnreadIndicator:SetHidden(true)
                    end
                    return GetString(LIBRARIAN_MARK_READ)
                else
                    self.loreReaderUnreadIndicator:SetHidden(true)
                    return GetString(LIBRARIAN_MARK_UNREAD)
                end
            end,
            keybind = "UI_SHORTCUT_SECONDARY",
            callback = function()
                local book = self:FindBook(LORE_READER.titleText)
                if not book then return end
                self:ToggleReadBook(book)
                KEYBIND_STRIP:UpdateKeybindButtonGroup(readerKeybinds)
                self:RefreshAllData()
            end
        }
        table.insert(readerKeybinds, toggleKeybind)
    end

    self.loreReaderUnreadIndicator = WINDOW_MANAGER:CreateControl("LibrarianLoreReaderUnreadIndicator", ZO_LoreReaderBookContainer, CT_TEXTURE)
    self.loreReaderUnreadIndicator:SetAnchor(TOPLEFT, ZO_LoreReaderBookContainerFirstPage, TOPLEFT, -32, 3)
    self.loreReaderUnreadIndicator:SetAlpha(self.settings.unreadIndicatorTransparency)
    self.loreReaderUnreadIndicator:SetDimensions(32, 32)
    self.loreReaderUnreadIndicator:SetHidden(true)
    self.loreReaderUnreadIndicator:SetTexture("esoui/art/miscellaneous/new_icon.dds")
end

function Librarian:AddLoreLibraryIcons()
    if not self.settings.showUnreadIndicatorInLoreLibrary then
        return
    end

    -- ADD UNREAD ICON ON EACH ENTRY UNREAD
    local BOOK_DATA_TYPE = 1
    local scrollList = LORE_LIBRARY.list:GetListControl()
    local initalDataType = scrollList.dataTypes[BOOK_DATA_TYPE]
    scrollList.dataTypes[BOOK_DATA_TYPE] = nil

    local function SetUpBookEntry(control, data)
        initalDataType.setupCallback(control, data)

        local title, _, known = GetLoreBookInfo(data.categoryIndex, data.collectionIndex, data.bookIndex)
        local shouldUnreadIconBeHidden = true
        if known then
            local book = self:FindBook(title)
            if not book or book.unread then
                shouldUnreadIconBeHidden = false
            end
        end
        control:GetNamedChild("UnreadIcon"):SetHidden(shouldUnreadIconBeHidden)
    end

    ZO_ScrollList_AddDataType(scrollList, BOOK_DATA_TYPE, "Librarian_LoreLibrary_BookEntry", initalDataType.height, SetUpBookEntry)

    -- ORDER BOOK LIST TO MAKE UNREAD BOOK FIRST
    local function BookEntryComparator(leftScrollData, rightScrollData)
        local leftData = leftScrollData.data
        local rightData = rightScrollData.data
        local leftTitle, _, leftKnown = GetLoreBookInfo(leftData.categoryIndex, leftData.collectionIndex, leftData.bookIndex)
        local rightTitle, _, rightKnown = GetLoreBookInfo(rightData.categoryIndex, rightData.collectionIndex, rightData.bookIndex)

        if leftKnown == rightKnown then
            if leftKnown then
                local leftBook = self:FindBook(leftTitle)
                local rightBook = self:FindBook(rightTitle)
                if (leftBook and not leftBook.unread) == (rightBook and not rightBook.unread) then
                    return leftTitle < rightTitle
                end

                return leftBook and leftBook.unread
            end
        end

        return leftKnown
    end
    LORE_LIBRARY.list.SortScrollList = function(loreLibrarySelf)
        local scrollData = ZO_ScrollList_GetDataList(loreLibrarySelf.list)
        table.sort(scrollData, BookEntryComparator)
    end

    -- ADD KEYBIND TO MARK BOOK AS READ FROM LORE LIBRARY
    local loreLibraryKeybinds = LORE_LIBRARY.keybindStripDescriptor

    -- Make current secondary tertiary
    for i, keybindDescriptor in ipairs(loreLibraryKeybinds) do
        if keybindDescriptor.keybind == "UI_SHORTCUT_SECONDARY" then
            keybindDescriptor.keybind = "UI_SHORTCUT_TERTIARY"
        end
    end

    local toggleKeybind =
    {
        alignment = KEYBIND_STRIP_ALIGN_RIGHT,
        name = function()
            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
            if selectedRow and selectedRow.known then
                local book = self:FindBook(selectedRow.text:GetText())
                if not book or book.unread then
                    return GetString(LIBRARIAN_MARK_READ)
                else
                    return GetString(LIBRARIAN_MARK_UNREAD)
                end
            end
        end,
        keybind = "UI_SHORTCUT_SECONDARY",
        visible = function()
            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
            return selectedRow and selectedRow.known
        end,
        callback = function()
            local selectedRow = LORE_LIBRARY.list:GetMouseOverRow()
            if selectedRow then
                local book = self:FindBook(selectedRow.text:GetText())
                if not book then
                    local bookId = select(4, GetLoreBookInfo(selectedRow.categoryIndex, selectedRow.collectionIndex, selectedRow.bookIndex))
                    local body, medium, showTitle = ReadLoreBook(selectedRow.categoryIndex, selectedRow.collectionIndex, selectedRow.bookIndex)
                    book = { title = selectedRow.text:GetText(), body = body, medium = medium, showTitle = showTitle, bookId = bookId }
                    self:AddBook(book, true)
                end

                if book then
                    self:ToggleReadBook(book)
                    KEYBIND_STRIP:UpdateKeybindButtonGroup(loreLibraryKeybinds)
                    self:RefreshAllData()
                end
            end
        end
    }
    table.insert(loreLibraryKeybinds, toggleKeybind)

    -- ADD UNREAD ICON ON COLLECTION IF IT CONTAINS AN UNREAD BOOK
    local navigationEntryTemplateInfo = LORE_LIBRARY.navigationTree.templateInfo["ZO_LoreLibraryNavigationEntry"]
    local previousSetupFunction = navigationEntryTemplateInfo.setupFunction

    local function TreeEntrySetup(node, control, data, open)
        previousSetupFunction(node, control, data, open)

        local shouldHideIcon = true
        local statusIcon = control:GetNamedChild("StatusIcon")
        if data.numKnownBooks > 0 then
            if self.unreadPerCollections[data.categoryIndex] and self.unreadPerCollections[data.categoryIndex][data.collectionIndex] then
                if self.unreadPerCollections[data.categoryIndex][data.collectionIndex] > 0 then
                    shouldHideIcon = false
                    statusIcon:SetTexture("esoui/art/miscellaneous/new_icon.dds")
                elseif data.numKnownBooks == data.totalBooks then
                    shouldHideIcon = false
                    statusIcon:SetTexture("esoui/art/miscellaneous/check.dds")
                end
            end
        end
        statusIcon:SetHidden(shouldHideIcon)
    end

    navigationEntryTemplateInfo.setupFunction = TreeEntrySetup
    navigationEntryTemplateInfo.template = "Librarian_LoreLibraryNavigationEntry"
    navigationEntryTemplateInfo.objectPool.templateName = "Librarian_LoreLibraryNavigationEntry"

    -- Fix of ESO because a label header is equal to all of its entries with the current code (API 101033)
    LORE_LIBRARY.navigationTree.templateInfo["ZO_LabelHeader"].equalityFunction = navigationEntryTemplateInfo.equalityFunction
end

function Librarian:UpdateSavedVariables()
    -- before version 3.0, unreadPerCollections didn't exist and bookId wasn't set
    if not self.unreadPerCollections then
        self.localSavedVars.unreadPerCollections = {}
        self.unreadPerCollections = self.localSavedVars.unreadPerCollections

        -- if this character doesn't know any book yet, no need to go further
        if next(self.characterBooks) ~= nil then
            local GetLoreBookInfo, ReadLoreBook = GetLoreBookInfo, ReadLoreBook
            local categoryIndex, collectionIndex, bookIndex = 0, 0, 0
            local numCollections, totalBooks = 0, 0
            while true do
                if bookIndex >= totalBooks then
                    if collectionIndex >= numCollections then
                        if categoryIndex >= GetNumLoreCategories() then
                            break
                        else
                            categoryIndex = categoryIndex + 1
                            local loreCategoryName
                            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, bookId = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
                book = self:FindBook(title)
                if book then
                    book.bookId = bookId
                    if known and book.unread and self:FindCharacterBook(title, bookId) then
                        self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
                    end
                end
            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(LIBRARIAN_MARK_READ)
                else
                    return GetString(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)
                self:ToggleReadBook(book)
                self:RefreshAllData()
            end,
        }
    }
end

function Librarian:InitializeScene()
    if not LIBRARIAN_SCENE then
        LIBRARIAN_TITLE_FRAGMENT = ZO_SetTitleFragment:New(LIBRARIAN_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 GetLoreCategoryInfo, GetLoreCollectionInfo = GetLoreCategoryInfo, GetLoreCollectionInfo
    local GetLoreBookInfo, ReadLoreBook = GetLoreBookInfo, ReadLoreBook
    local em = GetEventManager()
    local function endSearch()
        em:UnregisterForUpdate(identifier)
        self.settings.chatEnabled = chatEnabled
        self.settings.alertEnabled = alertEnabled
        if hasImportedBooks then
            self:RefreshAllData()
        end
    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
                    local loreCategoryName
                    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, bookId = GetLoreBookInfo(categoryIndex, collectionIndex, bookIndex)
        if known then
            if not self:FindCharacterBook(title, bookId) then
                local body, medium, showTitle = ReadLoreBook(categoryIndex, collectionIndex, bookIndex)
                local book = { title = title, body = body, medium = medium, showTitle = showTitle, bookId = bookId }
                self:AddBook(book, false)
                hasImportedBooks = true
            end
        end
        return true
    end

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

function Librarian:BuildMasterList()
    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, book.bookId)
        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(LIBRARIAN_BOOK_COUNT), bookCount)
    if unreadCount > 0 then message = string.format(GetString(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, bookId)
    if not self.characterBooks then
        return nil
    end

    if bookId and self.characterBooksCache[bookId] then
        return self.characterBooksCache[bookId]
    end

    for _,book in pairs(self.characterBooks) do
        if book.title == title then
            if bookId then
                self.characterBooksCache[bookId] = book
            end
            return book
        end
    end

    return nil
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, refreshDataRightAway)
    if not self:FindCharacterBook(book.title, book.bookId) 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)

        if book.unread then
            local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
            self:AddUnreadBookInCollection(categoryIndex, collectionIndex)
        end

        if refreshDataRightAway then
            self:RefreshAllData()
        end

        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))
            local params = CENTER_SCREEN_ANNOUNCE:CreateMessageParams(CSA_EVENT_LARGE_TEXT, SOUNDS.BOOK_ACQUIRED)
            params:SetCSAType(CENTER_SCREEN_ANNOUNCE_TYPE_DISPLAY_ANNOUNCEMENT )
            params:SetText(GetString(LIBRARIAN_NEW_BOOK_FOUND))
            CENTER_SCREEN_ANNOUNCE:AddMessageWithParams(params)
        end
        if self.settings.chatEnabled then
            d(string.format(GetString(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(LIBRARIAN_RELOAD_REMINDER))
        end
    end
end

function Librarian:AddUnreadBookInCollection(categoryIndex, collectionIndex)
    if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
    if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
    self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
end

function Librarian:ToggleReadBook(book)
    book.unread = not book.unread

    local categoryIndex, collectionIndex = GetLoreBookIndicesFromBookId(book.bookId)
    if categoryIndex and collectionIndex then
        if not self.unreadPerCollections[categoryIndex] then self.unreadPerCollections[categoryIndex] = {} end
        if not self.unreadPerCollections[categoryIndex][collectionIndex] then self.unreadPerCollections[categoryIndex][collectionIndex] = 0 end
        if book.unread then
            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] + 1
        elseif self.unreadPerCollections[categoryIndex][collectionIndex] > 0 then
            self.unreadPerCollections[categoryIndex][collectionIndex] = self.unreadPerCollections[categoryIndex][collectionIndex] - 1
        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:RefreshAllData()
    self:RefreshData()

    if self.settings.showUnreadIndicatorInLoreLibrary then
        local selectedCategoryIndex = LORE_LIBRARY:GetSelectedCategoryIndex()
        local selectedCollectionIndex = LORE_LIBRARY:GetSelectedCollectionIndex()
        local selectedCollectionId = select(7, GetLoreCollectionInfo(selectedCategoryIndex, selectedCollectionIndex))
        LORE_LIBRARY:SetCollectionIdToSelect(selectedCollectionId)
        LORE_LIBRARY:BuildCategoryList()
    end
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)

    -- Let's wait a little bit before updating the filters if the player didn't have time to enter its full string yet
    -- Because if we try to update the filters too much it sometimes crash
    local TIME_BEFORE_UPDATING_FILTERS = 600 -- ms
    self.lastTimeSearchModified = GetGameTimeMilliseconds()
    local function TryRefreshFilters()
        if self.lastTimeSearchModified + TIME_BEFORE_UPDATING_FILTERS <= GetGameTimeMilliseconds() then
            self:RefreshFilters()
        end
    end
    zo_callLater(TryRefreshFilters, TIME_BEFORE_UPDATING_FILTERS)
end

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

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

function Librarian.OnMouseEnterRow(control)
    LIBRARIAN:Row_OnMouseEnter(control)
end

function Librarian.OnMouseExitRow(control)
    LIBRARIAN:Row_OnMouseExit(control)
end

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

function Librarian.OnAddonLoaded(event, addon)
    if addon == "Librarian" then
        EVENT_MANAGER:UnregisterForEvent("Librarian", EVENT_ADD_ON_LOADED)
        LIBRARIAN = Librarian:New()
        EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_SHOW_BOOK, Librarian.OnShowBook)
    end
end

SLASH_COMMANDS[Librarian.slashCommandText] = Librarian.SlashCommand

EVENT_MANAGER:RegisterForEvent("Librarian", EVENT_ADD_ON_LOADED, Librarian.OnAddonLoaded)