# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals

from elementmaker import E
from gettext import gettext as _
from uuid import short_uuid

from book_list.book_details import CLASS_NAME as BD_CLASS_NAME, render_metadata
from book_list.globals import get_session_data
from book_list.library_data import (
    current_library_id, set_book_metadata, sync_library_books
)
from book_list.router import home
from book_list.theme import get_color
from book_list.top_bar import create_top_bar
from book_list.ui import query_as_href, show_panel
from dom import clear, ensure_id, set_css, svgicon
from modals import error_dialog, question_dialog
from read_book.bookmarks import create_bookmarks_panel
from read_book.chrome import apply_viewer_chrome_colors, viewer_chrome_colors
from read_book.globals import is_dark_theme, runtime, ui_operations
from read_book.goto import create_goto_panel, create_location_overlay, create_page_list_overlay
from read_book.highlights import builtin_color, create_highlights_panel
from read_book.main_overlay_style import (
    MAIN_OVERLAY_ACTION_CONTROL_CLASS, MAIN_OVERLAY_ACTION_ICON_CLASS,
    MAIN_OVERLAY_ACTIONS_CLASS, MAIN_OVERLAY_FOOTER_ACTION_CLASS,
    MAIN_OVERLAY_FOOTER_ACTIONS_CLASS, MAIN_OVERLAY_FOOTER_CLASS,
    MAIN_OVERLAY_FOOTER_MIDDLE_SLOT_CLASS, MAIN_OVERLAY_FOOTER_RIGHT_SLOT_CLASS,
    MAIN_OVERLAY_FOOTER_SLOT_CLASS, MAIN_OVERLAY_ICON_SIZE, MAIN_OVERLAY_TEXT_ICON_CLASS,
    MAIN_OVERLAY_TIMER_CLASS, MAIN_OVERLAY_TITLE_CLASS, MAIN_OVERLAY_TOP_ROW_CLASS,
    MAIN_OVERLAY_TS_CLASS, timer_id
)
from read_book.open_book import create_open_book
from read_book.prefs.font_size import create_font_size_panel
from read_book.prefs.head_foot import time_formatter, format_time_left
from read_book.prefs.main import create_prefs_panel
from read_book.profiles import create_profiles_panel
from read_book.toc import create_toc_panel
from read_book.word_actions import create_word_actions_panel
from session import session_defaults, get_device_uuid
from utils import (
    default_context_menu_should_be_allowed, full_screen_element,
    full_screen_supported, is_ios, safe_set_inner_html
)
from widgets import create_button, create_spinner


class LoadingMessage:  # {{{
    def __init__(self, msg, current_color_scheme):
        self.msg = msg or ''
        self.current_color_scheme = current_color_scheme
        self.is_not_escapable = True  # prevent Esc key from closing

    def show(self, container):
        self.container_id = container.getAttribute('id')
        container.style.backgroundColor = self.current_color_scheme.background
        container.style.color = self.current_color_scheme.foreground
        container.appendChild(
            E.div(
                style='text-align:center',
                E.div(create_spinner('100px', '100px')),
                E.h2()
        ))
        safe_set_inner_html(container.firstChild.lastChild, self.msg)
        set_css(container.firstChild, position='relative', top='50%', transform='translateY(-50%)')

    def set_msg(self, msg):
        self.msg = msg
        container = document.getElementById(self.container_id)
        safe_set_inner_html(container.firstChild.lastChild, self.msg)

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click
# }}}


class DeleteBook:  # {{{
    def __init__(self, overlay, question, ok_icon, ok_text, reload_book):
        self.overlay = overlay
        self.question = question or _(
            'Are you sure you want to remove this book from local storage? You will have to re-download it from calibre if you want to read it again.')
        self.ok_icon = ok_icon or 'trash'
        self.ok_text = ok_text or _('Delete book')
        self.reload_book = reload_book

    def show(self, container):
        self.container_id = container.getAttribute('id')
        apply_viewer_chrome_colors(container)
        container.appendChild(E.div(
            style='display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%',
            tabindex='0',
            E.div(style='margin:1ex 1em; max-width: 80%',
                E.h2(self.question),
                E.div(style='display:flex; justify-content:flex-end; margin-top:1rem',
                    create_button(self.ok_text, self.ok_icon, action=self.delete_book, highlight=True),
                    E.span('\xa0'),
                    create_button(_('Cancel'), action=self.cancel),
                )
            ),
            onkeyup=def (ev):
                if ev.key is 'Escape':
                    ev.stopPropagation(), ev.preventDefault()
                    self.cancel()
                elif ev.key is 'Enter' or ev.key is 'Return':
                    ev.stopPropagation(), ev.preventDefault()
                    self.delete_book()
        ))
        window.setTimeout(self.focus_me, 0)

    def focus_me(self):
        document.getElementById(self.container_id).lastChild.focus()

    def show_working(self):
        container = document.getElementById(self.container_id)
        clear(container)
        container.appendChild(
            E.div(
                style='text-align:center',
                E.div(create_spinner('100px', '100px')),
                E.h2()
        ))
        safe_set_inner_html(container.lastChild.lastChild, _('Deleting local book copy, please wait...'))

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def delete_book(self):
        if runtime.is_standalone_viewer:
            if self.reload_book:
                self.overlay.view.reload_book()
            return
        self.show_working()
        view = self.overlay.view
        ui_operations.delete_book(view.book, def (book, errmsg):
            self.overlay.hide_current_panel()
            if errmsg:
                view.ui.show_error(
                    _('Failed to delete book'),
                    _('Failed to delete book from local storage, click "Show details" for more information.'),
                    errmsg
                )
            else:
                if self.reload_book:
                    self.overlay.view.reload_book()
                else:
                    home()
        )

    def cancel(self):
        self.overlay.hide_current_panel()
