local BD = require("ui/bidi")
local DocumentRegistry = require("document/documentregistry")
local DocSettings = require("docsettings")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local ImageViewer = require("ui/widget/imageviewer")
local InfoMessage = require("ui/widget/infomessage")
local Menu = require("ui/widget/menu")
local TextViewer = require("ui/widget/textviewer")
local UIManager = require("ui/uimanager")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local logger = require("logger")
local _ = require("gettext")
local util = require("util")

local BookInfoManager = require("bookinfomanager")

-- This is a kind of "base class" for both MosaicMenu and ListMenu.
-- It implements the common code shared by these, mostly the non-UI
-- work : the updating of items and the management of backgrouns jobs.
--
-- Here are defined the common overriden methods of Menu:
--    :updateItems(select_number)
--    :onCloseWidget()
--
-- MosaicMenu or ListMenu should implement specific UI methods:
--    :_recalculateDimen()
--    :_updateItemsBuildUI()
-- This last method is called in the middle of :updateItems() , and
-- should fill self.item_group with some specific UI layout. It may add
-- not found item to self.items_to_update for us to update() them
-- regularly.

-- Store these as local, to be set by some object and re-used by
-- another object (as we plug the methods below to different objects,
-- we can't store them in 'self' if we want another one to use it)
local current_path = nil
local current_cover_specs = false

-- Do some collectgarbage() every few drawings
local NB_DRAWINGS_BETWEEN_COLLECTGARBAGE = 5
local nb_drawings_since_last_collectgarbage = 0

-- Simple holder of methods that will replace those
-- in the real Menu class or instance
local CoverMenu = {}

function CoverMenu:updateItems(select_number)
    -- As done in Menu:updateItems()
    local old_dimen = self.dimen and self.dimen:copy()
    -- self.layout must be updated for focusmanager
    self.layout = {}
    self.item_group:clear()
    -- NOTE: Our various _recalculateDimen overloads appear to have a stronger dependency
    --       on the rest of the widget elements being properly laid-out,
    --       so we have to run it *first*, unlike in Menu.
    --       Otherwise, various layout issues arise (e.g., MosaicMenu's page_info is misaligned).
    self:_recalculateDimen()
    self.page_info:resetLayout()
    self.return_button:resetLayout()
    self.vertical_span:clear()
    self.content_group:resetLayout()
    -- default to select the first item
    if not select_number then
        select_number = 1
    end

    -- Reset the list of items not found in db that will need to
    -- be updated by a scheduled action
    self.items_to_update = {}
    -- Cancel any previous (now obsolete) scheduled update
    if self.items_update_action then
        UIManager:unschedule(self.items_update_action)
        self.items_update_action = nil
    end

    -- Force garbage collecting before drawing a new page.
    -- It's not really needed from a memory usage point of view, we did
    -- all the free() where necessary, and koreader memory usage seems
    -- stable when file browsing only (15-25 MB).
    -- But I witnessed some freezes after browsing a lot when koreader's main
    -- process was using 100% cpu (and some slow downs while drawing soon before
    -- the freeze, like the full refresh happening before the final drawing of
    -- new text covers), while still having a small memory usage (20/30 Mb)
    -- that I suspect may be some garbage collecting happening at one point
    -- and getting stuck...
    -- With this, garbage collecting may be more deterministic, and it has
    -- no negative impact on user experience.
    -- But don't do it on every drawing, to not have all of them slow
    -- when memory usage is already high
    nb_drawings_since_last_collectgarbage = nb_drawings_since_last_collectgarbage + 1
    if nb_drawings_since_last_collectgarbage >= NB_DRAWINGS_BETWEEN_COLLECTGARBAGE then
        -- (delay it a bit so this pause is less noticable)
        UIManager:scheduleIn(0.2, function()
            collectgarbage()
            collectgarbage()
        end)
        nb_drawings_since_last_collectgarbage = 0
    end

    -- Specific UI building implementation (defined in some other module)
    self._has_cover_images = false
    self:_updateItemsBuildUI()

    -- Set the local variables with the things we know
    -- These are used only by extractBooksInDirectory(), which should
    -- use the cover_specs set for FileBrowser, and not those from History.
    -- Hopefully, we get self.path=nil when called fro History
    if self.path then
        current_path = self.path
        current_cover_specs = self.cover_specs
    end

    -- As done in Menu:updateItems()
    self:updatePageInfo(select_number)

    if self.show_path then
        self.title_bar:setSubTitle(BD.directory(filemanagerutil.abbreviate(self.path)))
    end
    self.show_parent.dithered = self._has_cover_images
    UIManager:setDirty(self.show_parent, function()
        local refresh_dimen =
            old_dimen and old_dimen:combine(self.dimen)
            or self.dimen
        return "ui", refresh_dimen, self.show_parent.dithered
    end)

    -- As additionally done in FileChooser:updateItems()
    if self.path_items then
        self.path_items[self.path] = (self.page - 1) * self.perpage + (select_number or 1)
    end

    -- Deal with items not found in db
    if #self.items_to_update > 0 then
        -- Prepare for background info extraction job
        local files_to_index = {} -- table of {filepath, cover_specs}
        for i=1, #self.items_to_update do
            table.insert(files_to_index, {
                filepath = self.items_to_update[i].filepath,
                cover_specs = self.items_to_update[i].cover_specs
            })
        end
        -- Launch it at nextTick, so UIManager can render us smoothly
        UIManager:nextTick(function()
            local launched = BookInfoManager:extractInBackground(files_to_index)
            if not launched then -- fork failed (never experienced that, but let's deal with it)
                -- Cancel scheduled update, as it won't get any result
                if self.items_update_action then
                    UIManager:unschedule(self.items_update_action)
                    self.items_update_action = nil
                end
                UIManager:show(InfoMessage:new{
                    text = _("Start-up of background extraction job failed.\nPlease restart KOReader or your device.")
                })
            end
        end)

        -- Scheduled update action
        self.items_update_action = function()
            logger.dbg("Scheduled items update:", #self.items_to_update, "waiting")
            local is_still_extracting = BookInfoManager:isExtractingInBackground()
            local i = 1
            while i <= #self.items_to_update do -- process and clean in-place
                local item = self.items_to_update[i]
                item:update()
                if item.bookinfo_found then
                    logger.dbg("  found", item.text)
                    self.show_parent.dithered = item._has_cover_image
                    local refreshfunc = function()
                        if item.refresh_dimen then
                            -- MosaicMenuItem may exceed its own dimen in its paintTo
                            -- with its "description" hint
                            return "ui", item.refresh_dimen, self.show_parent.dithered
                        else
                            return "ui", item[1].dimen, self.show_parent.dithered
                        end
                    end
                    UIManager:setDirty(self.show_parent, refreshfunc)
                    table.remove(self.items_to_update, i)
                else
                    logger.dbg("  not yet found", item.text)
                    i = i + 1
                end
            end
            if #self.items_to_update > 0 then -- re-schedule myself
                if is_still_extracting then -- we have still chances to get new stuff
                    logger.dbg("re-scheduling items update:", #self.items_to_update, "still waiting")
                    UIManager:scheduleIn(1, self.items_update_action)
                else
                    logger.dbg("Not all items found, but background extraction has stopped, not re-scheduling")
                end
            else
                logger.dbg("items update completed")
            end
        end
        UIManager:scheduleIn(1, self.items_update_action)
    end

    -- (We may not need to do the following if we extend showFileDialog
    -- code in filemanager.lua to check for existence and call a
    -- method: self:getAdditionalButtons() to add our buttons
    -- to its own set.)

    -- We want to add some buttons to the showFileDialog popup. This function
    -- is dynamically created by FileManager:init(), and we don't want
    -- to override this... So, here, when we see the showFileDialog function,
    -- we replace it by ours.
    -- (FileManager may replace file_chooser.showFileDialog after we've been called once, so we need
    -- to replace it again if it is not ours)
    if not self.showFileDialog_ours -- never replaced
            or self.showFileDialog ~= self.showFileDialog_ours then -- it is no more ours
        -- We need to do it at nextTick, once FileManager has instantiated
        -- its FileChooser completely
        UIManager:nextTick(function()
            -- Store original function, so we can call it
            self.showFileDialog_orig = self.showFileDialog

            -- Replace it with ours
            -- This causes luacheck warning: "shadowing upvalue argument 'self' on line 34".
            -- Ignoring it (as done in filemanager.lua for the same showFileDialog)
            self.showFileDialog = function(self, file) -- luacheck: ignore
                -- Call original function: it will create a ButtonDialogTitle
                -- and store it as self.file_dialog, and UIManager:show() it.
                self.showFileDialog_orig(self, file)

                local bookinfo = BookInfoManager:getBookInfo(file)
                if not bookinfo or bookinfo._is_directory then
                    -- If no bookinfo (yet) about this file, or it's a directory, let the original dialog be
                    return true
                end

                -- Remember some of this original ButtonDialogTitle properties
                local orig_title = self.file_dialog.title
                local orig_title_align = self.file_dialog.title_align
                local orig_buttons = self.file_dialog.buttons
                -- Close original ButtonDialogTitle (it has not yet been painted
                -- on screen, so we won't see it)
                UIManager:close(self.file_dialog)
                -- And clear the rendering stack to avoid inheriting its dirty/refresh queue
                UIManager:clearRenderStack()

                -- Add some new buttons to original buttons set
                table.insert(orig_buttons, {
                    { -- Allow user to view real size cover in ImageViewer
                        text = _("View full size cover"),
                        enabled = bookinfo.has_cover and true or false,
                        callback = function()
                            local document = DocumentRegistry:openDocument(file)
                            if document then
                                if document.loadDocument then -- needed for crengine
                                    document:loadDocument(false) -- load only metadata
                                end
                                local cover_bb = document:getCoverPageImage()
                                if cover_bb then
                                    local imgviewer = ImageViewer:new{
                                        image = cover_bb,
                                        with_title_bar = false,
                                        fullscreen = true,
                                    }
                                    UIManager:show(imgviewer)
                                else
                                    UIManager:show(InfoMessage:new{
                                        text = _("No cover image available."),
                                    })
                                end
                                UIManager:close(self.file_dialog)
                                document:close()
                            end
                        end,
                    },
                    { -- Allow user to directly view description in TextViewer
                        text = _("Book description"),
                        enabled = bookinfo.description and true or false,
                        callback = function()
                            local description = util.htmlToPlainTextIfHtml(bookinfo.description)
                            local textviewer = TextViewer:new{
                                title = _("Description:"),
                                text = description,
                            }
                            UIManager:close(self.file_dialog)
                            UIManager:show(textviewer)
                        end,
                    },
                })
                table.insert(orig_buttons, {
                    { -- Allow user to ignore some offending cover image
                        text = bookinfo.ignore_cover and _("Unignore cover") or _("Ignore cover"),
                        enabled = bookinfo.has_cover and true or false,
                        callback = function()
                            BookInfoManager:setBookInfoProperties(file, {
                                ["ignore_cover"] = not bookinfo.ignore_cover and 'Y' or false,
                            })
                            UIManager:close(self.file_dialog)
                            self:updateItems()
                        end,
                    },
                    { -- Allow user to ignore some bad metadata (filename will be used instead)
                        text = bookinfo.ignore_meta and _("Unignore metadata") or _("Ignore metadata"),
                        enabled = bookinfo.has_meta and true or false,
                        callback = function()
                            BookInfoManager:setBookInfoProperties(file, {
                                ["ignore_meta"] = not bookinfo.ignore_meta and 'Y' or false,
                            })
                            UIManager:close(self.file_dialog)
                            self:updateItems()
                        end,
                    },
                })
                table.insert(orig_buttons, {
                    { -- Mark the book as read/unread
                        text_func = function()
                            -- If the book has a cache entry, it means it has a sidecar file, and it *may* have the info we need.
                            local status
                            if self.cover_info_cache and self.cover_info_cache[file] then
                                local _, _, c_status = unpack(self.cover_info_cache[file])
                                status = c_status
                            end
                            -- NOTE: status may still be nil if the BookStatus widget was never opened in this book.
                            --       For our purposes, we assume this means reading or on hold, which is just fine.
                            -- NOTE: This also means we assume "on hold" means reading, meaning it'll be flipped to "finished",
                            --       which I'm personally okay with, too.
                            --       c.f., BookStatusWidget:generateSwitchGroup for the three possible constant values.
                            return status == "complete" and _("Mark as reading") or _("Mark as read")
                        end,
                        callback = function()
                            local status
                            if self.cover_info_cache and self.cover_info_cache[file] then
                                local c_pages, c_percent_finished, c_status = unpack(self.cover_info_cache[file])
                                status = c_status == "complete" and "reading" or "complete"
                                -- Update the cache, even if it had a nil status before
                                self.cover_info_cache[file] = {c_pages, c_percent_finished, status}
                            else
                                -- We assumed earlier an empty status meant "reading", so, flip that to "complete"
                                status = "complete"
                            end

                            -- In case the book doesn't have a sidecar file, this'll create it
                            local docinfo = DocSettings:open(file)
                            if docinfo.data.summary and docinfo.data.summary.status then
                                -- Book already had the full BookStatus table in its sidecar, easy peasy!
                                docinfo.data.summary.status = status
                            else
                                -- No BookStatus table, create a minimal one...
                                if docinfo.data.summary then
                                    -- Err, a summary table with no status entry? Should never happen...
                                    local summary = { status = status }
                                    -- Append the status entry to the existing summary...
                                    util.tableMerge(docinfo.data.summary, summary)
                                else
                                    -- No summary table at all, create a minimal one
                                    local summary = { status = status }
                                    docinfo:saveSetting("summary", summary)
                                end
                            end
                            docinfo:flush()

                            UIManager:close(self.file_dialog)
                            self:updateItems()
                        end,
                    },
                    { -- Allow a new extraction (multiple interruptions, book replaced)...
                        text = _("Refresh book info"),
                        enabled = bookinfo and true or false,
                        callback = function()
                            -- Wipe the cache
                            if self.cover_info_cache and self.cover_info_cache[file] then
                                self.cover_info_cache[file] = nil
                            end
                            BookInfoManager:deleteBookInfo(file)
                            UIManager:close(self.file_dialog)
                            self:updateItems()
                        end,
                    },
                })

                -- Create the new ButtonDialogTitle, and let UIManager show it
                local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
                self.file_dialog = ButtonDialogTitle:new{
                    title = orig_title,
                    title_align = orig_title_align,
                    buttons = orig_buttons,
                }

                -- Fudge the "Reset settings" button callback to also trash the cover_info_cache
                local button = self.file_dialog.button_table:getButtonById("reset_settings")
                local orig_purge_callback = button.callback
                button.callback = function()
                    -- Wipe the cache
                    if self.cover_info_cache and self.cover_info_cache[file] then
                        self.cover_info_cache[file] = nil
                    end
                    -- And then purge the sidecar folder as expected
                    orig_purge_callback()
                end

                -- Replace the "Book information" button callback to use directly our bookinfo
                button = self.file_dialog.button_table:getButtonById("book_information")
                button.callback = function()
                    FileManagerBookInfo:show(file, bookinfo)
                    UIManager:close(self.file_dialog)
                end

                UIManager:show(self.file_dialog)
                return true
            end

            -- Remember our function
            self.showFileDialog_ours = self.showFileDialog
        end)
    end
    Menu.mergeTitleBarIntoLayout(self)
end

-- Similar to showFileDialog setup just above, but for History,
-- which is plugged in main.lua _FileManagerHistory_updateItemTable()
function CoverMenu:onHistoryMenuHold(item)
    -- Call original function: it will create a ButtonDialog
    -- and store it as self.histfile_dialog, and UIManager:show() it.
    self.onMenuHold_orig(self, item)
    local file = item.file

    local bookinfo = BookInfoManager:getBookInfo(file)
    if not bookinfo then
        -- If no bookinfo (yet) about this file, let the original dialog be
        return true
    end

    -- Remember some of this original ButtonDialogTitle properties
    local orig_title = self.histfile_dialog.title
    local orig_title_align = self.histfile_dialog.title_align
    local orig_buttons = self.histfile_dialog.buttons
    -- Close original ButtonDialog (it has not yet been painted
    -- on screen, so we won't see it)
    UIManager:close(self.histfile_dialog)
    UIManager:clearRenderStack()

    -- Replace Book information callback to use directly our bookinfo
    orig_buttons[2][2].callback = function()
        FileManagerBookInfo:show(file, bookinfo)
        UIManager:close(self.histfile_dialog)
    end

    -- Add some new buttons to original buttons set
    table.insert(orig_buttons, {
        { -- Allow user to view real size cover in ImageViewer
            text = _("View full size cover"),
            enabled = bookinfo.has_cover and true or false,
            callback = function()
                local document = DocumentRegistry:openDocument(file)
                if document then
                    if document.loadDocument then -- needed for crengine
                        document:loadDocument(false) -- load only metadata
                    end
                    local cover_bb = document:getCoverPageImage()
                    if cover_bb then
                        local imgviewer = ImageViewer:new{
                            image = cover_bb,
                            with_title_bar = false,
                            fullscreen = true,
                        }
                        UIManager:show(imgviewer)
                    else
                        UIManager:show(InfoMessage:new{
                            text = _("No cover image available."),
                        })
                    end
                    UIManager:close(self.histfile_dialog)
                    document:close()
                end
            end,
        },
        { -- Allow user to directly view description in TextViewer
            text = _("Book description"),
            enabled = bookinfo.description and true or false,
            callback = function()
                local description = util.htmlToPlainTextIfHtml(bookinfo.description)
                local textviewer = TextViewer:new{
                    title = _("Description:"),
                    text = description,
                }
                UIManager:close(self.histfile_dialog)
                UIManager:show(textviewer)
            end,
        },
    })
    table.insert(orig_buttons, {
        { -- Allow user to ignore some offending cover image
            text = bookinfo.ignore_cover and _("Unignore cover") or _("Ignore cover"),
            enabled = bookinfo.has_cover and true or false,
            callback = function()
                BookInfoManager:setBookInfoProperties(file, {
                    ["ignore_cover"] = not bookinfo.ignore_cover and 'Y' or false,
                })
                UIManager:close(self.histfile_dialog)
                self:updateItems()
            end,
        },
        { -- Allow user to ignore some bad metadata (filename will be used instead)
            text = bookinfo.ignore_meta and _("Unignore metadata") or _("Ignore metadata"),
            enabled = bookinfo.has_meta and true or false,
            callback = function()
                BookInfoManager:setBookInfoProperties(file, {
                    ["ignore_meta"] = not bookinfo.ignore_meta and 'Y' or false,
                })
                UIManager:close(self.histfile_dialog)
                self:updateItems()
            end,
        },
    })
    table.insert(orig_buttons, {
        { -- Allow a new extraction (multiple interruptions, book replaced)...
            text = _("Refresh cached book information"),
            enabled = bookinfo and true or false,
            callback = function()
                BookInfoManager:deleteBookInfo(file)
                UIManager:close(self.histfile_dialog)
                self:updateItems()
            end,
        },
    })

    -- Create the new ButtonDialog, and let UIManager show it
    local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
    self.histfile_dialog = ButtonDialogTitle:new{
        title = orig_title,
        title_align = orig_title_align,
        buttons = orig_buttons,
    }
    UIManager:show(self.histfile_dialog)
    return true
end

-- Similar to showFileDialog setup just above, but for Collections,
-- which is plugged in main.lua _FileManagerCollections_updateItemTable()
function CoverMenu:onCollectionsMenuHold(item)
    -- Call original function: it will create a ButtonDialog
    -- and store it as self.collfile_dialog, and UIManager:show() it.
    self.onMenuHold_orig(self, item)
    local file = item.file

    local bookinfo = BookInfoManager:getBookInfo(file)
    if not bookinfo then
        -- If no bookinfo (yet) about this file, let the original dialog be
        return true
    end

    -- Remember some of this original ButtonDialogTitle properties
    local orig_title = self.collfile_dialog.title
    local orig_title_align = self.collfile_dialog.title_align
    local orig_buttons = self.collfile_dialog.buttons
    -- Close original ButtonDialog (it has not yet been painted
    -- on screen, so we won't see it)
    UIManager:close(self.collfile_dialog)
    UIManager:clearRenderStack()

    -- Replace Book information callback to use directly our bookinfo
    orig_buttons[2][1].callback = function()
        FileManagerBookInfo:show(file, bookinfo)
        UIManager:close(self.collfile_dialog)
    end

    -- Add some new buttons to original buttons set
    table.insert(orig_buttons, {
        { -- Allow user to view real size cover in ImageViewer
            text = _("View full size cover"),
            enabled = bookinfo.has_cover and true or false,
            callback = function()
                local document = DocumentRegistry:openDocument(file)
                if document then
                    if document.loadDocument then -- needed for crengine
                        document:loadDocument(false) -- load only metadata
                    end
                    local cover_bb = document:getCoverPageImage()
                    if cover_bb then
                        local imgviewer = ImageViewer:new{
                            image = cover_bb,
                            with_title_bar = false,
                            fullscreen = true,
                        }
                        UIManager:show(imgviewer)
                    else
                        UIManager:show(InfoMessage:new{
                            text = _("No cover image available."),
                        })
                    end
                    UIManager:close(self.collfile_dialog)
                    document:close()
                end
            end,
        },
        { -- Allow user to directly view description in TextViewer
            text = _("Book description"),
            enabled = bookinfo.description and true or false,
            callback = function()
                local description = util.htmlToPlainTextIfHtml(bookinfo.description)
                local textviewer = TextViewer:new{
                    title = _("Description:"),
                    text = description,
                }
                UIManager:close(self.collfile_dialog)
                UIManager:show(textviewer)
            end,
        },
    })
    table.insert(orig_buttons, {
        { -- Allow user to ignore some offending cover image
            text = bookinfo.ignore_cover and _("Unignore cover") or _("Ignore cover"),
            enabled = bookinfo.has_cover and true or false,
            callback = function()
                BookInfoManager:setBookInfoProperties(file, {
                    ["ignore_cover"] = not bookinfo.ignore_cover and 'Y' or false,
                })
                UIManager:close(self.collfile_dialog)
                self:updateItems()
            end,
        },
        { -- Allow user to ignore some bad metadata (filename will be used instead)
            text = bookinfo.ignore_meta and _("Unignore metadata") or _("Ignore metadata"),
            enabled = bookinfo.has_meta and true or false,
            callback = function()
                BookInfoManager:setBookInfoProperties(file, {
                    ["ignore_meta"] = not bookinfo.ignore_meta and 'Y' or false,
                })
                UIManager:close(self.collfile_dialog)
                self:updateItems()
            end,
        },
    })
    table.insert(orig_buttons, {
        { -- Allow a new extraction (multiple interruptions, book replaced)...
            text = _("Refresh cached book information"),
            enabled = bookinfo and true or false,
            callback = function()
                BookInfoManager:deleteBookInfo(file)
                UIManager:close(self.collfile_dialog)
                self:updateItems()
            end,
        },
    })
    -- Create the new ButtonDialog, and let UIManager show it
    local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
    self.collfile_dialog = ButtonDialogTitle:new{
        title = orig_title,
        title_align = orig_title_align,
        buttons = orig_buttons,
    }
    UIManager:show(self.collfile_dialog)
    return true
end

function CoverMenu:onCloseWidget()
    -- Due to close callback in FileManagerHistory:onShowHist, we may be called
    -- multiple times (witnessed that with print(debug.traceback())
    -- So, avoid doing what follows twice
    if self._covermenu_onclose_done then
        return
    end
    self._covermenu_onclose_done = true

    -- Stop background job if any (so that full cpu is available to reader)
    logger.dbg("CoverMenu:onCloseWidget: terminating jobs if needed")
    BookInfoManager:terminateBackgroundJobs()
    BookInfoManager:closeDbConnection() -- sqlite connection no more needed
    BookInfoManager:cleanUp() -- clean temporary resources

    -- Cancel any still scheduled update
    if self.items_update_action then
        logger.dbg("CoverMenu:onCloseWidget: unscheduling items_update_action")
        UIManager:unschedule(self.items_update_action)
        self.items_update_action = nil
    end

    -- Propagate a call to free() to all our sub-widgets, to release memory used by their _bb
    self.item_group:free()

    -- Clean any short term cache (used by ListMenu to cache some DocSettings info)
    self.cover_info_cache = nil

    -- Force garbage collecting when leaving too
    -- (delay it a bit so this pause is less noticable)
    UIManager:scheduleIn(0.2, function()
        collectgarbage()
        collectgarbage()
    end)
    nb_drawings_since_last_collectgarbage = 0

    -- Call the object's original onCloseWidget (i.e., Menu's, as none our our expected subclasses currently implement it)
    Menu.onCloseWidget(self)
end

function CoverMenu:tapPlus()
    -- Call original function: it will create a ButtonDialogTitle
    -- and store it as self.file_dialog, and UIManager:show() it.
    CoverMenu._FileManager_tapPlus_orig(self)
    if self.file_dialog.select_mode then return end -- do not change select menu

    -- Remember some of this original ButtonDialogTitle properties
    local orig_title = self.file_dialog.title
    local orig_title_align = self.file_dialog.title_align
    local orig_buttons = self.file_dialog.buttons
    -- Close original ButtonDialogTitle (it has not yet been painted
    -- on screen, so we won't see it)
    UIManager:close(self.file_dialog)
    UIManager:clearRenderStack()

    -- Add a new button to original buttons set
    table.insert(orig_buttons, {}) -- separator
    table.insert(orig_buttons, {
        {
            text = _("Extract and cache book information"),
            callback = function()
                UIManager:close(self.file_dialog)
                local Trapper = require("ui/trapper")
                Trapper:wrap(function()
                    BookInfoManager:extractBooksInDirectory(current_path, current_cover_specs)
                end)
            end,
        },
    })

    -- Create the new ButtonDialogTitle, and let UIManager show it
    local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
    self.file_dialog = ButtonDialogTitle:new{
        title = orig_title,
        title_align = orig_title_align,
        buttons = orig_buttons,
    }
    UIManager:show(self.file_dialog)
    return true
end

return CoverMenu
