local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local LeftContainer = require("ui/widget/container/leftcontainer")
local Menu = require("ui/widget/menu")
local OverlapGroup = require("ui/widget/overlapgroup")
local RenderText = require("ui/rendertext")
local ScrollableContainer = require("ui/widget/container/scrollablecontainer")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget")
local TitleBar = require("ui/widget/titlebar")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local Widget = require("ui/widget/widget")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Input = Device.input
local Screen = Device.screen
local logger = require("logger")
local util = require("util")
local _ = require("gettext")

-- BookMapRow (reused by PageBrowserWidget)
local BookMapRow = WidgetContainer:extend{
    width = nil,
    height = nil,
    pages_frame_border = Size.border.default,
    toc_span_border = Size.border.thin,
    -- pages_frame_border = 10, -- for debugging positionning
    -- toc_span_border = 5, -- for debugging positionning
    toc_items = nil, -- Arrays[levels] of arrays[items at this level to show as spans]
    -- Many other options not described here, see BookMapWidget:update()
    -- for the complete list.
}

function BookMapRow:getPageX(page, right_edge)
    local _mirroredUI = BD.mirroredUILayout()
    if right_edge then
        if (not _mirroredUI and page == self.end_page) or
               (_mirroredUI and page == self.start_page) then
            return self.pages_frame_inner_width
        else
            if _mirroredUI then
                return self:getPageX(page-1)
            else
                return self:getPageX(page+1)
            end
        end
    end
    local slot_idx
    if _mirroredUI then
        slot_idx = self.end_page - page
    else
        slot_idx = page - self.start_page
    end
    local x = slot_idx * self.page_slot_width
    x = x + math.floor(self.page_slot_extra * slot_idx / self.nb_page_slots)
    return x
end

function BookMapRow:getPageAtX(x, at_bounds_if_outside)
    x = x - self.pages_frame_offset_x
    if x < 0 then
        if not at_bounds_if_outside then return end
        x = 0
    end
    if x >= self.pages_frame_inner_width then
        if not at_bounds_if_outside then return end
        x = self.pages_frame_inner_width - 1
    end
    -- Reverse of the computation in :getPageX():
    local slot_idx = math.floor(x / (self.page_slot_width + self.page_slot_extra / self.nb_page_slots))
    if BD.mirroredUILayout() then
        return self.end_page - slot_idx
    else
        return self.start_page + slot_idx
    end
end

-- Helper function to be used before instantiating a BookMapRow instance,
-- to obtain the left_spacing equivalent to not showing nb_pages at start
-- of a row of pages_per_row items in a width of row_width
function BookMapRow:getLeftSpacingForNumberOfPageSlots(nb_pages, pages_per_row, row_width)
    -- Bits of the computation done in :init()
    local pages_frame_inner_width = row_width - 2*self.pages_frame_border
    local page_slot_width = math.floor(pages_frame_inner_width / pages_per_row)
    local page_slot_extra = pages_frame_inner_width - page_slot_width * pages_per_row
    -- Bits of the computation done in :getPageX()
    local x = nb_pages * page_slot_width
    x = x + math.floor(page_slot_extra * nb_pages / pages_per_row)
    return x - self.pages_frame_border
end