# }}}


class SyncBook:  # {{{
    def __init__(self, overlay):
        self.overlay = overlay
        self.canceled = False

    def show(self, container):
        self.container_id = container.getAttribute('id')
        apply_viewer_chrome_colors(container)
        book = self.overlay.view.book
        to_sync = [[book.key, Date(0)]]
        view = self.overlay.view

        def start_download():
            sync_library_books(book.key[0], to_sync, self.sync_data_received)

        def after_position_upload(success):
            start_download()

        def after_annot_upload(success):
            view.upload_current_position(after_position_upload)

        if ui_operations.upload_pending_annotations:
            ui_operations.upload_pending_annotations(after_annot_upload)
        else:
            after_annot_upload(True)
        container.appendChild(E.div(
            style='display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%',
            E.div(style='margin:1ex 1em; max-width: 80vw',
                E.h2(_('Syncing last read position and annotations')),
                E.p(_('Downloading data from server, please wait...')),
                E.div(style='display:flex; justify-content:flex-end',
                    create_button(_('Cancel'), action=self.cancel),
                )
            )
        ))

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def cancel(self):
        self.canceled = True
        self.overlay.hide_current_panel()

    def sync_data_received(self, library_id, lrmap, load_type, xhr, ev):
        if self.canceled:
            return
        self.overlay.hide()
        if load_type is not 'load':
            if xhr.responseText is 'login required for sync':
                error_dialog(_('Failed to fetch sync data'), _('You must setup user accounts and login to use the sync functionality'))
            else:
                error_dialog(
                    _('Failed to fetch sync data'),
                    _('Failed to download sync data from server, click "Show details" for more information.'),
                    xhr.error_html
                )
            return
        data = JSON.parse(xhr.responseText)
        book = self.overlay.view.book
        dev = get_device_uuid()
        epoch = 0
        ans = None
        new_annotations_map = None
        for key in data:
            book_id, fmt = key.partition(':')[::2]
            if book_id is str(book.key[1]) and fmt.upper() is book.key[2].upper():
                new_vals = data[key]
                last_read_positions = new_vals.last_read_positions
                new_annotations_map = new_vals.annotations_map
                for d in last_read_positions:
                    if d.device is not dev and d.epoch > epoch:
                        epoch = d.epoch
                        ans = d
                break
        cfi = ans?.cfi
        if new_annotations_map or cfi:
            self.overlay.view.sync_data_received(cfi, new_annotations_map)


# }}}


def create_main_overlay_action_with_icon(text, tooltip, action, icon_elem, icon_color=None):
    if icon_color:
        icon_elem.style.color = icon_color
    icon_elem.classList.add(MAIN_OVERLAY_ACTION_ICON_CLASS)
    return E.li(E.button(
        class_=MAIN_OVERLAY_ACTION_CONTROL_CLASS, title=tooltip, type='button', onclick=action,
        icon_elem, text
    ))


def create_main_overlay_action(text, tooltip, action, icon, icon_color=None):
    return create_main_overlay_action_with_icon(text, tooltip, action, svgicon(icon, MAIN_OVERLAY_ICON_SIZE, MAIN_OVERLAY_ICON_SIZE), icon_color)


def create_main_overlay_text_action(text, tooltip, action, icon_text):
    return create_main_overlay_action_with_icon(text, tooltip, action, E.span(icon_text, class_=MAIN_OVERLAY_TEXT_ICON_CLASS))


def create_main_overlay_footer_action(icon, text, tooltip, action):
    return E.button(
        class_=MAIN_OVERLAY_FOOTER_ACTION_CLASS, title=tooltip, type='button', onclick=action,
        svgicon(icon, MAIN_OVERLAY_ICON_SIZE, MAIN_OVERLAY_ICON_SIZE), text
    )


def create_main_overlay_footer(close_action, help_action):
    middle = E.div(class_=' '.join([MAIN_OVERLAY_FOOTER_SLOT_CLASS, MAIN_OVERLAY_FOOTER_MIDDLE_SLOT_CLASS]), '\xa0')
    right = E.div(class_=' '.join([MAIN_OVERLAY_FOOTER_SLOT_CLASS, MAIN_OVERLAY_FOOTER_RIGHT_SLOT_CLASS]), '\xa0')
    footer = E.div(
        class_=MAIN_OVERLAY_FOOTER_CLASS,
        E.div(
            class_=MAIN_OVERLAY_FOOTER_ACTIONS_CLASS,
            create_main_overlay_footer_action('close', _('Close'), _('Close the viewer controls'), close_action),
            create_main_overlay_footer_action('help', _('Help'), _('Show viewer help'), help_action)
        ),
        middle,
        right,
    )
    return {'footer': footer, 'middle_slot': middle, 'right_slot': right}


def render_main_overlay_footer_templates(footer_parts, renderer):
    if not renderer:
        return
    renderer(footer_parts.middle_slot, 'controls_footer', 'middle', '')
    renderer(footer_parts.right_slot, 'controls_footer', 'right', '')


