--[[--
Widget that displays a shortcut icon for menu item.
--]]

local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
local Button = require("ui/widget/button")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local FFIUtil = require("ffi/util")
local FocusManager = require("ui/widget/focusmanager")
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 Math = require("optmath")
local OverlapGroup = require("ui/widget/overlapgroup")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local UnderlineContainer = require("ui/widget/container/underlinecontainer")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen

local ItemShortCutIcon = WidgetContainer:new{
    dimen = Geom:new{ w = Screen:scaleBySize(22), h = Screen:scaleBySize(22) },
    key = nil,
    bordersize = Size.border.default,
    radius = 0,
    style = "square",
}

function ItemShortCutIcon:init()
    if not self.key then
        return
    end

    local radius = 0
    local background = Blitbuffer.COLOR_WHITE
    if self.style == "rounded_corner" then
        radius = math.floor(self.width/2)
    elseif self.style == "grey_square" then
        background = Blitbuffer.COLOR_LIGHT_GRAY
    end

    --- @todo Calculate font size by icon size  01.05 2012 (houqp).
    local sc_face
    if self.key:len() > 1 then
        sc_face = Font:getFace("ffont", 14)
    else
        sc_face = Font:getFace("scfont", 22)
    end

    self[1] = FrameContainer:new{
        padding = 0,
        bordersize = self.bordersize,
        radius = radius,
        background = background,
        dimen = self.dimen,
        CenterContainer:new{
            dimen = self.dimen,
            TextWidget:new{
                text = self.key,
                face = sc_face,
            },
        },
    }
end

--[[
NOTICE:
@menu entry must be provided in order to close the menu
--]]
local MenuCloseButton = InputContainer:new{
    overlap_align = "right",
    padding_right = 0,
    menu = nil,
    dimen = Geom:new{},
}

function MenuCloseButton:init()
    local text_widget = TextWidget:new{
        text = "×",
        face = Font:getFace("cfont", 30), -- this font size align nicely with title
    }
    -- The text box height is greater than its width, and we want this × to be
    -- diagonally aligned with the top right corner (assuming padding_right=0,
    -- or padding_right = padding_top so the diagonal aligment is preserved).
    local text_size = text_widget:getSize()
    local text_width_pad = (text_size.h - text_size.w) / 2

    self[1] = FrameContainer:new{
        bordersize = 0,
        padding = 0,
        padding_top = self.padding_top,
        padding_bottom = self.padding_bottom,
        padding_left = self.padding_left,
        padding_right = self.padding_right + text_width_pad,
        text_widget,
    }

    self.dimen = Geom:new{
        w = text_size.w + text_width_pad + self.padding_right,
        h = text_size.h,
    }
    self.ges_events.Close = {
        GestureRange:new{
            ges = "tap",
            range = self.dimen,
        },
        doc = "Close menu",
    }
end

function MenuCloseButton:onClose()
    self.menu:onClose()
    return true
end

--[[
Widget that displays an item for menu
--]]
local MenuItem = InputContainer:new{
    text = nil,
    bidi_wrap_func = nil,
    show_parent = nil,
    detail = nil,
    font = "cfont",
    font_size = 24,
    infont = "infont",
    infont_size = 18,
    dimen = nil,
    shortcut = nil,
    shortcut_style = "square",
    _underline_container = nil,
    linesize = Size.line.medium,
    single_line = false,
    -- Align text & mandatory baselines (only when single_line=true)
    align_baselines = false,
}