function BookMapRow:init()
    local _mirroredUI = BD.mirroredUILayout()
    self.dimen = Geom:new{ x = 0, y = 0, w = self.width, h = self.height }

    -- Keep one span_height under baseline (frame bottom border) for indicators (current page, bookmarks)
    self.pages_frame_height = self.height - self.span_height

    self.pages_frame_offset_x = self.left_spacing + self.pages_frame_border
    self.pages_frame_width = self.width - self.left_spacing
    self.pages_frame_inner_width = self.pages_frame_width - 2*self.pages_frame_border
    self.page_slot_width = math.floor(self.pages_frame_inner_width / self.pages_per_row)
    self.page_slot_extra = self.pages_frame_inner_width - self.page_slot_width * self.pages_per_row -- will be distributed

    -- Update the widths if this row contains fewer pages
    self.nb_page_slots = self.end_page - self.start_page + 1
    if self.nb_page_slots ~= self.pages_per_row then
        self.page_slot_extra = math.floor(self.page_slot_extra * self.nb_page_slots / self.pages_per_row)
        self.pages_frame_inner_width = self.page_slot_width * self.nb_page_slots + self.page_slot_extra
        self.pages_frame_width = self.pages_frame_inner_width + 2*self.pages_frame_border
    end

    if _mirroredUI then
        self.pages_frame_offset_x = self.width - self.pages_frame_width - self.left_spacing + self.pages_frame_border
    end

    -- We draw a frame container with borders for the book content, with
    -- some space on the left for the start page number, and some space
    -- below the bottom border as spacing before the next row (spacing
    -- that we can use to show current position and hanging bookmarks
    -- and highlights symbols)
    self.pages_frame = OverlapGroup:new{
        dimen = Geom:new{
            w = self.pages_frame_width,
            h = self.pages_frame_height,
        },
        allow_mirroring = false, -- we handle mirroring ourselves below
        FrameContainer:new{
            overlap_align = _mirroredUI and "right" or "left",
            margin = 0,
            padding = 0,
            bordersize = self.pages_frame_border,
            -- color = Blitbuffer.COLOR_GRAY, -- for debugging positionning
            Widget:new{ -- empty widget to give dimensions around which to draw borders
                dimen = Geom:new{
                    w = self.pages_frame_inner_width,
                    h = self.pages_frame_height - 2*self.pages_frame_border,
                }
            }
        },
    }

    -- We won't add this margin to the FrameContainer: to be able
    -- to tweak it on some sides, we'll just tweak its overlap
    -- offsets and width to ensure the margins
    local tspan_margin = Size.margin.tiny
    local tspan_padding_h = Size.padding.tiny
    local tspan_height = self.span_height - 2 * (tspan_margin + self.toc_span_border)
    if self.toc_items then
        for lvl, items in pairs(self.toc_items) do
            local offset_y = self.pages_frame_border + self.span_height * (lvl - 1) + tspan_margin
            local prev_p_start, same_p_start_offset_dx
            for __, item in ipairs(items) do
                local text = item.title
                local p_start, p_end = item.p_start, item.p_end
                local started_before, continues_after = item.started_before, item.continues_after
                if _mirroredUI then
                    -- Just flip these (beware below, we need to use item.p_start to get
                    -- the real start page to account in prev_p_start)
                    p_start, p_end = p_end, p_start
                    started_before, continues_after = continues_after, started_before
                end
                local offset_x = self:getPageX(p_start)
                local width = self:getPageX(p_end, true) - offset_x
                offset_x = offset_x + self.pages_frame_border
                if prev_p_start == item.p_start then
                    -- Multiple TOC items starting on the same page slot:
                    -- shift and shorten 2nd++ ones so we see a bit of
                    -- the previous overwritten span and we can know this
                    -- page slot contains multiple chapters
                    if width > same_p_start_offset_dx then
                        if not _mirroredUI then
                            offset_x = offset_x + same_p_start_offset_dx
                        end
                        width = width - same_p_start_offset_dx
                        same_p_start_offset_dx = same_p_start_offset_dx + self.toc_span_border * 2
                    end
                else
                    prev_p_start = item.p_start
                    same_p_start_offset_dx = self.toc_span_border * 2
                end
                if started_before then
                    -- No left margin, have span border overlap with outer border
                    offset_x = offset_x - self.toc_span_border
                    width = width + self.toc_span_border
                else
                    -- Add some left margin
                    offset_x = offset_x + tspan_margin
                    width = width - tspan_margin
                end
                if continues_after then
                    -- No right margin, have span border overlap with outer border
                    width = width + self.toc_span_border
                else
                    -- Add some right margin
                    width = width - tspan_margin
                end
                local text_max_width = width - 2 * (self.toc_span_border + tspan_padding_h)
                local text_widget = nil
                if text_max_width > 0 then
                    text_widget = TextWidget:new{
                        text = BD.auto(text),
                        max_width = text_max_width,
                        face = self.font_face,
                        padding = 0,
                    }
                    if text_widget:getWidth() > text_max_width then
                        -- May happen with very small max_width when smaller
                        -- than the truncation ellipsis
                        text_widget:free()
                        text_widget = nil
                    end
                end
                local span_w = FrameContainer:new{
                    overlap_offset = {offset_x, offset_y},
                    margin = 0,
                    padding = 0,
                    bordersize = self.toc_span_border,
                    background = Blitbuffer.COLOR_WHITE,
                    CenterContainer:new{
                        dimen = Geom:new{
                            w = width - 2 * self.toc_span_border,
                            h = tspan_height,
                        },
                        text_widget or VerticalSpan:new{ width = 0 },
                    }
                }
                table.insert(self.pages_frame, span_w)
            end
        end
    end

    -- For page numbers:
    self.smaller_font_face = Font:getFace(self.font_face.orig_font, self.font_face.orig_size - 4)
    -- For current page triangle
    self.larger_font_face = Font:getFace(self.font_face.orig_font, self.font_face.orig_size + 6)

    self.hgroup = HorizontalGroup:new{
        align = "top",
    }

    if self.left_spacing > 0 then
        local spacing = Size.padding.small
        table.insert(self.hgroup, TextBoxWidget:new{
            text = self.start_page_text,
            width = self.left_spacing - spacing,
            face = self.smaller_font_face,
            line_height = 0, -- no additional line height
            alignment = _mirroredUI and "left" or "right",
            alignment_strict = true,
        })
        table.insert(self.hgroup, HorizontalSpan:new{ width = spacing })
    end
    table.insert(self.hgroup, self.pages_frame)

    -- Get hidden flows rectangle ready to be painted gray first as background
    self.background_fillers = {}
    if self.hidden_flows then
        for _, flow_edges in ipairs(self.hidden_flows) do
            local f_start, f_end = flow_edges[1], flow_edges[2]
            if f_start <= self.end_page and f_end >= self.start_page then
                local r_start = math.max(f_start, self.start_page)
                local r_end = math.min(f_end, self.end_page)
                local x, w
                if _mirroredUI then
                    x = self:getPageX(r_end)
                    w = self:getPageX(r_start, true) - x
                else
                    x = self:getPageX(r_start)
                    w = self:getPageX(r_end, true) - x
                end
                table.insert(self.background_fillers, {
                    x = x, y = 0,
                    w = w, h = self.pages_frame_height,
                    color = Blitbuffer.COLOR_LIGHT_GRAY,
                })
            end
        end
    end

    self[1] = LeftContainer:new{ -- needed only for auto UI mirroring
        dimen = Geom:new{
            w = self.dimen.w,
            h = self.hgroup:getSize().h,
        },
        self.hgroup,
    }

    -- Get read pages markers and other indicators ready to be drawn
    self.pages_markers = {}
    self.indicators = {}
    self.bottom_texts = {}
    local prev_page_was_read = true -- avoid one at start of row
    local unread_marker_h = math.ceil(self.span_height * 0.05)
    local read_min_h = math.max(math.ceil(self.span_height * 0.1), unread_marker_h+Size.line.thick)
    if self.page_slot_width >= 5 * unread_marker_h then
        -- If page slots are large enough, we can make unread markers a bit taller (so they
        -- are noticable and won't be confused with read page slots)
        unread_marker_h = unread_marker_h * 2
    end
    for page = self.start_page, self.end_page do
        if self.read_pages and self.read_pages[page] then
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x
            local h = math.ceil(self.read_pages[page][1] * self.span_height * 0.8)
            h = math.max(h, read_min_h) -- so it's noticable
            local y = self.pages_frame_height - self.pages_frame_border - h + 1
            if self.with_page_sep then
                -- We put the blank at the start of a page slot
                x = x + 1
                w = w - 1
                if w > 2 then
                    if page == self.end_page and not _mirroredUI then
                        w = w - 1 -- some spacing before right border (like we had at start)
                    end
                    if page == self.start_page and _mirroredUI then
                        w = w - 1
                    end
                end
            end
            local color = Blitbuffer.COLOR_BLACK
            if self.current_session_duration and self.read_pages[page][2] < self.current_session_duration then
                color = Blitbuffer.COLOR_GRAY_5
            end
            table.insert(self.pages_markers, {
                x = x, y = y,
                w = w, h = h,
                color = color,
            })
            prev_page_was_read = true
        else
            if self.with_page_sep and not prev_page_was_read then
                local w = Size.line.thin
                local x
                if _mirroredUI then
                    x = self:getPageX(page, true) - w
                else
                    x = self:getPageX(page)
                end
                local y = self.pages_frame_height - self.pages_frame_border - unread_marker_h + 1
                table.insert(self.pages_markers, {
                    x = x, y = y,
                    w = w, h = unread_marker_h,
                    color = Blitbuffer.COLOR_BLACK,
                })
            end
            prev_page_was_read = false
        end
        -- Indicator for bookmark/highlight type, and current page
        if self.bookmarked_pages[page] then
            local page_bookmark_types = self.bookmarked_pages[page]
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x
            x = x + math.ceil(w/2)
            local y = self.pages_frame_height + 1
            -- These 3 icons overlap quite ok, so no need for any shift
            if page_bookmark_types["highlight"] then
                table.insert(self.indicators, {
                    x = x, y = y,
                    c = 0x2592, -- medium shade
                })
            end
            if page_bookmark_types["note"] then
                table.insert(self.indicators, {
                    x = x, y = y,
                    c = 0xF040, -- pencil
                    rotation = -90,
                    shift_x_pct = 0.2, -- 20% looks a bit better
                    -- This glyph is a pencil pointing to the bottom left,
                    -- so we make it point to the top left and align it so
                    -- it points to the page slot it is associated with
                })
            end
            if page_bookmark_types["bookmark"] then
                table.insert(self.indicators, {
                    x = x, y = y,
                    c = 0xF097, -- empty bookmark
                })
            end
        end
        -- Indicator for previous locations
        if self.previous_locations[page] and page ~= self.cur_page then
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x
            x = x + math.ceil(w/2)
            local y = self.pages_frame_height + 1
            if self.bookmarked_pages[page] then
                -- Shift it a bit down to keep bookmark glyph(s) readable
                y = y + math.floor(self.span_height * (1/3))
            end
            local num = self.previous_locations[page]
            table.insert(self.indicators, {
                c = 0x2775 + (num < 10 and num or 10), -- number in solid black circle
                -- c = 0x245F + (num < 20 and num or 20), -- number in white circle
                x = x, y = y,
            })
        end
        -- Extra indicator
        if self.extra_symbols_pages and self.extra_symbols_pages[page] then
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x
            x = x + math.ceil(w/2)
            local y = self.pages_frame_height + 1
            if self.bookmarked_pages[page] then
                -- Shift it a bit down to keep bookmark glyph(s) readable
                y = y + math.floor(self.span_height * (1/3))
            end
            table.insert(self.indicators, {
                c = self.extra_symbols_pages[page],
                x = x, y = y,
            })
        end
        -- Current page indicator
        if page == self.cur_page then
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x
            x = x + math.ceil(w/2)
            local y = self.pages_frame_height + 1
            if self.bookmarked_pages[page] then
                -- Shift it a bit down to keep bookmark glyph(s) readable
                y = y + math.floor(self.span_height * (1/3))
            end
            table.insert(self.indicators, {
                c = 0x25B2, -- black up-pointing triangle
                x = x, y = y,
                face = self.larger_font_face,
            })
        end
        if self.page_texts and self.page_texts[page] then
            -- These have been put on pages free from any other indicator, so
            -- we can show the page number at the very bottom
            local x = self:getPageX(page)
            local w = self:getPageX(page, true) - x - Size.padding.tiny
            table.insert(self.bottom_texts, {
                text = self.page_texts[page].text,
                x = x,
                slot_width = w,
                block = self.page_texts[page].block,
                block_dx = self.page_texts[page].block_dx,
            })
        end
    end
end

function BookMapRow:paintTo(bb, x, y)
    -- Paint background fillers (which are not subwidgets) first
    for _, filler in ipairs(self.background_fillers) do
        bb:paintRect(x + self.pages_frame_offset_x + filler.x, y + filler.y, filler.w, filler.h, filler.color)
    end
    -- Paint regular sub widgets the classic way
    InputContainer.paintTo(self, bb, x, y)
    -- And explicitely paint read pages markers (which are not subwidgets)
    for _, marker in ipairs(self.pages_markers) do
        bb:paintRect(x + self.pages_frame_offset_x + marker.x, y + marker.y, marker.w, marker.h, marker.color)
    end
    -- And explicitely paint indicators (which are not subwidgets)
    for _, indicator in ipairs(self.indicators) do
        local glyph = RenderText:getGlyph(indicator.face or self.font_face, indicator.c)
        local alt_bb
        if indicator.rotation then
            alt_bb = glyph.bb:rotatedCopy(indicator.rotation)
        end
        -- Glyph's bb fit the blackbox of the glyph, so there's no cropping
        -- or complicated positionning to do
        -- By default, just center the glyph at x
        local d_x_pct = indicator.shift_x_pct or 0.5
        local d_x = math.floor(glyph.bb:getWidth() * d_x_pct)
        bb:colorblitFrom(
            alt_bb or glyph.bb,
            x + self.pages_frame_offset_x + indicator.x - d_x,
            y + indicator.y,
            0, 0,
            glyph.bb:getWidth(), glyph.bb:getHeight(),
            Blitbuffer.COLOR_BLACK)
        if alt_bb then
            alt_bb:free()
        end
    end
    -- And explicitely paint bottom texts (which are not subwidgets)
    for _, btext in ipairs(self.bottom_texts) do
        local text_w = TextWidget:new{
            text = btext.text,
            face = self.smaller_font_face,
            padding = 0,
        }
        local d_y = self.height - math.ceil(text_w:getSize().h)
        local d_x
        local text_width = text_w:getWidth()
        local d_width = btext.slot_width - text_width
        if not btext.block then
            -- no block constraint: can be centered
            d_x = math.ceil(d_width / 2)
        else
            if d_width >= 2 * btext.block_dx then
                -- small enough: can be centered
                d_x = math.ceil(d_width / 2)
            elseif btext.block == "left" then
                d_x = btext.block_dx
            else -- "right"
                d_x = d_width - btext.block_dx
            end
        end
        text_w:paintTo(bb, x + self.pages_frame_offset_x + btext.x + d_x, y + d_y)
        text_w:free()
    end
end

-- BookMapWidget: shows a map of content, including TOC, boomarks, read pages, non-linear flows...
local BookMapWidget = InputContainer:extend{
    title = _("Book map"),
    -- Focus page: show the BookMapRow containing this page
    -- in the middle of screen
    focus_page = nil,
    -- Should only be nil on the first launch via ReaderThumbnail
    launcher = nil,
    -- Extra symbols to show below pages
    extra_symbols_pages = nil,

    -- Make this local subwidget available for reuse by PageBrowser
    BookMapRow = BookMapRow,
}

function BookMapWidget:init()
    if self.ui.view:shouldInvertBiDiLayoutMirroring() then
        BD.invert()
    end

    -- Compute non-settings-dependant sizes and options
    self.dimen = Geom:new{
        x = 0,
        y = 0,
        w = Screen:getWidth(),
        h = Screen:getHeight(),
    }
    self.covers_fullscreen = true -- hint for UIManager:_repaint()

    if Device:hasKeys() then
        self.key_events = {
            Close = { { Input.group.Back } },
            ScrollRowUp = { { "Up" } },
            ScrollRowDown = { { "Down" } },
            ScrollPageUp = { { Input.group.PgBack } },
            ScrollPageDown = { { Input.group.PgFwd } },
        }
    end
    if Device:isTouchDevice() then
        self.ges_events = {
            Swipe = {
                GestureRange:new{
                    ges = "swipe",
                    range = self.dimen,
                }
            },
            MultiSwipe = {
                GestureRange:new{
                    ges = "multiswipe",
                    range = self.dimen,
                }
            },
            Tap = {
                GestureRange:new{
                    ges = "tap",
                    range = self.dimen,
                }
            },
            Pinch = {
                GestureRange:new{
                    ges = "pinch",
                    range = self.dimen,
                }
            },
            Spread = {
                GestureRange:new{
                    ges = "spread",
                    range = self.dimen,
                }
            },
        }
        -- No need for any long-press handler: page slots may be small and we can't
        -- really target a precise page slot with our fat finger above it...
        -- Tap will zoom the zone in a PageBrowserWidget where things will be clearer
        -- and allow us to get where we want.
        -- (Also, handling "hold" is a bit more complicated when we have our
        -- ScrollableContainer that would also like to handle it.)
    end

    -- No real need for any explicite edge and inter-row padding:
    -- we use the scrollbar width on both sides for balance (we may put a start
    -- page number on the left space), and each BookMapRow will have itself some
    -- blank space at bottom below page slots (where we may put hanging markers
    -- for current page and bookmark/highlights)
    self.scrollbar_width = ScrollableContainer:getScrollbarWidth()
    self.row_width = self.dimen.w - self.scrollbar_width
    self.row_left_spacing = self.scrollbar_width
    self.swipe_hint_bar_width = Screen:scaleBySize(6)

    self.title_bar = TitleBar:new{
        fullscreen = true,
        title = self.title,
        left_icon = "info",
        left_icon_tap_callback = function() self:showHelp() end,
        left_icon_hold_callback = function()
            self:toggleDefaultSettings() -- toggle between user settings and default view
        end,
        close_callback = function() self:onClose() end,
        close_hold_callback = function() self:onClose(true) end,
        show_parent = self,
    }
    self.title_bar_h = self.title_bar:getHeight()
    self.crop_height = self.dimen.h - self.title_bar_h - Size.margin.small - self.swipe_hint_bar_width

    -- Guess grid TOC span height from its font size
    -- (it feels this font size does not need to be configurable: too large and
    -- titles will be too easily truncated, too small and they will be unreadable)
    self.toc_span_font_name = "infofont"
    self.toc_span_font_size = 14
    self.toc_span_face = Font:getFace(self.toc_span_font_name, self.toc_span_font_size)
    local test_w = TextWidget:new{
        text = "z",
        face = self.toc_span_face,
    }
    self.span_height = test_w:getSize().h + BookMapRow.toc_span_border
    test_w:free()

    -- Reference font size for flat TOC items, as set (or default) in ReaderToc
    self.reader_toc_font_size = G_reader_settings:readSetting("toc_items_font_size")
            or Menu.getItemFontSize(G_reader_settings:readSetting("toc_items_per_page") or self.ui.toc.toc_items_per_page_default)

    -- Our container of stacked BookMapRows (and TOC titles in flat map mode)
    self.vgroup = VerticalGroup:new{
        align = "left",
    }
    -- We'll handle all events in this main BookMapWidget: none of the vgroup
    -- children have any handler. Hack into vgroup so it doesn't propagate
    -- events needlessly to its children (the slowness gets noticable when
    -- we have many TOC items in flat map mode - the also needless :paintTo()
    -- don't seen to cause such a noticable slowness)
    self.vgroup.propagateEvent = function() return false end

    -- Our scrollable container needs to be known as widget.cropping_widget in
    -- the widget that is passed to UIManager:show() for UIManager to ensure
    -- proper interception of inner widget self repainting/invert (mostly used
    -- when flashing for UI feedback that we want to limit to the cropped area).
    self.cropping_widget = ScrollableContainer:new{
        dimen = Geom:new{
            w = self.dimen.w,
            h = self.crop_height,
        },
        show_parent = self,
        ignore_events = {"swipe"},
        self.vgroup,
    }

    self[1] = FrameContainer:new{
        width = self.dimen.w,
        height = self.dimen.h,
        padding = 0,
        margin = 0,
        bordersize = 0,
        background = Blitbuffer.COLOR_WHITE,
        VerticalGroup:new{
            align = "center",
            self.title_bar,
            self.cropping_widget,
        }
    }

    -- Note: some of these could be cached in ReaderThumbnail, and discarded/updated
    -- on some events (ie. TocUpdated, PageUpdate, AddHhighlight...)
    -- Get some info that shouldn't change across calls to update()
    self.nb_pages = self.ui.document:getPageCount()
    self.ui.toc:fillToc()
    self.cur_page = self.ui.toc.pageno
    self.max_toc_depth = self.ui.toc.toc_depth
    -- Get bookmarks and highlights from ReaderBookmark
    self.bookmarked_pages = self.ui.bookmark:getBookmarkedPages()
    -- Get read page from the statistics plugin if enabled
    self.statistics_enabled = self.ui.statistics and self.ui.statistics:isEnabled()
    self.read_pages = self.ui.statistics and self.ui.statistics:getCurrentBookReadPages()
    self.current_session_duration = self.ui.statistics and (os.time() - self.ui.statistics.start_current_period)
    -- Hidden flows, for first page display, and to draw them gray
    self.has_hidden_flows = self.ui.document:hasHiddenFlows()
    if self.has_hidden_flows and #self.ui.document.flows > 0 then
        self.hidden_flows = {}
        -- Pick into credocument internal data to build a table
        -- of {first_page_number, last_page_number) for each flow
        for flow, tab in ipairs(self.ui.document.flows) do
            table.insert(self.hidden_flows, { tab[1], tab[1]+tab[2]-1 })
        end
    end
    -- Reference page numbers, for first row page display
    self.page_labels = nil
    if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
        self.page_labels = self.ui.document:getPageMap()
    end
    -- Location stack
    self.previous_locations = self.ui.link:getPreviousLocationPages()

    -- Compute settings-dependant sizes and options, and build the inner widgets
    self:update()
end

function BookMapWidget:update()
    if not self.focus_page then -- Initial display
        -- Focus (show at the middle of screen) on the BookMapRow that contains
        -- current page
        self.focus_page = self.cur_page
    else
        -- We have a previous focus page: if we have not scrolled around, keep
        -- focusing on this one. Otherwise, use the start_page of the BookMapRow
        -- at the middle of screen as the new focus page.
        if self.initial_scroll_offset_y ~= self.cropping_widget._scroll_offset_y then
            local h = math.min(self.vgroup:getSize().h, self.crop_height)
            local row = self:getBookMapRowNearY(h/2)
            if row then
                self.focus_page = row.start_page
            end
        end
    end

    -- Reset main widgets
    self.vgroup:clear()
    self.cropping_widget:reset()

    -- Flat book map has each TOC item on a new line, and pages graph underneath.
    -- Non-flat book map shows a grid with TOC items following each others.
    self.flat_map = self.ui.doc_settings:readSetting("book_map_flat", false)
    self.toc_depth = self.ui.doc_settings:readSetting("book_map_toc_depth", self.max_toc_depth)
    if self.flat_map then
        self.nb_toc_spans = 0 -- no span shown in grid
    else
        self.nb_toc_spans = self.toc_depth
    end

    self.flat_toc_depth_faces = nil
    if self.flat_map then
        self.flat_toc_depth_faces = {}
        -- Use ReaderToc setting font size for items at the lowest depth
        self.flat_toc_depth_faces[self.toc_depth] = Font:getFace(self.toc_span_font_name, self.reader_toc_font_size)
        for lvl=self.toc_depth-1, 1, -1 do
            -- But increase font size for each upper level
            local inc = 2 * (self.toc_depth - lvl)
            self.flat_toc_depth_faces[lvl] = Font:getFace(self.toc_span_font_name, self.reader_toc_font_size + inc)
        end
        -- Use 1.5em with the reference font size for indenting chapters and their BookMapRow
        self.flat_toc_level_indent = Screen:scaleBySize(self.reader_toc_font_size * 1.5)
    end

    -- Row will contain: nb_toc_spans + page slots + spacing (+ some borders)
    local page_slots_height_ratio = 1 -- default to 1 * span_height
    if not self.statistics_enabled then
        -- If statistics are disabled, we won't show black page slots for read pages.
        -- We can gain a bit of height by reducing the height reserved for these
        -- (don't go too low: we need some height to show the page number on the left).
        if self.flat_map or self.nb_toc_spans == 0 then
            -- Enough to show 4 digits page numbers
            page_slots_height_ratio = 0.7
        elseif self.nb_toc_spans > 0 then
            -- Just enough to show page separators below toc spans
            page_slots_height_ratio = 0.2
        end
    end
    self.row_height = math.ceil((self.nb_toc_spans + page_slots_height_ratio + 1) * self.span_height + 2*BookMapRow.pages_frame_border)

    if self.flat_map then
        -- Max pages per row, when each page slots takes 1px
        self.max_pages_per_row = math.floor(self.row_width - self.row_left_spacing - 2*Size.border.default
                                                - Size.span.horizontal_default*self.toc_depth)
        -- Find out the length of the largest chapter that we may show
        local len
        local max_len = 0
        local p = 1
        for _, item in ipairs(self.ui.toc.toc) do
            if item.depth <= self.toc_depth then
                len = item.page - p + 1
                if len > max_len then max_len = len end
                p = item.page
            end
        end
        len = self.nb_pages - p + 1 -- last chapter
        if len > max_len then max_len = len end
        self.fit_pages_per_row = max_len
    else
        -- Max pages per row, when each page slots takes 1px
        self.max_pages_per_row = math.floor(self.row_width - self.row_left_spacing - 2*Size.border.default)
        -- What can fit without scrollbar
        local fit_nb_rows = math.floor(self.crop_height / self.row_height)
        self.fit_pages_per_row = math.ceil(self.nb_pages / fit_nb_rows)
    end
    self.min_pages_per_row = 10
    -- If page slots are at least 4 pixels wide, we can steal one to act as a 1px blank separator
    self.max_pages_per_row_with_sep = math.floor(self.max_pages_per_row / 4)
    if self.fit_pages_per_row < self.min_pages_per_row then
        self.fit_pages_per_row = self.min_pages_per_row
    end
    if self.fit_pages_per_row > self.max_pages_per_row then
        self.fit_pages_per_row = self.max_pages_per_row
    end

    -- Show the whole book without scrollbar initially
    self.pages_per_row = self.ui.doc_settings:readSetting("book_map_pages_per_row", self.fit_pages_per_row)
    self.page_slot_width = nil -- will be fetched from the first BookMapRow

    -- Build BookMapRows as we walk the ToC
    local toc = self.ui.toc.toc
    local toc_idx = 1
    local cur_toc_items = {}
    local p_start = 1
    local cur_left_spacing = self.row_left_spacing -- updated when flat_map with previous TOC item indentation
    local cur_page_label_idx = 1
    while true do
        local p_max = p_start + self.pages_per_row - 1 -- max page number in this row
        local p_end = math.min(p_max, self.nb_pages) -- last book page in this row
        -- Find out the toc items that can be shown on this row
        local row_toc_items = {}
        while toc_idx <= #toc do
            local item = toc[toc_idx]
            if item.page > p_max then
                -- This TOC item will close previous items and start on the next row
                break
            end
            if item.depth <= self.toc_depth then -- ignore lower levels we won't show
                if self.flat_map then
                    if item.page == p_start then
                        cur_left_spacing = self.row_left_spacing + self.flat_toc_level_indent * (item.depth-1)
                        local txt_max_width = self.row_width - cur_left_spacing
                        table.insert(self.vgroup, HorizontalGroup:new{
                            HorizontalSpan:new{
                                width = cur_left_spacing,
                            },
                            TextBoxWidget:new{
                                text = self.ui.toc:cleanUpTocTitle(item.title, true),
                                width = txt_max_width,
                                face = self.flat_toc_depth_faces[item.depth],
                            },
                            -- Store this TOC item page, so we can tap on it to launch PageBrowser on its page
                            toc_item_page = item.page,
                        })
                        -- Add a bit more spacing for the BookMapRow(s) underneath this Toc item title
                        -- (so the page number painted in this spacing feels included in the indentation)
                        cur_left_spacing = cur_left_spacing + Size.span.horizontal_default
                        -- Note: this variable indentation may make the page slot widths variable across
                        -- rows from different levels (and self.fit_pages_per_row not really accurate) :/
                        -- Hopefully, it won't be noticable.
                    else
                        p_max = item.page - 1
                        p_end = p_max
                        -- Will be reprocessed on a new row
                        break
                    end
                else
                    -- An item at level N closes all previous items at level >= N
                    for lvl = item.depth, self.toc_depth do
                        local done_toc_item = cur_toc_items[lvl]
                        cur_toc_items[lvl] = nil
                        if done_toc_item then
                            done_toc_item.p_end = math.max(item.page - 1, done_toc_item.p_start)
                            if done_toc_item.p_end >= p_start then
                                -- Can go into row_toc_items[lvl]
                                if done_toc_item.p_start < p_start then
                                    done_toc_item.p_start = p_start
                                    done_toc_item.started_before = true -- no left margin
                                end
                                if not row_toc_items[lvl] then
                                    row_toc_items[lvl] = {}
                                end
                                -- We're done with it, we can just move it
                                table.insert(row_toc_items[lvl], done_toc_item)
                            end
                        end
                    end
                    cur_toc_items[item.depth] = {
                        title = item.title,
                        p_start = item.page,
                        p_end = nil,
                    }
                end
            end
            toc_idx = toc_idx + 1
        end
        local is_last_row = p_end == self.nb_pages
        -- We may have current toc_items that are active and may continue on next row
        -- Add a slightly adjusted copy of the current ones to row_toc_items
        for lvl = 1, self.nb_toc_spans do -- (no-op/no-loop if flat_map)
            local active_toc_item = cur_toc_items[lvl]
            if active_toc_item then
                local copied_toc_item = {}
                for k,v in next, active_toc_item, nil do copied_toc_item[k] = v end
                if copied_toc_item.p_start < p_start then
                    copied_toc_item.p_start = p_start
                    copied_toc_item.started_before = true -- no left margin
                end
                copied_toc_item.p_end = p_end
                copied_toc_item.continues_after = not is_last_row -- no right margin (except if last row)
                -- Look at next TOC item to see if it would close this one
                local coming_up_toc_item = toc[toc_idx]
                if coming_up_toc_item and coming_up_toc_item.page == p_max+1 and coming_up_toc_item.depth <= lvl then
                    copied_toc_item.continues_after = false -- right margin
                end
                if not row_toc_items[lvl] then
                    row_toc_items[lvl] = {}
                end
                table.insert(row_toc_items[lvl], copied_toc_item)
            end
        end

        -- Get the page number to display at start of row
        local start_page_text
        if self.page_labels then
            local label
            for idx=cur_page_label_idx, #self.page_labels do
                local item = self.page_labels[idx]
                if item.page > p_start then
                    break
                end
                label = item.label
                cur_page_label_idx = idx
            end
            if label then
                start_page_text = self.ui.pagemap:cleanPageLabel(label)
            end
        elseif self.has_hidden_flows then
            local flow = self.ui.document:getPageFlow(p_start)
            if flow == 0 then
                start_page_text = tostring(self.ui.document:getPageNumberInFlow(p_start))
            else
                -- start_page_text = string.format("[%d]%d", self.ui.document:getPageNumberInFlow(p_start), self.ui.document:getPageFlow(p_start))
                -- start_page_text = string.format("/%d\\", self.ui.document:getPageFlow(p_start))
                -- Just don't display anything
                start_page_text = nil
            end
        else
            start_page_text = tostring(p_start)
        end
        if start_page_text then
            start_page_text = table.concat(util.splitToChars(start_page_text), "\n")
        else
            start_page_text = ""
        end

        local row = BookMapRow:new{
            height = self.row_height,
            width = self.row_width,
            show_parent = self,
            left_spacing = cur_left_spacing,
            nb_toc_spans = self.nb_toc_spans,
            span_height = self.span_height,
            font_face = self.toc_span_face,
            start_page_text = start_page_text,
            start_page = p_start,
            end_page = p_end,
            pages_per_row = self.pages_per_row,
            cur_page = self.cur_page,
            with_page_sep = self.pages_per_row < self.max_pages_per_row_with_sep,
            toc_items = row_toc_items,
            bookmarked_pages = self.bookmarked_pages,
            previous_locations = self.previous_locations,
            extra_symbols_pages = self.extra_symbols_pages,
            hidden_flows = self.hidden_flows,
            read_pages = self.read_pages,
            current_session_duration = self.current_session_duration,
        }
        table.insert(self.vgroup, row)
        if not self.page_slot_width then
            self.page_slot_width = row.page_slot_width
        end
        if is_last_row then
            break
        end
        p_start = p_max + 1
    end

    -- Have main VerticalGroup size and subwidgets' offsets computed
    self.vgroup:getSize()

    -- Scroll so we get the focus page at the middle of screen
    local row, row_idx, row_y, row_h = self:getMatchingVGroupRow(function(r, r_y, r_h) -- luacheck: no unused
        return r.start_page and self.focus_page >= r.start_page and self.focus_page <= r.end_page
    end)
    if row_y then
        local top_y = row_y + row_h/2 - self.crop_height/2
        -- Align it so that we don't see any truncated BookMapRow at top
        row, row_idx, row_y, row_h = self:getMatchingVGroupRow(function(r, r_y, r_h)
            return r_y < top_y and r_y + r_h > top_y
        end)
        if row then
            if top_y - row_y > row_y + row_h - top_y then
                -- Less adjustment if we scroll to align the next row
                top_y = row_y + row_h
            else
                top_y = row_y
            end
        end
        if top_y > 0 then
            self.cropping_widget:initState() -- anticipate this (otherwise delayed and done at :paintTo() time)
            if self.cropping_widget._is_scrollable then
                self.cropping_widget:_scrollBy(0, top_y)
            end
        end
    end
    self.initial_scroll_offset_y = self.cropping_widget._scroll_offset_y

    UIManager:setDirty(self, function()
        return "ui", self.dimen
    end)
end


function BookMapWidget:showHelp()
    UIManager:show(InfoMessage:new{
        text = _([[
Book map displays an overview of the book content.

If statistics are enabled, black bars are shown for already read pages (gray for pages read in the current reading session). Their heights vary with the time spent reading the page.
Chapters are shown above their pages.
Under the pages can be found some indicators:
▲ current page
❶ ❷ … previous locations
▒ highlighted text
 highlighted text with notes
 bookmarked page
▢ focused page when coming from Pages browser

Tap on a location in the book to browse thumbnails of the pages there.
Swipe along the left screen edge to change the level of chapters to include in the book map, and the type of book map (grid or flat) when crossing the level 0.
Swipe along the bottom screen edge to change the width of page slots.
Swipe or pan vertically on content to scroll.
Any multiswipe will close the book map.

On a newly opened book, the book map will start in grid mode showing all chapter levels, fitting on a single screen, to give the best initial overview of the book's content.
Long-press on ⓘ to switch between current and initial views.]]),
    })
end

function BookMapWidget:onClose(close_all_parents)
    -- Close this widget
    logger.dbg("closing BookMapWidget")
    UIManager:close(self)
    if self.launcher then
        -- We were launched by a PageBrowserWidget, don't do any cleanup.
        if close_all_parents then
            -- The last one of these (which has no launcher attribute)
            -- will do the cleanup below.
            self.launcher:onClose(true)
        else
            UIManager:setDirty(self.launcher, "ui")
        end
    else
        BD.resetInvert()
        -- Remove all thumbnails generated for a different target size than
        -- the last one used (no need to keep old sizes if the user played
        -- with nb_cols/nb_rows, as on next opening, we just need the ones
        -- with the current size to be available)
        self.ui.thumbnail:tidyCache()
        -- Force a GC to free the memory used by the widgets and tiles
        -- (delay it a bit so this pause is less noticable)
        UIManager:scheduleIn(0.5, function()
            collectgarbage()
            collectgarbage()
        end)
        -- As we're getting back to Reader, do a full flashing refresh to remove
        -- any ghost trace of thumbnails or black page slots
        UIManager:setDirty(self.ui.dialog, "full")
    end
    return true
end

function BookMapWidget:getMatchingVGroupRow(check_func)
    -- Generic Vertical subwidget search function.
    -- We use some of VerticalGroup's internal data, no need
    -- to keep public copies of these data in here
    for i=1, #self.vgroup do
        local row = self.vgroup[i]
        local y = self.vgroup._offsets[i].y
        local h = (i < #self.vgroup and self.vgroup._offsets[i+1].y or self.vgroup._size.h) - y
        if check_func(row, y, h) then
            return row, i, y, h
        end
    end
end

function BookMapWidget:getVGroupRowAtY(y)
    -- y is expected relative to the ScrollableContainer crop top
    -- (if y is from a screen coordinate, substract 'self.title_bar_h' before calling this)
    y = y + self.cropping_widget._scroll_offset_y
    return self:getMatchingVGroupRow(function(r, r_y, r_h)
        return y >= r_y and y < r_y + r_h
    end)
end

function BookMapWidget:getBookMapRowNearY(y)
    -- y is expected relative to the ScrollableContainer crop top
    -- (if y is from a screen coordinate, substract 'self.title_bar_h' before calling this)
    y = y + self.cropping_widget._scroll_offset_y
    -- Return the BookMapRow at y, or if the vgroup element is a ToC
    -- title (in flat_map mode), return the follow up BookMapRow
    return self:getMatchingVGroupRow(function(r, r_y, r_h)
        return y < r_y + r_h and r.start_page
    end)
end

function BookMapWidget:onScrollPageUp()
    -- Show previous content, ensuring any truncated widget at top is now full at bottom
    local scroll_offset_y = self.cropping_widget._scroll_offset_y
    local row, row_idx, row_y, row_h = self:getVGroupRowAtY(-1) -- luacheck: no unused
    local to_keep = 0
    if row then
        to_keep = row_h - (scroll_offset_y - row_y)
    end
    self.cropping_widget:_scrollBy(0, -(self.crop_height - to_keep))
    return true
end

function BookMapWidget:onScrollPageDown()
    -- Show next content, ensuring any truncated widget at bottom is now full at top
    local scroll_offset_y = self.cropping_widget._scroll_offset_y
    local row, row_idx, row_y, row_h = self:getVGroupRowAtY(self.crop_height) -- luacheck: no unused
    if row then
        self.cropping_widget:_scrollBy(0, row_y - scroll_offset_y)
    else
        self.cropping_widget:_scrollBy(0, self.crop_height)
    end
    return true
end

function BookMapWidget:onScrollRowUp()
    local scroll_offset_y = self.cropping_widget._scroll_offset_y
    local row, row_idx, row_y, row_h = self:getVGroupRowAtY(-1) -- luacheck: no unused
    if row then
        self.cropping_widget:_scrollBy(0, row_y - scroll_offset_y)
    end
    return true
end

function BookMapWidget:onScrollRowDown()
    local scroll_offset_y = self.cropping_widget._scroll_offset_y
    local row, row_idx, row_y, row_h = self:getVGroupRowAtY(0) -- luacheck: no unused
    if row then
        self.cropping_widget:_scrollBy(0, row_y + row_h - scroll_offset_y)
    end
    return true
end

function BookMapWidget:saveSettings(reset)
    if reset then
        self.flat_map = nil
        self.toc_depth = nil
        self.pages_per_row = nil
    end
    self.ui.doc_settings:saveSetting("book_map_flat", self.flat_map)
    self.ui.doc_settings:saveSetting("book_map_toc_depth", self.toc_depth)
    self.ui.doc_settings:saveSetting("book_map_pages_per_row", self.pages_per_row)
end

function BookMapWidget:toggleDefaultSettings()
    if not self.flat_map and self.toc_depth == self.max_toc_depth
            and self.pages_per_row == self.fit_pages_per_row then
        -- Still in default/initial view: restore previous settings (if any)
        self.flat_map = self.ui.doc_settings:readSetting("book_map_previous_flat")
        self.toc_depth = self.ui.doc_settings:readSetting("book_map_previous_toc_depth")
        self.pages_per_row = self.ui.doc_settings:readSetting("book_map_previous_pages_per_row")
        self:saveSettings()
    else
        -- Save previous settings and switch to defaults
        self.ui.doc_settings:saveSetting("book_map_previous_flat", self.flat_map)
        self.ui.doc_settings:saveSetting("book_map_previous_toc_depth", self.toc_depth)
        self.ui.doc_settings:saveSetting("book_map_previous_pages_per_row", self.pages_per_row)
        self:saveSettings(true)
    end
    self:update()
end

function BookMapWidget:updateTocDepth(depth, flat)
    -- if flat == nil, consider value relative, and allow toggling
    -- flatness when crossing 0
    local new_toc_depth = self.toc_depth
    local new_flat_map = self.flat_map
    if flat == nil then
        if self.flat_map then
            -- Reverse increment if flat_map
            new_toc_depth = new_toc_depth - depth
        else
            new_toc_depth = new_toc_depth + depth
        end
        if new_toc_depth < 0 then
            new_toc_depth = - new_toc_depth
            new_flat_map = not new_flat_map
        end
    else
        new_toc_depth = depth
        new_flat_map = flat
    end
    if new_toc_depth < 0 then
        new_toc_depth = 0
    end
    if new_toc_depth > self.max_toc_depth then
        new_toc_depth = self.max_toc_depth
    end
    if new_toc_depth == self.toc_depth and new_flat_map == self.flat_map then
        return false
    end
    self.toc_depth = new_toc_depth
    self.flat_map = new_flat_map
    self:saveSettings()
    return true
end

function BookMapWidget:updatePagesPerRow(value, relative)
    local new_pages_per_row
    if relative then
        new_pages_per_row = self.pages_per_row + value
    else
        new_pages_per_row = value
    end
    if new_pages_per_row < self.min_pages_per_row then
        new_pages_per_row = self.min_pages_per_row
    end
    if new_pages_per_row > self.max_pages_per_row then
        new_pages_per_row = self.max_pages_per_row
    end
    if new_pages_per_row == self.pages_per_row then
        return false
    end
    self.pages_per_row = new_pages_per_row
    self:saveSettings()
    return true
end

function BookMapWidget:onSwipe(arg, ges)
    local _mirroredUI = BD.mirroredUILayout()
    local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
    if (not _mirroredUI and ges.pos.x < Screen:getWidth() * 1/8) or
           (_mirroredUI and ges.pos.x > Screen:getWidth() * 7/8) then
        -- Swipe along the left screen edge: increase/decrease toc levels shown
        if direction == "north" or direction == "south" then
            local rel = direction == "south" and 1 or -1
            if self:updateTocDepth(rel, nil) then
                self:update()
            end
            return true
        end
    end
    if ges.pos.y > Screen:getHeight() * 7/8 then
        -- Swipe along the bottom screen edge: increase/decrease pages per row
        if direction == "west" or direction == "east" then
            -- Have a swipe distance 0.8 x screen width do *2 or *1/2
            local ratio = ges.distance / Screen:getWidth()
            local new_pages_per_row
            if direction == "west" then -- increase pages per row
                new_pages_per_row = math.ceil(self.pages_per_row * (1 + ratio))
            else
                new_pages_per_row = math.floor(self.pages_per_row / (1 + ratio))
            end
            -- If we are crossing the ideal fit_pages_per_row, stop on it
            if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
                    or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
                new_pages_per_row = self.fit_pages_per_row
            end
            if self:updatePagesPerRow(new_pages_per_row) then
                self:update()
            end
            return true
        end
    end
    -- Let our MovableContainer handle other swipes:
    -- return self.cropping_widget:onScrollableSwipe(arg, ges)
    -- No, we prefer not to, and have swipe north/south do full prev/next page
    -- rather than based on the swipe distance
    if direction == "north" then
        return self:onScrollPageDown()
    elseif direction == "south" then
        return self:onScrollPageUp()
    elseif direction == "west" or direction == "east" then
        return true
    else -- diagonal swipe
        -- trigger full refresh
        UIManager:setDirty(nil, "full")
        -- a long diagonal swipe may also be used for taking a screenshot,
        -- so let it propagate
        return false
    end
end

function BookMapWidget:onPinch(arg, ges)
    local updated = false
    if ges.direction == "horizontal" or ges.direction == "diagonal" then
        local new_pages_per_row = math.ceil(self.pages_per_row * 1.5)
        if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
                or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
            new_pages_per_row = self.fit_pages_per_row
        end
        if self:updatePagesPerRow(new_pages_per_row) then
            updated = true
        end
    end
    if ges.direction == "vertical" or ges.direction == "diagonal" then
        -- Keep current flat map mode
        if self:updateTocDepth(self.toc_depth-1, self.flat_map) then
            updated = true
        else
            -- Already at 0: toggle flat mode, stay at 0 (no visual feedback though...)
            if self:updateTocDepth(0, not self.flat_map) then
                updated = true
            end
        end
    end
    if updated then
        self:update()
    end
    return true
end

function BookMapWidget:onSpread(arg, ges)
    local updated = false
    if ges.direction == "horizontal" or ges.direction == "diagonal" then
        local new_pages_per_row = math.floor(self.pages_per_row * (2/3))
        if (self.pages_per_row < self.fit_pages_per_row and new_pages_per_row > self.fit_pages_per_row)
                or (self.pages_per_row > self.fit_pages_per_row and new_pages_per_row < self.fit_pages_per_row) then
            new_pages_per_row = self.fit_pages_per_row
        end
        if self:updatePagesPerRow(new_pages_per_row) then
            updated = true
        end
    end
    if ges.direction == "vertical" or ges.direction == "diagonal" then
        if self:updateTocDepth(self.toc_depth+1, self.flat_map) then
            updated = true
        end
    end
    if updated then
        self:update()
    end
    return true
end

function BookMapWidget:onMultiSwipe(arg, ges)
    -- Swipe south (the usual shortcut for closing a full screen window)
    -- is used for navigation. Swipe left/right are free, but a little
    -- unusual for the purpose of closing.
    -- So, allow for quick closing with any multiswipe.
    self:onClose()
    return true
end

function BookMapWidget:onTap(arg, ges)
    if ges.pos:notIntersectWith(self.cropping_widget.dimen) then
        return true
    end
    local x, y = ges.pos.x, ges.pos.y
    local row, row_idx, row_y, row_h = self:getVGroupRowAtY(y-self.title_bar_h) -- luacheck: no unused
    if not row then
        return true
    end
    local page = row.toc_item_page      -- it might be a TOC title
    if not page and row.start_page then -- or a BookMapRow
        if BD.mirroredUILayout() then
            x = x - self.scrollbar_width
        end
        page = row:getPageAtX(x, true)
    end
    if page then
        local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
        UIManager:show(PageBrowserWidget:new{
            launcher = self,
            ui = self.ui,
            focus_page = page,
        })
    end
    return true
end

function BookMapWidget:paintTo(bb, x, y)
    -- Paint regular sub widgets the classic way
    InputContainer.paintTo(self, bb, x, y)
    -- And explicitely paint "swipe" hints along the left and bottom borders
    self:paintLeftVerticalSwipeHint(bb, x, y)
    self:paintBottomHorizontalSwipeHint(bb, x, y)
end

function BookMapWidget:paintLeftVerticalSwipeHint(bb, x, y)
    -- Vertical bar with a part of it darker, as a scale showing
    -- selected flat_map and toc_depth. In gray so it's visible
    -- when you look at it, but not distracting when you don't.
    local v = self.vs_hint_info
    if not v then
        -- Compute and remember sizes, positions and info
        v = {}
        v.width = self.swipe_hint_bar_width
        if BD.mirroredUILayout() then
            v.left = Screen:getWidth() - v.width
        else
            v.left = 0
        end
        v.top = self.title_bar_h + math.floor(self.crop_height * 1/6)
        v.height = math.floor(self.crop_height * 4/6)
        v.nb_units = self.max_toc_depth * 2 + 1
        v.unit_h = math.floor(v.height / v.nb_units)
        self.vs_hint_info = v
    end
    -- Paint a vertical light gray bar
    bb:paintRect(v.left, v.top, v.width, v.height, Blitbuffer.COLOR_LIGHT_GRAY)
    -- And paint a part of it in a darker gray
    local unit_idx -- starts from 0
    if self.flat_map then -- upper part of the vertical bar
        unit_idx = self.max_toc_depth - self.toc_depth
    else -- lower part of the vertical bar
        unit_idx = self.max_toc_depth + self.toc_depth
    end
    local dy = unit_idx * v.unit_h
    if unit_idx == v.nb_units - 1 then
        -- avoid possible rounding error for last unit
        dy = v.height - v.unit_h
    end
    bb:paintRect(v.left, v.top + dy, v.width, v.unit_h, Blitbuffer.COLOR_DARK_GRAY)
end

function BookMapWidget:paintBottomHorizontalSwipeHint(bb, x, y)
    -- Horizontal bar with a part of it darker, as a scale showing
    -- selected pages_per_row.
    local h = self.hs_hint_info
    if not h then
        -- Compute and remember sizes, positions and info
        h = {}
        h.height = self.swipe_hint_bar_width
        h.top = Screen:getHeight() - h.height
        h.width = math.floor(Screen:getWidth() * 4/6)
        h.left = math.floor(Screen:getWidth() * 1/6)
        -- We show a fixed width handle with a granular dx
        h.hint_w = math.floor(h.width / 8)
        h.max_dx = h.width - h.hint_w
        self.hs_hint_info = h
    end
    -- Paint a horizontal light gray bar
    bb:paintRect(h.left, h.top, h.width, h.height, Blitbuffer.COLOR_LIGHT_GRAY)
    -- And paint a part of it in a darker gray
    -- (Somebody good at maths could probably do better than this... which
    -- could be related to the increment/ratio we use in onSwipe)
    local cur = self.pages_per_row - self.min_pages_per_row
    local max = self.max_pages_per_row - self.min_pages_per_row
    local dx = math.floor(h.max_dx*(1-math.log(1+cur)/math.log(1+max)))
    if BD.mirroredUILayout() then
        dx = h.max_dx - dx
    end
    bb:paintRect(h.left + dx, h.top, h.hint_w, h.height, Blitbuffer.COLOR_DARK_GRAY)
end

return BookMapWidget