def simple_overlay_title(title, overlay, container, close_action):
    apply_viewer_chrome_colors(container)

    def action():
        event = this
        event.stopPropagation()
        overlay.hide_current_panel(event)

    create_top_bar(container, title=title, icon='close', action=close_action or action)


class MainOverlay:  # {{{
    def __init__(self, overlay, elements):
        self.overlay = overlay
        self.elements = elements or {}
        self.timer = None
        if window.Intl?.DateTimeFormat:
            self.date_formatter = window.Intl.DateTimeFormat(undefined, {'hour': 'numeric', 'minute': 'numeric'})
        else:
            self.date_formatter = {'format': def (date):
                return '{}:{}'.format(date.getHours(), date.getMinutes())
            }

    def create_actions(self, sd):
        ac = create_main_overlay_action
        text_ac = create_main_overlay_text_action

        sync_icon_color = None
        if ui_operations.has_unsynced_changes and ui_operations.has_unsynced_changes():
            sync_icon_color = builtin_color('yellow', is_dark_theme())
        sync_action = ac(_('Sync'), _('Get last read position and annotations from the server'), self.overlay.sync_book, 'cloud-download', sync_icon_color)
        delete_action = ac(_('Delete'), _('Delete this book from local storage'), self.overlay.delete_book, 'trash')
        reload_action = ac(
            _('Reload'),
            _('Reload this book from the {}').format(_('computer') if runtime.is_standalone_viewer else _('server')),
            self.overlay.reload_book,
            'refresh'
        )
        back_action = ac(_('Back'), _('Go back'), self.back, 'arrow-left')
        forward_action = ac(_('Forward'), _('Go forward'), self.forward, 'arrow-right')
        nav_actions = E.ul(back_action, forward_action)
        if runtime.is_standalone_viewer:
            reload_actions = E.ul(
                ac(_('Open book'), _('Open a book'), self.overlay.open_book, 'book'),
                reload_action
            )
            editable_formats = v'''__EDITABLE_FORMATS__'''
            fmt = self.overlay.view?.book?.manifest.book_format or ''
            if fmt.toUpperCase() in editable_formats:
                reload_actions.appendChild(ac(_('Edit book'), _('Edit this book'), self.overlay.view.edit_book, 'edit'))
        else:
            reload_actions = E.ul(sync_action, delete_action, reload_action)

        bookmarks_action = ac(_('Bookmarks'), _('Browse all bookmarks'), self.overlay.show_bookmarks, 'bookmark')
        highlights_action = ac(
            _('Highlights'), _('Browse all highlights'), def ():
                self.overlay.show_highlights()
            , 'image')
        reading_stats_action = ac(_('Reading statistics'), _('Show reading statistics for this book'), def ():
            self.overlay.show_reading_stats()
        , 'hourglass')

        toc_actions = E.ul(ac(_('Table of Contents'), _('Browse the Table of Contents'), self.overlay.show_toc, 'toc'))
        toc_actions.appendChild(ac(_('Reference mode'), _('Toggle the Reference mode'), self.overlay.toggle_reference_mode, 'reference-mode'))

        actions_div = E.div(  # actions
            nav_actions,
            E.ul(
                ac(_('Search'), _('Search for text in this book'), self.overlay.show_search, 'search'),
                ac(_('Go to'), _('Go to a specific location in the book'), self.overlay.show_goto, 'chevron-right'),
            ),
            reload_actions,
            toc_actions,
            E.ul(highlights_action, bookmarks_action, reading_stats_action),
            E.ul(
                text_ac(_('Font size'), _('Change text size'), self.overlay.show_font_size_chooser, 'Aa'),
                ac(_('Preferences'), _('Configure the E-book viewer'), self.overlay.show_prefs, 'cogs'),
            ),
            class_=MAIN_OVERLAY_ACTIONS_CLASS
        )
        if not runtime.is_standalone_viewer:
            home_action = ac(_('Home'), _('Return to the home page'), def ():
                    home()
                    ui_operations.close_book()
            , 'home')
            library_action = ac(_('Library'), _('Return to the library page'), self.overlay.show_library, 'library')
            book_action = ac(_('Book'), _('Return to the book\'s page'), self.overlay.show_book_page, 'book')
            actions_div.insertBefore(E.ul(home_action, library_action, book_action), actions_div.firstChild)
        full_screen_actions = []
        if runtime.is_standalone_viewer:
            text = _('Exit full screen') if runtime.viewer_in_full_screen else _('Enter full screen')
            full_screen_actions.push(
                ac(text, _('Toggle full screen mode'), def (): self.overlay.hide(), ui_operations.toggle_full_screen();, 'full-screen'))
            full_screen_actions.push(
                ac(_('Print'), _('Print book to PDF'), def (): self.overlay.hide(), ui_operations.print_book();, 'print'))
        else:
            if not is_ios and full_screen_supported():
                text = _('Exit full screen') if full_screen_element() else _('Enter full screen')
                # No fullscreen on iOS, see http://caniuse.com/#search=fullscreen
                full_screen_actions.push(
                    ac(text, _('Toggle full screen mode'), def (): self.overlay.hide(), ui_operations.toggle_full_screen();, 'full-screen')
                )
        if sd.get('read_mode') is 'flow':
            asa = self.overlay.view.autoscroll_active
            full_screen_actions.append(ac(
                _('Stop auto scroll') if asa else _('Auto scroll'), _('Toggle auto-scrolling'), def ():
                    self.overlay.hide()
                    window.setTimeout(self.overlay.view.toggle_autoscroll, 0)
                , 'auto-scroll'))

        actions_div.appendChild(E.ul(
            ac(_('Read aloud'), _('Read the book aloud'), def ():
               self.overlay.hide()
               self.overlay.view.start_read_aloud()
            , 'bullhorn'),
            ac(_('Profiles'), _('Quickly switch between different settings'), def ():
                self.overlay.show_profiles()
            , 'convert')
        ))

        if full_screen_actions.length:
            actions_div.appendChild(E.ul(*full_screen_actions))

        no_selection_bar = not sd.get('show_selection_bar')
        if runtime.is_standalone_viewer:
            if no_selection_bar:
                actions_div.appendChild(E.ul(
                    ac(_('Lookup/search word'), _('Lookup or search for the currently selected word'),
                    def (): self.overlay.hide(), ui_operations.toggle_lookup(True);, 'library')
                ))
            copy_actions = E.ul()
            if self.elements.link:
                copy_actions.appendChild(ac(_('Copy link'), _('Copy the current link'), def ():
                    self.overlay.hide(), ui_operations.copy_selection(self.elements.link)
                , 'link'))
            if self.elements.img:
                copy_actions.appendChild(ac(_('View image'), _('View the current image'), def ():
                    self.overlay.hide(), ui_operations.view_image(self.elements.img)
                , 'image'))
                copy_actions.appendChild(ac(_('Copy image'), _('Copy the current image'), def ():
                    self.overlay.hide(), ui_operations.copy_image(self.elements.img)
                , 'copy'))
            if copy_actions.childNodes.length:
                actions_div.appendChild(copy_actions)

            actions_div.appendChild(E.ul(
                ac(_('Inspector'), _('Show the content inspector'),
                   def (): self.overlay.hide(), ui_operations.toggle_inspector();, 'bug'),
                ac(_('Reset interface'), _('Reset E-book viewer panels, toolbars and scrollbars to defaults'),
                    def ():
                        question_dialog(
                            _('Are you sure?'), _(
                                'Are you sure you want to reset the viewer interface'
                                ' to its default appearance?'
                        ),
                            def (yes):
                                if yes:
                                    self.overlay.hide()
                                    ui_operations.reset_interface()
                                    sd = get_session_data()
                                    sd.set('skipped_dialogs', session_defaults().skipped_dialogs)
                        )
                    , 'window-restore'),
                ac(_('Quit'), _('Close the E-book viewer'),
                   def (): self.overlay.hide(), ui_operations.quit();, 'remove'),
            ))
        else:
            copy_actions = E.ul()
            if self.elements.link:
                copy_actions.appendChild(ac(_('Copy link'), _('Copy the current link'), def ():
                    self.overlay.hide(), ui_operations.copy_selection(self.elements.link)
                , 'link'))
            if self.elements.img:
                copy_actions.appendChild(ac(_('View image'), _('View the current image'), def ():
                    self.overlay.hide(), ui_operations.view_image(self.elements.img)
                , 'image'))
                if window.navigator.clipboard:
                    copy_actions.appendChild(ac(_('Copy image'), _('Copy the current image'), def ():
                        self.overlay.hide(), ui_operations.copy_image(self.elements.img)
                    , 'copy'))
            if copy_actions.childNodes.length:
                actions_div.appendChild(copy_actions)

        return actions_div

    def create_top_section(self, actions_div):
        return E.div(class_=MAIN_OVERLAY_TS_CLASS,
            onclick=def (evt): evt.stopPropagation();,
            E.div(
                E.div(self.overlay.view.book?.metadata?.title or _('Unknown'), class_=MAIN_OVERLAY_TITLE_CLASS),
                E.div(time_formatter.format(Date()), id=timer_id(), class_=MAIN_OVERLAY_TIMER_CLASS),
                class_=MAIN_OVERLAY_TOP_ROW_CLASS,
            ),
            actions_div,
        )

    def create_footer(self):
        def close_viewer_controls(ev):
            ev.stopPropagation()
            self.overlay.hide_current_panel(ev)

        def show_viewer_help(ev):
            ev.stopPropagation()
            ui_operations.show_help('viewer')
            self.overlay.hide_current_panel(ev)

        footer_parts = create_main_overlay_footer(close_viewer_controls, show_viewer_help)
        render_main_overlay_footer_templates(footer_parts, self.overlay.view.create_template_renderer())
        return footer_parts.footer

    def show(self, container):
        self.container_id = container.getAttribute('id')
        sd = get_session_data()
        container.appendChild(self.create_top_section(self.create_actions(sd)))
        container.appendChild(self.create_footer())

        self.on_hide()
        self.timer = setInterval(self.update_time, 1000)

    def update_time(self):
        tm = document.getElementById(timer_id())
        if tm:
            tm.textContent = self.date_formatter.format(Date())

    def on_hide(self):
        if self.timer is not None:
            clearInterval(self.timer)
        self.timer = None

    def back(self):
        window.history.back()
        self.overlay.hide()

    def forward(self):
        window.history.forward()
        self.overlay.hide()