function MenuItem:init()
    self.content_width = self.dimen.w - 2 * Size.padding.fullscreen
    local shortcut_icon_dimen = Geom:new()
    if self.shortcut then
        shortcut_icon_dimen.w = math.floor(self.dimen.h*4/5)
        shortcut_icon_dimen.h = shortcut_icon_dimen.w
        self.content_width = self.content_width - shortcut_icon_dimen.w - Size.span.horizontal_default
    end

    self.detail = self.text

    -- we need this table per-instance, so we declare it here
    if Device:isTouchDevice() then
        self.ges_events = {
            TapSelect = {
                GestureRange:new{
                    ges = "tap",
                    range = self.dimen,
                },
                doc = "Select Menu Item",
            },
            HoldSelect = {
                GestureRange:new{
                    ges = "hold",
                    range = self.dimen,
                },
                doc = "Hold Menu Item",
            },
        }
    end

    local max_item_height = self.dimen.h - 2 * self.linesize

    -- We want to show at least one line, so cap the provided font sizes
    local max_font_size = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 1)
    if self.font_size > max_font_size then
        self.font_size = max_font_size
    end
    if self.infont_size > max_font_size then
        self.infont_size = max_font_size
    end
    local multilines_show_more_text = G_reader_settings:isTrue("items_multilines_show_more_text")
    if not self.single_line and not multilines_show_more_text then
        -- For non single line menus (File browser, Bookmarks), if the
        -- user provided font size is large and would not allow showing
        -- more than one line in our item height, just switch to single
        -- line mode. This allows, when truncating, to take the full
        -- width and cut inside a word to add the ellipsis - while in
        -- multilines modes, with TextBoxWidget, words are wrapped to
        -- follow line breaking rules, and the ellipsis might be placed
        -- way earlier than the full width.
        local min_font_size_2_lines = TextBoxWidget:getFontSizeToFitHeight(max_item_height, 2)
        if self.font_size > min_font_size_2_lines then
            self.single_line = true
        end
    end

    -- State button and indentation for tree expand/collapse (for TOC)
    local state_button_width = self.state_size.w or 0
    local state_button = self.state or HorizontalSpan:new{
        width = state_button_width,
    }
    local state_indent = self.state and self.state.indent or ""
    local state_container = LeftContainer:new{
        dimen = Geom:new{w = self.content_width/2, h = self.dimen.h},
        HorizontalGroup:new{
            TextWidget:new{
                text = state_indent,
                face = Font:getFace(self.font, self.font_size),
            },
            state_button,
        }
    }

    -- Font for main text (may have its size decreased to make text fit)
    self.face = Font:getFace(self.font, self.font_size)
    -- Font for "mandatory" on the right
    self.info_face = Font:getFace(self.infont, self.infont_size)

    -- "mandatory" is the text on the right: file size, page number...
    -- Padding before mandatory
    local text_mandatory_padding = 0
    local text_ellipsis_mandatory_padding = 0
    local mandatory = self.mandatory_func and self.mandatory_func() or self.mandatory
    if mandatory then
        text_mandatory_padding = Size.span.horizontal_default
        -- Smaller padding when ellipsis for better visual feeling
        text_ellipsis_mandatory_padding = Size.span.horizontal_small
    end
    mandatory = mandatory and ""..mandatory or ""
    local mandatory_widget = TextWidget:new{
        text = mandatory,
        face = self.info_face,
        bold = self.bold,
        fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
    }
    local mandatory_w = mandatory_widget:getWidth()

    local available_width = self.content_width - state_button_width - text_mandatory_padding - mandatory_w
    local item_name

    -- Whether we show text on a single or multiple lines, we don't want it shortened
    -- because of some \n that would push the following text on another line that would
    -- overflow and not be displayed, or show a tofu char when displayed by TextWidget:
    -- get rid of any \n (which could be found in highlighted text in bookmarks).
    local text = self.text:gsub("\n", " ")

    -- Wrap text with provided bidi_wrap_func (only provided by FileChooser,
    -- to correctly display filenames and directories)
    if self.bidi_wrap_func then
        text = self.bidi_wrap_func(text)
    end

    if self.single_line then  -- items only in single line
        -- No font size change: text will be truncated if it overflows
        item_name = TextWidget:new{
            text = text,
            face = self.face,
            bold = self.bold,
            fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
        }
        local w = item_name:getWidth()
        if w > available_width then
            -- We give it a little more room if truncated for better visual
            -- feeling (which might make it no more truncated, but well...)
            local text_max_width_if_ellipsis = available_width + text_mandatory_padding - text_ellipsis_mandatory_padding
            item_name:setMaxWidth(text_max_width_if_ellipsis)
        end
        if self.align_baselines then -- Align baselines of text and mandatory
            local name_baseline = item_name:getBaseline()
            local mandatory_baseline = mandatory_widget:getBaseline()
            local baselines_diff = Math.round(name_baseline - mandatory_baseline)
            if baselines_diff > 0 then
                mandatory_widget = VerticalGroup:new{
                    VerticalSpan:new{width = baselines_diff},
                    mandatory_widget,
                }
            elseif baselines_diff < 0 then
                item_name = VerticalGroup:new{
                    VerticalSpan:new{width = -baselines_diff},
                    item_name,
                }
            end
        end

    elseif multilines_show_more_text then
        -- Multi-lines, with font size decrease if needed to show more of the text.
        -- It would be costly/slow with use_xtext if we were to try all
        -- font sizes from self.font_size to min_font_size (12).
        -- So, we try to optimize the search of the best font size.
        logger.dbg("multilines_show_more_text menu item font sizing start")
        local function make_item_name(font_size)
            if item_name then
                item_name:free()
            end
            logger.dbg("multilines_show_more_text trying font size", font_size)
            item_name = TextBoxWidget:new {
                text = text,
                face = Font:getFace(self.font, font_size),
                width = available_width,
                alignment = "left",
                bold = self.bold,
                fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
            }
            -- return true if we fit
            return item_name:getSize().h <= max_item_height
        end
        local min_font_size = 12
        -- First, try with specified font size: short text might fit
        if not make_item_name(self.font_size) then
            -- It doesn't, try with min font size: very long text might not fit
            if not make_item_name(min_font_size) then
                -- Does not fit with min font size: keep widget with min_font_size, but
                -- impose a max height to show only the first lines up to where it fits
                item_name:free()
                item_name.height = max_item_height
                item_name.height_adjust = true
                item_name.height_overflow_show_ellipsis = true
                item_name:init()
            else
                -- Text fits with min font size: try to find some larger
                -- font size in between that make text fit, with some
                -- binary search to limit the number of checks.
                local bad_font_size = self.font_size
                local good_font_size = min_font_size
                local item_name_is_good = true
                while true do
                    local test_font_size = math.floor((good_font_size + bad_font_size) / 2)
                    if test_font_size == good_font_size then -- +1 would be bad_font_size
                        if not item_name_is_good then
                            make_item_name(good_font_size)
                        end
                        break
                    end
                    if make_item_name(test_font_size) then
                        good_font_size = test_font_size
                        item_name_is_good = true
                    else
                        bad_font_size = test_font_size
                        item_name_is_good = false
                    end
                end
            end
        end
    else
        -- Multi-lines, with fixed user provided font size
        item_name = TextBoxWidget:new {
            text = text,
            face = self.face,
            width = available_width,
            height = max_item_height,
            height_adjust = true,
            height_overflow_show_ellipsis = true,
            alignment = "left",
            bold = self.bold,
            fgcolor = self.dim and Blitbuffer.COLOR_DARK_GRAY or nil,
        }
    end

    local text_container = LeftContainer:new{
        dimen = Geom:new{w = self.content_width, h = self.dimen.h},
        HorizontalGroup:new{
            HorizontalSpan:new{
                width = self.state_size.w,
            },
            item_name,
        }
    }

    local mandatory_container = RightContainer:new{
        dimen = Geom:new{w = self.content_width, h = self.dimen.h},
        mandatory_widget,
    }

    self._underline_container = UnderlineContainer:new{
        color = self.line_color,
        linesize = self.linesize,
        vertical_align = "center",
        padding = 0,
        dimen = Geom:new{
            w = self.content_width,
            h = self.dimen.h
        },
        HorizontalGroup:new{
            align = "center",
            OverlapGroup:new{
                dimen = Geom:new{w = self.content_width, h = self.dimen.h},
                state_container,
                text_container,
                mandatory_container,
            },
        }
    }
    local hgroup = HorizontalGroup:new{
        align = "center",
        HorizontalSpan:new{ width = Size.padding.fullscreen },
    }
    if self.shortcut then
        table.insert(hgroup, ItemShortCutIcon:new{
            dimen = shortcut_icon_dimen,
            key = self.shortcut,
            style = self.shortcut_style,
        })
        table.insert(hgroup, HorizontalSpan:new{ width = Size.span.horizontal_default })
    end
    table.insert(hgroup, self._underline_container)
    table.insert(hgroup, HorizontalSpan:new{ width = Size.padding.fullscreen })

    self[1] = FrameContainer:new{
        bordersize = 0,
        padding = 0,
        hgroup,
    }