# }}}


class SimpleOverlay:  # {{{
    def __init__(self, overlay, create_func, title):
        self.overlay = overlay
        self.create_func = create_func
        self.title = title

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def show(self, container):
        simple_overlay_title(self.title, self.overlay, container)
        self.create_func(self.overlay, container)
# }}}


class TOCOverlay(SimpleOverlay):  # {{{
    def __init__(self, overlay, create_func, title):
        self.overlay = overlay
        self.create_func = create_func or create_toc_panel
        self.title = title or _('Table of Contents')

    def show(self, container):
        container.appendChild(E.div())
        container = container.firstChild
        simple_overlay_title(self.title, self.overlay, container)
        self.create_func(self.overlay.view.book, container, self.handle_activate)

    def handle_activate(self, dest, frag):
        self.overlay.hide()
        if jstype(dest) is 'function':
            dest(self.overlay.view, frag)
        else:
            self.overlay.view.goto_named_destination(dest, frag)
# }}}


class GotoOverlay:  # {{{
    def __init__(self, overlay):
        self.overlay = overlay

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def show(self, container):
        container.appendChild(E.div())
        inner = container.firstChild
        simple_overlay_title(_('Go to\u2026'), self.overlay, inner)
        create_goto_panel(self.overlay.view.current_position_data, self.overlay.view.book, inner, self.handle_navigate, self.show_sub_panel)

    def handle_navigate(self, dest, frag):
        self.overlay.hide()
        if jstype(dest) is 'function':
            dest(self.overlay.view, frag)
        else:
            self.overlay.view.goto_named_destination(dest, frag)

    def show_sub_panel(self, name):
        # Push the sub-panel on top of the goto panel (keep goto in stack so
        # closing the sub-panel returns to goto rather than to reading).
        if name is 'location':
            self.overlay.panels.push(SimpleOverlay(
                self.overlay,
                create_location_overlay.bind(None, self.overlay.view.current_position_data, self.overlay.view.book),
                _('Go to location, position or reference\u2026')))
        elif name is 'metadata':
            self.overlay.panels.push(self.overlay._build_metadata_panel())
        elif name is 'page_list':
            self.overlay.panels.push(SimpleOverlay(
                self.overlay,
                create_page_list_overlay.bind(None, self.overlay.view.book),
                _('Go to page')))
        self.overlay.show_current_panel()
# }}}


class WordActionsOverlay:  # {{{
    def __init__(self, word, overlay):
        self.word = word
        self.overlay = overlay

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def show(self, container):
        simple_overlay_title(_('Lookup: {}').format(self.word), self.overlay, container)
        create_word_actions_panel(container, self.word, self.overlay.hide)
# }}}


class PrefsOverlay:  # {{{
    def __init__(self, overlay):
        self.overlay = overlay
        self.changes_occurred = False

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def create_panel_impl(self, container):
        return create_prefs_panel(container, self.overlay.hide_current_panel, self.on_change)

    def show(self, container):
        self.changes_occurred = False
        apply_viewer_chrome_colors(container)
        self.prefs = self.create_panel_impl(container)

    def on_change(self):
        self.changes_occurred = True

    def handle_escape(self):
        self.prefs.onclose()

    def on_hide(self):
        if self.changes_occurred:
            self.changes_occurred = False
            self.overlay.view.preferences_changed()


class ProfilesOverlay(PrefsOverlay):
    def create_panel_impl(self, container):
        return create_profiles_panel(container, self.overlay.hide_current_panel, self.on_change)

    def handle_escape(self):
        self.overlay.hide_current_panel()

# }}}


class FontSizeOverlay:  # {{{
    def __init__(self, overlay):
        self.overlay = overlay

    def show(self, container):
        create_font_size_panel(container, self.overlay.hide_current_panel)
# }}}


class OpenBook:  # {{{
    def __init__(self, overlay, closeable):
        self.overlay = overlay
        self.closeable = closeable

    def handle_escape(self):
        if self.closeable:
            self.overlay.hide_current_panel()
        else:
            if self.overlay.handling_context_menu_event is None:
                ui_operations.quit()

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def show(self, container):
        simple_overlay_title(_('Open a book'), self.overlay, container, def ():
            ev = this
            ev.stopPropagation(), ev.preventDefault()
            if self.closeable:
                self.overlay.hide_current_panel(ev)
            else:
                ui_operations.quit()
        )
        create_open_book(container, self.overlay.view?.book)
# }}}