end

function MenuItem:onFocus(initial_focus)
    if Device:isTouchDevice() then
        -- Devices which are Keys capable will get this onFocus called by
        -- updateItems(), which will toggle the underline color of first item.
        -- If the device is also Touch capable, let's not show the initial
        -- underline for a prettier display (it will be shown only when keys
        -- are used).
        if not initial_focus or self.menu.did_focus_with_keys then
            self._underline_container.color = Blitbuffer.COLOR_BLACK
            self.menu.did_focus_with_keys = true
        end
    else
        self._underline_container.color = Blitbuffer.COLOR_BLACK
    end
    return true
end

function MenuItem:onUnfocus()
    self._underline_container.color = self.line_color
    return true
end

function MenuItem:onShowItemDetail()
    UIManager:show(InfoMessage:new{ text = self.detail, })
    return true
end

function MenuItem:getGesPosition(ges)
    local dimen = self[1].dimen
    return {
        x = (ges.pos.x - dimen.x)/dimen.w,
        y = (ges.pos.y - dimen.y)/dimen.h,
    }
end

function MenuItem:onTapSelect(arg, ges)
    local pos = self:getGesPosition(ges)
    if G_reader_settings:isFalse("flash_ui") then
        logger.dbg("creating coroutine for menu select")
        local co = coroutine.create(function()
            self.menu:onMenuSelect(self.table, pos)
        end)
        coroutine.resume(co)
    else
        self[1].invert = true
        UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
        UIManager:setDirty(nil, function()
            return "fast", self[1].dimen
        end)
        UIManager:tickAfterNext(function()
            logger.dbg("creating coroutine for menu select")
            local co = coroutine.create(function()
                self.menu:onMenuSelect(self.table, pos)
            end)
            coroutine.resume(co)
            self[1].invert = false
            --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
            UIManager:setDirty(self.show_parent, function()
                return "ui", self[1].dimen
            end)
        end)
    end
    return true
end