class ReadingStatsOverlay:  # {{{
    def __init__(self, overlay):
        self.overlay = overlay

    def on_container_click(self, evt):
        pass  # Don't allow panel to be closed by a click

    def reset_stats(self):
        question_dialog(
            _('Reset reading statistics'),
            _('Are you sure you want to delete all reading statistics? This will remove all saved reading rate data.'),
            def (yes):
                if yes:
                    self.do_reset()
        )

    def do_reset(self):
        view = self.overlay.view
        if view.timers:
            view.timers.rates = []
            view.timers.chapter_times = v'{}'
            view.timers.average = view.timers.stddev = 0
        if ui_operations.reset_reading_rates:
            ui_operations.reset_reading_rates()
        self.overlay.hide_current_panel()

    def show(self, container):
        simple_overlay_title(_('Reading statistics'), self.overlay, container)
        view = self.overlay.view
        book = view.book
        if not book or not book.manifest:
            container.appendChild(E.div(_('No book open'), style='padding: 1ex 1em'))
            return

        spine = book.manifest.spine
        current_name = view.currently_showing.name
        current_idx = spine.indexOf(current_name)
        file_progress = view.current_file_progress_frac
        chapter_length = book.manifest.files[current_name]?.length or 0
        remaining_chars = chapter_length * max(0, 1 - file_progress)
        chapter_remaining_time = view.timers.time_for(remaining_chars)
        chapter_done_pct = Math.round(file_progress * 100)
        total_chapters = spine.length

        next_idx = current_idx + 1
        next_name = spine[next_idx] if next_idx < spine.length else None
        next_chapter_time = None
        if next_name:
            next_length = book.manifest.files[next_name]?.length or 0
            next_chapter_time = view.timers.time_for(next_length)

        book_remaining_chars = remaining_chars
        for spine_idx in range(next_idx, spine.length):
            book_remaining_chars += book.manifest.files[spine[spine_idx]]?.length or 0
        book_remaining_time = view.timers.time_for(book_remaining_chars)

        content = E.div(style='padding: 1ex 1em')
        if current_idx >= 0:
            content.appendChild(E.p(
                _('Chapter {current} of {total}: {pct}% complete').format(
                    current=current_idx + 1, total=total_chapters, pct=chapter_done_pct),
                style='margin: 0.5ex 0; font-weight: bold'))

        if chapter_remaining_time is not None:
            content.appendChild(E.p(
                _('Time remaining in chapter: {}').format(format_time_left(chapter_remaining_time)),
                style='margin: 0.5ex 0'))
        else:
            content.appendChild(E.p(
                _('Time remaining in chapter: {}').format(_('Calculating…')),
                style='margin: 0.5ex 0'))

        if book_remaining_time is not None:
            content.appendChild(E.p(
                _('Time remaining in book: {}').format(format_time_left(book_remaining_time)),
                style='margin: 0.5ex 0'))
        else:
            content.appendChild(E.p(
                _('Time remaining in book: {}').format(_('Calculating…')),
                style='margin: 0.5ex 0'))

        if next_name:
            if next_chapter_time is not None:
                content.appendChild(E.p(
                    _('Time to read next chapter: {}').format(format_time_left(next_chapter_time)),
                    style='margin: 0.5ex 0'))
            else:
                content.appendChild(E.p(
                    _('Time to read next chapter: {}').format(_('Calculating…')),
                    style='margin: 0.5ex 0'))
        else:
            content.appendChild(E.p(_('This is the last chapter'), style='margin: 0.5ex 0'))

        container.appendChild(content)

        # Build bar graph of chapter reading times
        chapter_times = view.timers.chapter_times
        bar_data = []
        max_time = 0
        for i in range(spine.length):
            name = spine[i]
            length = book.manifest.files[name]?.length or 0
            is_current = i == current_idx
            if is_current:
                actual_t = chapter_times[name] or 0
                rem = view.timers.time_for(remaining_chars)
                t = actual_t + rem if rem is not None else view.timers.time_for(length)
                is_actual = actual_t > 0
            elif i < current_idx:
                actual_t = chapter_times[name]
                if actual_t and actual_t > 0:
                    t = actual_t
                    is_actual = True
                else:
                    t = view.timers.time_for(length)
                    is_actual = False
            else:
                t = view.timers.time_for(length)
                is_actual = False
            if t is not None and t > 0:
                max_time = Math.max(max_time, t)
            bar_data.push({'t': t, 'is_actual': is_actual, 'is_current': is_current, 'past': i < current_idx})

        BAR_H = 120
        BAR_W = 22
        BAR_GAP = 3
        COLOR_ACTUAL = '#27ae60'
        COLOR_CURRENT = '#e67e22'
        COLOR_ESTIMATED = '#7fb3d3'
        COLOR_PAST_EST = '#95a5a6'

        graph_section = E.div(style='padding: 0 1em 1em 1em')
        graph_section.appendChild(E.div(
            _('Chapter reading times'),
            style='font-size: smaller; font-weight: bold; margin-bottom: 0.5ex'))

        if max_time > 0:
            bars_row = E.div(
                style='display: flex; align-items: flex-end; overflow-x: auto; '
                    + f'border-bottom: 1px solid currentColor; min-height: {BAR_H + 4}px; padding-top: 4px')
            for i in range(bar_data.length):
                bd = bar_data[i]
                t = bd.t
                if t is not None and t > 0:
                    h = max(2, Math.round(t / max_time * BAR_H))
                else:
                    h = 2
                if bd.is_current:
                    color = COLOR_CURRENT
                elif bd.is_actual:
                    color = COLOR_ACTUAL
                elif bd.past:
                    color = COLOR_PAST_EST
                else:
                    color = COLOR_ESTIMATED
                outline = 'outline: 2px solid currentColor; outline-offset: 1px; ' if bd.is_current else ''
                label = str(i + 1)
                tip = _('Chapter {}').format(i + 1)
                if t and t > 0:
                    tip += ': ' + format_time_left(t)
                elif bd.is_current or bd.past:
                    tip += ' (' + _('no data') + ')'
                else:
                    tip += ' (' + _('Calculating…') + ')'
                bar = E.div(
                    style='display: inline-flex; flex-direction: column; align-items: center; '
                        + f'width: {BAR_W}px; flex-shrink: 0; margin-right: {BAR_GAP}px; cursor: default',
                    title=tip,
                    E.div(style=f'width: {BAR_W}px; height: {h}px; background-color: {color}; {outline}'),
                    E.div(label, style=f'font-size: 9px; text-align: center; width: {BAR_W}px; margin-top: 2px; overflow: hidden')
                )
                bars_row.appendChild(bar)
            graph_section.appendChild(bars_row)
            if current_idx >= 0:
                scroll_offset = max(0, current_idx * (BAR_W + BAR_GAP) - 80)
                window.setTimeout(def (): bars_row.scrollLeft=scroll_offset;, 0)
        else:
            graph_section.appendChild(E.div(
                _('Start reading to see chapter time statistics'),
                style='color: {}; font-style: italic'.format(get_color('faded-text'))))

        legend = E.div(
            style='display: flex; gap: 1em; margin-top: 0.5ex; font-size: smaller; flex-wrap: wrap',
            E.span(E.span('■ ', style=f'color: {COLOR_ACTUAL}'), _('Actual time')),
            E.span(E.span('■ ', style=f'color: {COLOR_CURRENT}'), _('Current chapter')),
            E.span(E.span('■ ', style=f'color: {COLOR_ESTIMATED}'), _('Estimated')),
            E.span(E.span('■ ', style=f'color: {COLOR_PAST_EST}'), _('Previous (estimated)')),
        )
        graph_section.appendChild(legend)
        container.appendChild(graph_section)
        container.appendChild(E.div(_(
            'For reading stats, a "chapter" is a single internal file inside the larger book. If you'
            ' see very few "chapters" above, then try converting your book to EPUB in calibre, that'
            ' will often automatically split up the book into per chapter files.'),
            style='padding: 0 1em 1em 1em; font-size: smaller'))
        container.appendChild(E.div(
            style='padding: 0 1em 1em 1em; display: flex; justify-content: flex-end',
            create_button(_('Reset statistics'), 'trash', action=self.reset_stats)
        ))