function MenuItem:onHoldSelect(arg, ges)
    local pos = self:getGesPosition(ges)
    if G_reader_settings:isFalse("flash_ui") then
        self.menu:onMenuHold(self.table, pos)
    else
        self[1].invert = true
        UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
        UIManager:setDirty(nil, function()
            return "fast", self[1].dimen
        end)
        UIManager:tickAfterNext(function()
            self.menu:onMenuHold(self.table, pos)
            self[1].invert = false
            --UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
            UIManager:setDirty(self.show_parent, function()
                return "ui", self[1].dimen
            end)
        end)
    end
    return true
end

--[[
Widget that displays menu
--]]
local Menu = FocusManager:new{
    show_parent = nil,
    -- face for displaying item contents
    cface = Font:getFace("cfont"),
    -- face for menu title
    tface = Font:getFace("tfont"),
    -- face for paging info display
    fface = Font:getFace("ffont"),
    -- font for item shortcut
    sface = Font:getFace("scfont"),

    title = "No Title",
    -- default width and height
    width = nil,
    -- height will be calculated according to item number if not given
    height = nil,
    header_padding = Size.padding.large,
    dimen = Geom:new{},
    item_table = {},
    item_shortcuts = {
        "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
        "A", "S", "D", "F", "G", "H", "J", "K", "L", "Del",
        "Z", "X", "C", "V", "B", "N", "M", ".", "Sym",
    },
    item_table_stack = nil,
    is_enable_shortcut = true,

    item_dimen = nil,
    page = 1,

    item_group = nil,
    page_info = nil,
    page_return = nil,

    paths = {},  -- table to trace navigation path

    -- set this to true to not paint as popup menu
    is_borderless = false,
    -- if you want to embed the menu widget into another widget, set
    -- this to false
    is_popout = true,
    -- set this to true to add close button
    has_close_button = true,
    -- close_callback is a function, which is executed when menu is closed
    -- it is usually set by the widget which creates the menu
    close_callback = nil,
    linesize = Size.line.medium,
    perpage = G_reader_settings:readSetting("items_per_page") or 14,
    line_color = Blitbuffer.COLOR_DARK_GRAY,
}