# }}}


class Overlay:
    def __init__(self, view):
        self.view = view
        self.handling_context_menu_event = None
        c = self.clear_container()
        c.addEventListener('click', self.container_clicked)
        c.addEventListener('contextmenu', self.oncontextmenu, {'passive': False})
        self.panels = []

    def oncontextmenu(self, evt):
        if default_context_menu_should_be_allowed(evt):
            return
        evt.preventDefault()
        self.handling_context_menu_event = evt
        self.handle_escape()
        self.handling_context_menu_event = None

    def clear_container(self):
        c = self.container
        colors = viewer_chrome_colors()
        clear(c)
        c.style.backgroundColor = 'transparent'
        c.style.color = colors.foreground
        if c.style.display is not 'block':
            c.style.display = 'block'
        return c

    @property
    def container(self):
        return document.getElementById('book-overlay')

    @property
    def is_visible(self):
        return self.container.style.display is not 'none'

    def update_visibility(self):
        if self.panels.length:
            self.container.style.display = 'block'
            self.view.overlay_visibility_changed(True)
        elif self.container.style.display is 'block':
            self.container.style.display = 'none'
            self.view.focus_iframe()
            self.view.overlay_visibility_changed(False)

    def container_clicked(self, evt):
        if self.panels.length and jstype(self.panels[-1].on_container_click) is 'function':
            self.panels[-1].on_container_click(evt)
        else:
            self.hide_current_panel()

    def show_loading_message(self, msg):
        if ui_operations.show_loading_message:
            ui_operations.show_loading_message(msg)
        else:
            lm = LoadingMessage(msg, self.view.current_color_scheme)
            self.panels.push(lm)
            self.show_current_panel()

    def hide_loading_message(self):
        if ui_operations.show_loading_message:
            ui_operations.show_loading_message(None)
        else:
            self.panels = [p for p in self.panels if not isinstance(p, LoadingMessage)]
            self.show_current_panel()

    def hide_current_panel(self):
        p = self.panels.pop()
        if p and callable(p.on_hide):
            p.on_hide()
        self.show_current_panel()

    def handle_escape(self):
        if self.panels.length:
            p = self.panels[-1]
            if not p.is_not_escapable:
                if p.handle_escape:
                    p.handle_escape()
                else:
                    self.hide_current_panel()
                return True
        return False

    def show_current_panel(self):
        if self.panels.length:
            c = self.clear_container()
            self.panels[-1].show(c)
        self.update_visibility()

    def show(self, elements):
        self.panels = [MainOverlay(self, elements)]
        self.show_current_panel()

    def hide(self):
        while self.panels.length:
            self.hide_current_panel()
        self.update_visibility()

    def delete_book(self):
        self.hide_current_panel()
        self.panels = [DeleteBook(self)]
        self.show_current_panel()

    def reload_book(self):
        self.hide_current_panel()
        self.panels = [DeleteBook(self, _('Are you sure you want to reload this book?'), 'refresh', _('Reload book'), True)]
        self.show_current_panel()

    def open_book(self, closeable):
        self.hide_current_panel()
        if jstype(closeable) is not 'boolean':
            closeable = True
        self.panels = [OpenBook(self, closeable)]
        self.show_current_panel()

    def sync_book(self):
        self.hide_current_panel()
        self.panels = [SyncBook(self)]
        self.show_current_panel()

    def show_toc(self):
        self.hide_current_panel()
        if runtime.is_standalone_viewer:
            ui_operations.toggle_toc()
            return
        self.panels.push(TOCOverlay(self))
        self.show_current_panel()

    def toggle_reference_mode(self):
        self.hide_current_panel()
        self.view.toggle_reference_mode()

    def show_bookmarks(self):
        self.hide_current_panel()
        if runtime.is_standalone_viewer:
            ui_operations.toggle_bookmarks()
            return

        def do_it(action, data):
            self.panels.push(TOCOverlay(self, create_bookmarks_panel.bind(None, self.view.annotations_manager, data), _('Bookmarks')))
            self.show_current_panel()
        self.view.get_current_cfi('show-bookmarks', do_it)

    def show_highlights(self):
        self.hide_current_panel()
        if ui_operations.toggle_highlights:
            self.hide()
            ui_operations.toggle_highlights()
            return
        self.panels.push(TOCOverlay(self, create_highlights_panel.bind(None, self.view.annotations_manager, self.hide_current_panel), _('Highlights')))
        self.show_current_panel()

    def show_profiles(self):
        self.hide_current_panel()
        self.panels.push(ProfilesOverlay(self))
        self.show_current_panel()

    def show_goto(self):
        self.hide_current_panel()
        self.panels.push(GotoOverlay(self))
        self.show_current_panel()

    def _build_metadata_panel(self):
        from_read_book = short_uuid()

        def show_metadata_overlay(mi, pathtoebook, calibre_book_in_library_url, lname, book_id, overlay, container):
            container_id = ensure_id(container)
            container.appendChild(E.div(class_=BD_CLASS_NAME, style='padding: 1ex 1em'))
            table = E.table(class_='metadata')

            def handle_message(msg):
                data = msg.data
                if data.from_read_book is not from_read_book:
                    return
                if data.type is 'edit_metadata_closed':
                    ui_operations.stop_waiting_for_messages_from(this)
                    self.hide_current_panel()
                    return
                if data.type is 'update_cached_book_metadata' and data.metadata:
                    if current_library_id() is data.library_id:
                        mi = data.metadata
                        book_id = int(data.book_id)
                        set_book_metadata(book_id, mi)
                        book = self.view.book
                        if book and book.key[0] is data.library_id and book.key[1] is book_id:
                            ui_operations.update_metadata(book, mi)

            if not runtime.is_standalone_viewer and lname:
                container.lastChild.appendChild(E.div(
                    style='text-align: right',
                    E.a(_('Edit the metadata for this book in the library'), class_='blue-link', onclick=def ():
                        container = document.getElementById(container_id)
                        clear(container)
                        container.appendChild(E.iframe(
                            style='position: absolute; left: 0; top: 0; width: 100vw; height: 100vh; border-width: 0',
                            src=query_as_href({
                            'from_read_book': from_read_book, 'library_id': lname, 'book_id': book_id + ''}, 'edit_metadata')
                        ))
                        w = container.lastChild.contentWindow
                        ui_operations.wait_for_messages_from(w, handle_message.bind(w))
                    )
                ))
            container.lastChild.appendChild(table)
            render_metadata(mi, table, None, f'html {{ font-size: {document.documentElement.style.fontSize} }}')
            for a in table.querySelectorAll('a[href]'):
                a.removeAttribute('href')
                a.removeAttribute('title')
                a.classList.remove('blue-link')
                a.style.color = 'currentColor'
                a.style.textDecorationLine = 'none'
            if pathtoebook:
                container.lastChild.appendChild(E.div(
                    style='margin-top: 1ex; padding-top: 1ex; border-top: solid 1px',
                    _('Path:'), ' ',
                    E.a(
                        href='javascript: void(0)',
                        class_='blue-link', title=_('Click to open the folder the book is in'),
                        onclick=def ():
                            ui_operations.show_book_folder()
                        ,
                        pathtoebook)
                    )
                )
            if calibre_book_in_library_url:
                container.lastChild.appendChild(E.div(
                    style='margin-top: 1ex; padding-top: 1ex;',
                    E.a(
                        href=calibre_book_in_library_url,
                        class_='blue-link', title=_('Click to see this book in the main calibre program book list'),
                        _('Show book in the main calibre program'))
                    )
                )

        book = self.view.book
        key = book.key or [None, 0]
        lname, book_id = key
        book_id = int(book_id or 0)
        panel = SimpleOverlay(
            self,
            show_metadata_overlay.bind(None, book.metadata, book.manifest.pathtoebook, book.calibre_book_in_library_url, lname, book_id),
            self.view.book.metadata.title
        )
        panel.from_read_book = from_read_book
        return panel

    def show_metadata(self):
        self.hide_current_panel()
        self.panels.push(self._build_metadata_panel())
        self.show_current_panel()

    def show_ask_for_location(self):
        self.hide_current_panel()
        self.panels.push(SimpleOverlay(
            self, create_location_overlay.bind(None, self.view.current_position_data, self.view.book), _('Go to location, position or reference…')))
        self.show_current_panel()

    def show_page_list(self):
        self.hide_current_panel()
        self.panels.push(SimpleOverlay(
            self, create_page_list_overlay.bind(None, self.view.book), _('Go to page')))
        self.show_current_panel()

    def show_search(self):
        self.hide()
        self.view.show_search()

    def show_reading_stats(self):
        self.hide_current_panel()
        self.panels.push(ReadingStatsOverlay(self))
        self.show_current_panel()

    def show_prefs(self):
        self.hide_current_panel()
        self.panels.push(PrefsOverlay(self))
        self.show_current_panel()

    def show_library(self):
        self.hide_current_panel()
        book = self.view.book
        show_panel('book_list', {'library_id': book.key[0], 'book_id': book.key[1] + ''}, replace=False)
        ui_operations.close_book()

    def show_book_page(self):
        self.hide_current_panel()
        self.view.open_book_page()
        ui_operations.close_book()

    def show_font_size_chooser(self):
        self.hide_current_panel()
        self.panels.push(FontSizeOverlay(self))
        self.show_current_panel()

    def show_word_actions(self, word):
        self.hide_current_panel()
        self.panels.push(WordActionsOverlay(word, self))
        self.show_current_panel()