function Menu:_recalculateDimen()
    self.perpage = self.perpage_custom or G_reader_settings:readSetting("items_per_page") or 14
    self.span_width = 0
    self.dimen.w = self.width
    self.dimen.h = self.height or Screen:getHeight()
    if self.dimen.h > Screen:getHeight() or self.dimen.h == nil then
        self.dimen.h = Screen:getHeight()
    end
    self.item_dimen = Geom:new{
        w = self.dimen.w,
        h = Screen:scaleBySize(46),
    }
    local height_dim
    local bottom_height = 0
    local top_height = 0
    if self.page_return_arrow and self.page_info_text then
        bottom_height = math.max(self.page_return_arrow:getSize().h, self.page_info_text:getSize().h)
            + 2 * Size.padding.button
    end
    if self.menu_title and not self.no_title then
        top_height = self.menu_title_group:getSize().h + 2 * Size.padding.small
    end
    height_dim = self.dimen.h - bottom_height - top_height
    self.item_dimen.h = math.floor(height_dim / self.perpage)
    self.span_width = math.floor((height_dim - (self.perpage * (self.item_dimen.h ))) / 2 -1 )
    self.page_num = math.ceil(#self.item_table / self.perpage)
    -- fix current page if out of range
    if self.page_num > 0 and self.page > self.page_num then self.page = self.page_num end
end

function Menu:init()
    self.show_parent = self.show_parent or self
    self.item_table_stack = {}
    self.dimen.w = self.width
    self.dimen.h = self.height or Screen:getHeight()
    if self.dimen.h > Screen:getHeight() or self.dimen.h == nil then
        self.dimen.h = Screen:getHeight()
    end
    self.page = 1

    -----------------------------------
    -- start to set up widget layout --
    -----------------------------------
    self.menu_title = TextWidget:new{
        overlap_align = "center",
        text = self.title,
        face = self.tface,
    }
    local menu_title_container = CenterContainer:new{
        dimen = Geom:new{
            w = self.dimen.w,
            h = self.menu_title:getSize().h,
        },
        self.menu_title,
    }
    local path_text_container

    if self.show_path then
        self.path_text = TextWidget:new{
            face = Font:getFace("xx_smallinfofont"),
            text = BD.directory(self.path),
            max_width = self.dimen.w - 2*Size.padding.small,
            truncate_left = true,
        }
        path_text_container = CenterContainer:new{
            dimen = Geom:new{
                w = self.dimen.w,
                h = self.path_text:getSize().h,
            },
            self.path_text,
        }
        self.menu_title_group = VerticalGroup:new{
            align = "center",
            menu_title_container,
            path_text_container,
        }
    else
        self.menu_title_group = VerticalGroup:new{
            align = "center",
            menu_title_container
        }
    end
    -- group for title bar
    self.title_bar = OverlapGroup:new{
        dimen = {w = self.dimen.w, h = self.menu_title_group:getSize().h},
        self.menu_title_group,
    }
    -- group for items
    self.item_group = VerticalGroup:new{}
    -- group for page info
    local chevron_left = "resources/icons/appbar.chevron.left.png"
    local chevron_right = "resources/icons/appbar.chevron.right.png"
    local chevron_first = "resources/icons/appbar.chevron.first.png"
    local chevron_last = "resources/icons/appbar.chevron.last.png"
    if BD.mirroredUILayout() then
        chevron_left, chevron_right = chevron_right, chevron_left
        chevron_first, chevron_last = chevron_last, chevron_first
    end
    self.page_info_left_chev = Button:new{
        icon = chevron_left,
        callback = function() self:onPrevPage() end,
        bordersize = 0,
        show_parent = self.show_parent,
    }
    self.page_info_right_chev = Button:new{
        icon = chevron_right,
        callback = function() self:onNextPage() end,
        bordersize = 0,
        show_parent = self.show_parent,
    }
    self.page_info_first_chev = Button:new{
        icon = chevron_first,
        callback = function() self:onFirstPage() end,
        bordersize = 0,
        show_parent = self.show_parent,
    }
    self.page_info_last_chev = Button:new{
        icon = chevron_last,
        callback = function() self:onLastPage() end,
        bordersize = 0,
        show_parent = self.show_parent,
    }
    self.page_info_spacer = HorizontalSpan:new{
        width = Screen:scaleBySize(32),
    }
    self.page_info_left_chev:hide()
    self.page_info_right_chev:hide()
    self.page_info_first_chev:hide()
    self.page_info_last_chev:hide()

    local title_goto, type_goto, hint_func
    local buttons = {
        {
            {
                text = _("Cancel"),
                callback = function()
                    self.page_info_text:closeInputDialog()
                end,
            },
            {
                text = _("Go to page"),
                is_enter_default = true,
                callback = function()
                    local page = tonumber(self.page_info_text.input_dialog:getInputText())
                    if page and page >= 1 and page <= self.page_num then
                        self:onGotoPage(page)
                    end
                    self.page_info_text:closeInputDialog()
                end,
            },
        },
    }

    if self.goto_letter then
        title_goto = _("Enter page number or letter")
        type_goto = "string"
        hint_func = function()
            return string.format("(1 - %s) or (a - z)", self.page_num)
        end
        table.insert(buttons[1], {
            text = _("Go to letter"),
            is_enter_default = true,
            callback = function()
                for k, v in ipairs(self.item_table) do
                    --- @todo Support utf8 lowercase.
                    local filename = FFIUtil.basename(v.path):lower()
                    local search_string = self.page_info_text.input_dialog:getInputText():lower()
                    local i, _ = filename:find(search_string)
                    if i == 1 and not v.is_go_up then
                        self:onGotoPage(math.ceil(k / self.perpage))
                        break
                    end
                end
                self.page_info_text:closeInputDialog()
            end,
        })
    else
        title_goto = _("Enter page number")
        type_goto = "number"
        hint_func = function()
            return string.format("(1 - %s)", self.page_num)
        end
    end

    self.page_info_text = Button:new{
        text = "",
        hold_input = {
            title = title_goto ,
            type = type_goto,
            hint_func = hint_func,
            buttons = buttons,
        },
        bordersize = 0,
        text_font_face = "cfont",
        text_font_size = 20,
        text_font_bold = false,
    }
    self.page_info = HorizontalGroup:new{
        self.page_info_first_chev,
        self.page_info_spacer,
        self.page_info_left_chev,
        self.page_info_text,
        self.page_info_right_chev,
        self.page_info_spacer,
        self.page_info_last_chev,
    }

    -- return button
    self.page_return_arrow = Button:new{
        icon = "resources/icons/appbar.arrow.left.up.png",
        callback = function()
            if self.onReturn then self:onReturn() end
        end,
        bordersize = 0,
        show_parent = self.show_parent,
        readonly = self.return_arrow_propagation,
    }
    self.page_return_arrow:hide()
    self.return_button = HorizontalGroup:new{
        HorizontalSpan:new{
            width = Size.span.horizontal_small,
        },
        self.page_return_arrow,
    }

    local header = VerticalGroup:new{
        VerticalSpan:new{width = self.header_padding},
        self.title_bar,
    }
    local body = self.item_group
    local footer = BottomContainer:new{
        dimen = self.dimen:copy(),
        self.page_info,
    }
    local page_return = BottomContainer:new{
        dimen = self.dimen:copy(),
        WidgetContainer:new{
            dimen = Geom:new{
                w = Screen:getWidth(),
                h = self.page_return_arrow:getSize().h,
            },
            self.return_button,
        }
    }

    self:_recalculateDimen()
    self.vertical_span = HorizontalGroup:new{
        VerticalSpan:new{ width = self.span_width }
    }
    if self.no_title then
        self.content_group = VerticalGroup:new{
            align = "left",
            self.vertical_span,
            body,
        }
    else
        self.content_group = VerticalGroup:new{
            align = "left",
            header,
            self.vertical_span,
            body,
        }
    end
    local content = OverlapGroup:new{
        -- This unique allow_mirroring=false looks like it's enough
        -- to have this complex Menu, and all widgets based on it,
        -- be mirrored correctly with RTL languages
        allow_mirroring = false,
        dimen = self.dimen:copy(),
        self.content_group,
        page_return,
        footer,
    }

    self[1] = FrameContainer:new{
        background = Blitbuffer.COLOR_WHITE,
        bordersize = self.is_borderless and 0 or 2,
        padding = 0,
        margin = 0,
        radius = self.is_popout and math.floor(self.dimen.w/20) or 0,
        content
    }
    ------------------------------------------
    -- start to set up input event callback --
    ------------------------------------------
    if Device:isTouchDevice() then
        if self.has_close_button then
            table.insert(self.title_bar, MenuCloseButton:new{
                menu = self,
                padding_right = self.header_padding,
            })
        end
        -- watch for outer region if it's a self contained widget
        if self.is_popout then
            self.ges_events.TapCloseAllMenus = {
                GestureRange:new{
                    ges = "tap",
                    range = Geom:new{
                        x = 0, y = 0,
                        w = Screen:getWidth(),
                        h = Screen:getHeight(),
                    }
                }
            }
        end
        -- delegate swipe gesture to GestureManager in filemanager
        if self.is_file_manager ~= true then
            self.ges_events.Swipe = {
                GestureRange:new{
                    ges = "swipe",
                    range = self.dimen,
                }
            }
        end
        self.ges_events.Close = self.on_close_ges
    end

    if not Device:hasKeyboard() then
        -- remove menu item shortcut for K4
        self.is_enable_shortcut = false
    end

    if Device:hasKeys() then
        -- set up keyboard events
        self.key_events.Close = { {"Back"}, doc = "close menu" }
        if Device:hasFewKeys() then
            self.key_events.Close = { {"Left"}, doc = "close menu" }
        end
        self.key_events.NextPage = {
            {Input.group.PgFwd}, doc = "goto next page of the menu"
        }
        self.key_events.PrevPage = {
            {Input.group.PgBack}, doc = "goto previous page of the menu"
        }
    end

    if Device:hasDPad() then
        -- we won't catch presses to "Right", leave that to MenuItem.
        self.key_events.FocusRight = nil
        -- shortcut icon is not needed for touch device
        if self.is_enable_shortcut then
            self.key_events.SelectByShortCut = { {self.item_shortcuts} }
        end
        self.key_events.Select = {
            {"Press"}, doc = "select current menu item"
        }
        self.key_events.Right = {
            {"Right"}, doc = "hold  menu item"
        }
    end

    if #self.item_table > 0 then
        -- if the table is not yet initialized, this call
        -- must be done manually:
        self.page = math.ceil((self.item_table.current or 1) / self.perpage)
    end
    if self.path_items then
        self:refreshPath()
    else
        self:updateItems()
    end
end

function Menu:onCloseWidget()
    --- @fixme
    -- we cannot refresh regionally using the dimen field
    -- because some menus without menu title use VerticalGroup to include
    -- a text widget which is not calculated into the dimen.
    -- For example, it's a dirty hack to use two menus(one this menu and one
    -- touch menu) in the filemanager in order to capture tap gesture to popup
    -- the filemanager menu.
    -- NOTE: For the same reason, don't make it flash,
    --       because that'll trigger when we close the FM and open a book...
    UIManager:setDirty(nil, "partial")
end

function Menu:updatePageInfo(select_number)
    if self.item_group[1] then
        if Device:hasDPad() then
            -- reset focus manager accordingly
            self.selected = { x = 1, y = select_number }
        end
        -- update page information
        if self.page_num > 1 then
            self.page_info_text:setText(FFIUtil.template(_("Page %1 of %2"), self.page, self.page_num))
        else
            self.page_info_text:setText("");
        end
        self.page_info_left_chev:showHide(self.page_num > 1)
        self.page_info_right_chev:showHide(self.page_num > 1)
        self.page_info_first_chev:showHide(self.page_num > 2)
        self.page_info_last_chev:showHide(self.page_num > 2)
        self.page_return_arrow:showHide(self.onReturn ~= nil)

        self.page_info_left_chev:enableDisable(self.page > 1)
        self.page_info_right_chev:enableDisable(self.page < self.page_num)
        self.page_info_first_chev:enableDisable(self.page > 1)
        self.page_info_last_chev:enableDisable(self.page < self.page_num)
        self.page_return_arrow:enableDisable(#self.paths > 0)
    else
        self.page_info_text:setText(_("No choices available"))
    end
end

function Menu:updateItems(select_number)
    local old_dimen = self.dimen and self.dimen:copy()
    -- self.layout must be updated for focusmanager
    self.layout = {}
    self.item_group:clear()
    self.page_info:resetLayout()
    self.return_button:resetLayout()
    self.vertical_span:clear()
    self.content_group:resetLayout()
    self:_recalculateDimen()

    -- default to select the first item
    if not select_number then
        select_number = 1
    end
    --font size between 12 and 18 for better matching
    local infont_size = math.floor(18 - (self.perpage - 6) / 3)
    --default font size between 14 and 24 for better matching
    local font_size = G_reader_settings:readSetting("items_font_size") or math.floor(24 - ((self.perpage - 6)/ 18) * 10 )

    for c = 1, math.min(self.perpage, #self.item_table) do
        -- calculate index in item_table
        local i = (self.page - 1) * self.perpage + c
        if i <= #self.item_table then
            local item_shortcut = nil
            local shortcut_style = "square"
            if self.is_enable_shortcut then
                -- give different shortcut_style to keys in different
                -- lines of keyboard
                if c >= 11 and c <= 20 then
                    --shortcut_style = "rounded_corner"
                    shortcut_style = "grey_square"
                end
                item_shortcut = self.item_shortcuts[c]
            end
            local item_tmp = MenuItem:new{
                show_parent = self.show_parent,
                state = self.item_table[i].state,
                state_size = self.state_size or {},
                text = Menu.getMenuText(self.item_table[i]),
                bidi_wrap_func = self.item_table[i].bidi_wrap_func,
                mandatory = self.item_table[i].mandatory,
                mandatory_func = self.item_table[i].mandatory_func,
                bold = self.item_table.current == i or self.item_table[i].bold == true,
                dim = self.item_table[i].dim,
                font = "smallinfofont",
                font_size = font_size,
                infont = "infont",
                infont_size = infont_size,
                dimen = self.item_dimen:new(),
                shortcut = item_shortcut,
                shortcut_style = shortcut_style,
                table = self.item_table[i],
                menu = self,
                linesize = self.linesize,
                single_line = self.single_line,
                align_baselines = self.align_baselines,
                line_color = self.line_color,
            }
            table.insert(self.item_group, item_tmp)
            -- this is for focus manager
            table.insert(self.layout, {item_tmp})
        end -- if i <= self.items
    end -- for c=1, self.perpage

    self:updatePageInfo(select_number)
    if self.show_path then
        self.path_text:setText(BD.directory(self.path))
    end

    UIManager:setDirty(self.show_parent, function()
        local refresh_dimen =
            old_dimen and old_dimen:combine(self.dimen)
            or self.dimen
        return "ui", refresh_dimen
    end)
end

--[[
    the itemnumber paramter determines menu page number after switching item table
    1. itemnumber >= 0
        the page number is calculated with items per page
    2. itemnumber == nil
        the page number is 1
    3. itemnumber is negative number
        the page number is not changed, used when item_table is appended with
        new entries

    alternatively, itemmatch may be provided as a {key = value} table,
    and the page number will be the page containing the first item for
    which item.key = value
--]]
function Menu:switchItemTable(new_title, new_item_table, itemnumber, itemmatch)
    if self.menu_title and new_title then
        self.menu_title:setText(new_title)
    end

    if itemnumber == nil then
        self.page = 1
    elseif itemnumber >= 0 then
        self.page = math.ceil(itemnumber / self.perpage)
    end

    if type(itemmatch) == "table" then
        local key, value = next(itemmatch)
        for num, item in ipairs(new_item_table) do
            if item[key] == value then
                self.page = math.floor((num-1) / self.perpage) + 1
                break
            end
        end
    end

    -- make sure current page is in right page range
    local max_pages = math.ceil(#new_item_table / self.perpage)
    if self.page > max_pages then
        self.page = max_pages
    end

    self.item_table = new_item_table
    self:updateItems()
end

function Menu:onScreenResize(dimen)
    --- @todo Investigate: could this cause minor memory leaks?
    self:init()
    return false
end

function Menu:onSelectByShortCut(_, keyevent)
    for k,v in ipairs(self.item_shortcuts) do
        if k > self.perpage then
            break
        elseif v == keyevent.key then
            if self.item_table[(self.page-1)*self.perpage + k] then
                self:onMenuSelect(self.item_table[(self.page-1)*self.perpage + k])
            end
            break
        end
    end
    return true
end

function Menu:onShowGotoDialog()
    if self.page_info_text and self.page_info_text.hold_input then
        self.page_info_text:onInput(self.page_info_text.hold_input)
    end
    return true
end

function Menu:onWrapFirst()
    if self.page > 1 then
        self.page = self.page - 1
        local end_position = self.perpage
        if self.page == self.page_num then
            end_position = #self.item_table % self.perpage
        end
        self:updateItems(end_position)
    end
    return false
end

function Menu:onWrapLast()
    if self.page < self.page_num then
        self:onNextPage()
    end
    return false
end

--[[
override this function to process the item selected in a different manner
]]--
function Menu:onMenuSelect(item)
    if item.sub_item_table == nil then
        if self.close_callback then
            self.close_callback()
        end
        self:onMenuChoice(item)
    else
        -- save menu title for later resume
        self.item_table.title = self.title
        table.insert(self.item_table_stack, self.item_table)
        self:switchItemTable(item.text, item.sub_item_table)
    end
    return true
end

--[[
    default to call item callback
    override this function to handle the choice
--]]
function Menu:onMenuChoice(item)
    if item.callback then
        item.callback()
    end
    return true
end

--[[
override this function to process the item hold in a different manner
]]--
function Menu:onMenuHold(item)
    return true
end

function Menu:onNextPage()
    if self.onNext and self.page == self.page_num - 1 then
        self:onNext()
    end
    if self.page < self.page_num then
        self.page = self.page + 1
        self:updateItems()
    elseif self.page == self.page_num then
        -- on the last page, we check if we're on the last item
        local end_position = #self.item_table % self.perpage
        if end_position == 0 then
            end_position = self.perpage
        end
        if end_position ~= self.selected.y then
            self:updateItems(end_position)
        end
        self.page = 1
        self:updateItems()
    end
    return true
end

function Menu:onPrevPage()
    if self.page > 1 then
        self.page = self.page - 1
    elseif self.page == 1 then
        self.page = self.page_num
    end
    self:updateItems()
    return true
end

function Menu:onFirstPage()
    self.page = 1
    self:updateItems()
    return true
end

function Menu:onLastPage()
    self.page = self.page_num
    self:updateItems()
    return true
end

function Menu:onGotoPage(page)
    self.page = page
    self:updateItems()
    return true
end

function Menu:onSelect()
    local item = self.item_table[(self.page-1)*self.perpage+self.selected.y]
    if item then
        self:onMenuSelect(item)
    end
    return true
end

function Menu:onRight()
    local item = self.item_table[(self.page-1)*self.perpage+self.selected.y]
    if item then
        self:onMenuHold(item)
    end
    return true
end

function Menu:onClose()
    local table_length = #self.item_table_stack
    if table_length == 0 then
        self:onCloseAllMenus()
    else
        -- back to parent menu
        local parent_item_table = table.remove(self.item_table_stack, table_length)
        self:switchItemTable(parent_item_table.title, parent_item_table)
    end
    return true
end

function Menu:onCloseAllMenus()
    UIManager:close(self)
    if self.close_callback then
        self.close_callback()
    end
    return true
end

function Menu:onTapCloseAllMenus(arg, ges_ev)
    if ges_ev.pos:notIntersectWith(self.dimen) then
        self:onCloseAllMenus()
        return true
    end
end

function Menu:onSwipe(arg, ges_ev)
    local direction = BD.flipDirectionIfMirroredUILayout(ges_ev.direction)
    if direction == "west" then
        self:onNextPage()
    elseif direction == "east" then
        self:onPrevPage()
    elseif direction == "south" then
        if self.has_close_button and not self.no_title then
            -- If there is a close button displayed (so, this Menu can be
            -- closed), allow easier closing with swipe up/down
            self:onClose()
        end
        -- If there is no close button, it's a top level Menu and swipe
        -- up/down may hide/show top menu
    elseif direction == "north" then
        -- no use for now
        do end -- luacheck: ignore 541
    else -- diagonal swipe
        -- trigger full refresh
        UIManager:setDirty(nil, "full")
    end
end

--- Adds > to touch menu items with a submenu
local arrow_left  = "◂" -- U+25C2 BLACK LEFT-POINTING SMALL TRIANGLE
local arrow_right = "▸" -- U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE
local sub_item_format
-- Adjust arrow direction and position for menu with sub items
-- according to possible user choices
if BD.mirroredUILayout() then
    if BD.rtlUIText() then -- normal case with RTL language
        sub_item_format = "%s " .. BD.rtl(arrow_left)
    else -- user reverted text direction, so LTR
        sub_item_format = BD.ltr(arrow_left) .. " %s"
    end
else
    if BD.rtlUIText() then -- user reverted text direction, so RTL
        sub_item_format = BD.rtl(arrow_right) .. " %s"
    else -- normal case with LTR language
        sub_item_format = "%s " .. BD.ltr(arrow_right)
    end
end

function Menu.getMenuText(item)
    local text
    if item.text_func then
        text = item.text_func()
    else
        text = item.text
    end
    if item.sub_item_table ~= nil or item.sub_item_table_func then
        text = string.format(sub_item_format, text)
    end
    return text
end

function Menu.itemTableFromTouchMenu(t)
    local item_t = {}
    for k, v in FFIUtil.orderedPairs(t) do
        local item = { text = k }
        if v.callback then
            item.callback = v.callback
        else
            item.sub_item_table = v
        end
        table.insert(item_t, item)
    end
    return item_t
end

return Menu
