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

import traceback
from elementmaker import E
from gettext import gettext as _

from ajax import ajax, ajax_send, encode_query_component
from book_list.delete_book import refresh_after_delete, start_delete_book
from book_list.globals import get_session_data
from book_list.item_list import create_item, create_item_list, create_side_action
from book_list.library_data import (
    all_libraries, book_after, book_metadata, cover_url, current_library_id,
    current_virtual_library, download_url, library_data, load_status,
    set_book_metadata, download_data_file_url, web_search_url
)
from book_list.router import back, home, open_book, report_a_load_failure, show_note, open_book_url_in_library, search_url_in_library
from book_list.theme import (
    color_scheme, get_color, get_color_as_rgba
)
from book_list.top_bar import clear_buttons, create_top_bar, set_title
from book_list.ui import query_as_href, set_panel_handler, show_panel
from book_list.views import search_query_for
from date import format_date
from dom import add_extra_css, build_rule, clear, ensure_id, svgicon, unique_id
from file_uploads import update_status_widget, upload_files_widget, upload_status_widget
from modals import create_custom_dialog, error_dialog, warning_dialog, question_dialog
from read_book.touch import (
    copy_touch, install_handlers, interpret_single_gesture, touch_id, update_touch
)
from session import get_interface_data
from utils import (
    conditional_timeout, debounce, fmt_sidx, human_readable, parse_url_params,
    safe_set_inner_html, sandboxed_html
)
from widgets import create_button, create_spinner

bd_counter = 0

CLASS_NAME = 'book-details-panel'
SEARCH_INTERNET_CLASS = 'book-details-search-internet'
COPY_TO_LIBRARY_CLASS = 'book-details-copy-to-library'
DATA_FILES_CLASS = 'book-details-data-files'
FORMAT_PRIORITIES = [
    'EPUB', 'AZW3', 'DOCX', 'LIT', 'MOBI', 'ODT', 'RTF', 'MD', 'MARKDOWN', 'TXT', 'PDF'
]


def sort_formats_key(fmt):
    ans = FORMAT_PRIORITIES.indexOf(fmt)
    if ans < 0:
        ans = FORMAT_PRIORITIES.length
    return ans


def get_preferred_format(metadata, output_format, input_formats, for_download):
    formats = (metadata and metadata.formats) or []
    formats = [f.toUpperCase() for f in formats]
    fmt = 'EPUB' if output_format is 'PDF' else output_format
    if formats.length and formats.indexOf(fmt) is -1:
        found = False
        formats = sorted(formats, key=sort_formats_key)
        for q in formats:
            if input_formats[q]:
                fmt = q
                found = True
                break
        if for_download and not found and formats.length:
            fmt = formats[0]
    return fmt.toUpperCase()

IGNORED_FIELDS = {'title', 'sort', 'uuid', 'id', 'urls_from_identifiers', 'lang_names', 'last_modified', 'path'}
default_sort = {
    f: i + 1 for i, f in enumerate((
        'title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', \
        'timestamp', 'pubdate', 'identifiers', 'languages', 'publisher', 'last_modified')
)}
default_sort['formats'] = 999


def field_sorter(field_metadata):
    return def (field):
        lvl = '{:03d}'.format(default_sort[field] or 998)
        fm = (field_metadata[field] or {})[field] or {}
        return lvl + (fm.name or 'zzzzz')


def href_for_search(name, val, use_quotes=True):
    query = ('{}:"={}"' if use_quotes else '{}:{}').format(name, str.replace(val, '"', r'\"'))
    q = search_query_for(query)
    return query_as_href(q)


def on_fmt_click(ev):
    fmt = ev.currentTarget.dataset.format
    book_id = int(ev.currentTarget.dataset.bookId)
    title, sz = this

    create_custom_dialog(title, def (parent, close_modal):
        def action(which):
            close_modal()
            which(book_id, fmt)
        if fmt.lower() is 'pdf':
            read_button = download_url(book_id, fmt, 'inline')
        else:
            read_button = action.bind(None, read_format)
        parent.appendChild(E.div(
            E.div(_('What would you like to do with the {} format?').format(fmt)),
            E.div(class_='button-box',
                create_button(_('Read'), 'book', read_button),
                create_button(_('Download'), 'cloud-download', download_url(book_id, fmt),
                              _('File size: {}').format(human_readable(sz)),
                              download_filename=f'{title}.{fmt.toLowerCase()}')
            )
        ))
    )


def adjust_iframe_height(iframe):
    de = iframe.contentWindow.document.documentElement
    # scrollHeight is inaccurate on Firefox
    iframe.style.height = de.offsetHeight + 5 + 'px'
    iframe.dataset.last_window_width = window.innerWidth + ''
    return de


def setup_iframe(iframe):
    de = adjust_iframe_height(iframe)
    for a in de.querySelectorAll('a[href]'):
        a.setAttribute('target', '_parent')

    def forward_touch_events(ev):
        container = window.parent.document.getElementById(render_book.container_id)
        if container:
            dup = v'new ev.constructor(ev.type, ev)'
            container.dispatchEvent(dup)

    for key in ('start', 'move', 'end', 'cancel'):
        iframe.contentWindow.addEventListener(f'touch{key}', forward_touch_events)


def adjust_all_iframes(ev):
    for iframe in document.querySelectorAll(f'.{CLASS_NAME} iframe'):
        ww = parseInt(iframe.dataset.last_window_width)
        if ww is not window.innerWidth:
            adjust_iframe_height(iframe)


def add_stars_to(stars, val, allow_half_stars):
    for i in range(val // 2):
        stars.appendChild(svgicon('star'))
    if allow_half_stars and (val % 2):
        stars.appendChild(svgicon('star-half'))


if window?:
    window.addEventListener('resize', debounce(adjust_all_iframes, 250))


def hex_decode(lib):
    v'const uint8Array = new Uint8Array(lib.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));'
    v'lib = new TextDecoder("utf-8").decode(uint8Array);'
    return lib


def decode_calibre_url_library_id(lib):
    if lib.startswith('_hex_-'):
        lib = hex_decode(lib[6:])
    elif lib is '_':
        lib = current_library_id()
    return lib


def replace_calibre_links_to_books(html):
    parser = v'new DOMParser()'
    dom = parser.parseFromString(html, 'text/html')
    changed = False
    dom.body.querySelectorAll('a[href]').forEach(def (link):
        nonlocal changed
        if not link.href or not link.href.startswith('calibre://'):
            return
        url = v'new URL(link.href, window.location.origin)'
        if url.hostname is not 'show-book' and url.hostname is not 'view-book' and url.hostname is not 'search':
            return
        path = url.pathname
        parts = str.split(path, '/')
        lib, book_id = parts[1], parts[2]
        lib = decode_calibre_url_library_id(lib)
        if url.hostname is 'search':
            q = vl = ''
            if url.searchParams.get('eq'):
                q = hex_decode(url.searchParams.get('eq'))
            elif url.searchParams.get('q'):
                q = url.searchParams.get('q')
            if not q:
                return
            if url.searchParams.get('encoded_virtual_library'):
                vl = hex_decode(url.searchParams.get('encoded_virtual_library'))
            elif url.searchParams.get('virtual_library'):
                vl = url.searchParams.get('virtual_library')
            if vl is '_':
                vl = ''
            link.href = search_url_in_library(lib, q, vl)
            changed = True
        else:
            try:
                book_id = int(book_id)
            except:
                return
            if url.hostname is 'show-book':
                new_href = query_as_href({'book_id': book_id + '', 'library_id': lib}, 'book_details')
            else:
                fmt = parts[3]
                extra_query = {}
                url.searchParams.forEach(def (val, key):
                    if key is 'open_at':
                        key = 'bookpos'
                    extra_query[key] = val
                )
                new_href = open_book_url_in_library(lib, book_id, fmt, extra_query)
                if url.search:
                    new_href
            link.href = new_href
            changed = True
    )
    if changed:
        return v'new XMLSerializer().serializeToString(dom);'
    return html


def adjusting_sandboxed_html(html, extra_css):
    color = get_color_as_rgba('window-foreground')
    css = f'\n\n:root {{ color-scheme: {color_scheme()} }}\n\nhtml, body {{ overflow: hidden; color: rgba({color[0]}, {color[1]}, {color[2]}, {color[3]}) }}'
    if extra_css:
        css += '\n\n' + extra_css
    # allow-same-origin is needed for resizing and allow-top-navigation is
    # needed for links with target="_parent"
    ans = sandboxed_html(html, css, 'allow-same-origin allow-top-navigation-by-user-activation')
    ans.addEventListener('load', def (ev): setup_iframe(ev.target);)
    ans.style.height = '50vh'
    ans.dataset.last_window_width = '0'
    return ans


def render_metadata(mi, table, book_id, iframe_css):  # {{{
    field_metadata = library_data.field_metadata
    interface_data = get_interface_data()

    def allowed_fields(field):
        if field.endswith('_index'):
            fm = field_metadata[field[:-len('_index')]]
            if fm and fm.datatype is 'series':
                return False
        if field.startswith('#'):
            return True
        if field in IGNORED_FIELDS or field.endswith('_sort'):
            return False
        if mi[field] is undefined:
            return False
        return True

    fields = library_data.book_display_fields
    vertical_categories = {x: True for x in (library_data.book_details_vertical_categories or [])}
    if not fields or not fields.length or get_session_data().get('show_all_metadata'):
        if field_metadata:
            fields = sorted(filter(allowed_fields, mi), key=field_sorter(field_metadata))
        else:
            fields = filter(allowed_fields, fields)
    else:
        fields = filter(allowed_fields, fields)
    comments = []
    link_maps = mi.link_maps or v'{}'

    def show_note_action(field, item_id, item_val):
        show_note(field, item_id, item_val)

    def add_note_link(field, name, val, parent):
        if mi.items_with_notes and mi.items_with_notes[field] and mi.items_with_notes[field][val]:
            parent.appendChild(document.createTextNode(' '))
            parent.appendChild(E.a(
                svgicon('pencil'), title=_('Show notes for: {}').format(val), href='javascript:void(0)', onclick=show_note_action.bind(
                    None, field, mi.items_with_notes[field][val], val), class_='blue-link'))

    def add_row(field, name, val, is_searchable=False, is_html=False, join=None, search_text=None, use_quotes=True):
        if val is undefined or val is None:
            return
        is_vertical = vertical_categories[field]

        def add_val(v):
            if not v.appendChild:
                v += ''
            parent = table.lastChild.lastChild
            if is_searchable:
                websearch_link = False
                no_link = False
                if field is 'authors':
                    no_link = interface_data.default_author_link is '__NO_SEARCH_LINK__'
                    websearch_link = bool(interface_data.default_author_link) and interface_data.default_author_link is not 'search-calibre'
                else:
                    fm = field_metadata[field]
                    if fm?.display and fm.display.web_search_template and fm.display.web_search_template.partition(':')[0] in ('http', 'https'):
                        websearch_link = True
                text_rep = search_text or v
                target = '_self'
                if websearch_link and jstype(v) is not 'string':
                    websearch_link = False
                calibre_search_url = href_for_search(is_searchable, text_rep, use_quotes=use_quotes)
                calibre_search_tooltip = _('Click to see books with {0}: {1}').format(name, text_rep)
                if websearch_link:
                    url = web_search_url(book_id, field, v)
                    target = '_blank'
                    tooltip = _('Click to browse for {} on the web').format(text_rep)
                else:
                    url = calibre_search_url
                    tooltip = calibre_search_tooltip
                if no_link:
                    parent.appendChild(v if v.appendChild else document.createTextNode(v))
                else:
                    parent.appendChild(E.a(v, target=target, title=tooltip, class_='blue-link', href=url))
                if link_maps[field] and link_maps[field][text_rep]:
                    url = link_maps[field][text_rep]
                    if url.startswith('https://') or url.startswith('http://'):
                        parent.appendChild(document.createTextNode(' '))
                        parent.appendChild(E.a(
                            svgicon('external-link'), title=_('Click to open') + ': ' + url, href=url, target='_blank', class_='blue-link'))
                if websearch_link or no_link:
                    parent.appendChild(document.createTextNode(' '))
                    parent.appendChild(E.a(
                        svgicon('search'), title=calibre_search_tooltip, href=calibre_search_url, class_='blue-link'))
            else:
                parent.appendChild(v if v.appendChild else document.createTextNode(v))
            if jstype(v) is 'string' and not is_html:
                add_note_link(field, name, v, parent)

        table.appendChild(E.tr(E.td(name), E.td()))
        if is_html and /[<>]/.test(val + ''):
            table.lastChild.lastChild.appendChild(adjusting_sandboxed_html(val + '', iframe_css))
        else:
            if not join:
                add_val(val)
            else:
                for v in val:
                    add_val(v)
                    if v is not val[-1]:
                        if is_vertical:
                            table.lastChild.lastChild.appendChild(E.br())
                        else:
                            table.lastChild.lastChild.appendChild(document.createTextNode(join))
        return table.lastChild.lastChild

    def process_composite(field, fm, name, val):
        if fm.display and fm.display.contains_html:
            add_row(field, name, val, is_html=True)
            return
        if fm.is_multiple and fm.is_multiple.list_to_ui:
            all_vals = filter(None, map(str.strip, val.split(fm.is_multiple.list_to_ui)))
            add_row(field, name, all_vals, is_searchable=field, join=fm.is_multiple.list_to_ui)
        else:
            add_row(field, name, val, is_searchable=field)

    def process_authors(field, fm, name, val):
        add_row(field, name, val, is_searchable=field, join=' & ')

    def process_publisher(field, fm, name, val):
        add_row(field, name, val, is_searchable=field)

    def process_formats(field, fm, name, val):
        if val.length and book_id?:
            table.appendChild(E.tr(E.td(name), E.td()))
            td = table.lastChild.lastChild
            for fmt in val:
                fmt = fmt.toUpperCase()
                td.appendChild(E.a(
                    fmt, class_='blue-link', href='javascript:void(0)',
                    title=_('Read or download this book in the {} format').format(fmt),
                    onclick=on_fmt_click.bind([mi.title, mi.format_sizes[fmt] or 0]),
                    data_format=fmt, data_book_id='' + book_id))
                if fmt is not val[-1]:
                    td.appendChild(document.createTextNode(', '))

    def process_rating(field, fm, name, val):
        stars = E.span()
        val = int(val or 0)
        if val > 0:
            add_stars_to(stars, val, fm.display?.allow_half_stars)
            add_row(field, name, stars, is_searchable=field, search_text=val / 2 + '')

    def process_identifiers(field, fm, name, val):
        def ids_sorter(x):
            return (x[1] or '').toLowerCase()

        if val and mi.urls_from_identifiers and mi.urls_from_identifiers.length > 0:
            td = E.td()
            for _, id_name, id_value, url in sorted(mi.urls_from_identifiers or [], key=ids_sorter):
                if td.childNodes.length:
                    td.appendChild(document.createTextNode(', '))
                label = '{}:{}'.format(id_name or '', id_value or '')
                td.appendChild(E.a(class_='blue-link', title=label, target='_new', href=url, label))
            if td.childNodes.length:
                table.appendChild(E.tr(E.td(name), td))

    def process_size(field, fm, name, val):
        if val:
            try:
                add_row(field, name, human_readable(int(val)))
            except:
                add_row(field, name, val + '')

    def process_languages(field, fm, name, val):
        if val and val.length:
            table.appendChild(E.tr(E.td(name), E.td()))
            td = table.lastChild.lastChild
            for k in val:
                if mi.lang_names:
                    lang = mi.lang_names[k] or k
                else:
                    lang = k
                td.appendChild(E.a(lang,
                    class_='blue-link',
                    title=_('Click to see books with language: {}').format(lang),
                    href=href_for_search(field, k)
                ))
                if k is not val[-1]:
                    td.appendChild(document.createTextNode(', '))

    def process_datetime(field, fm, name, val):
        if val:
            fmt = interface_data['gui_' + field + '_display_format'] or (fm['display'] or {}).date_format
            formatted_val = format_date(val, fmt)
            if formatted_val:
                add_row(field, name, formatted_val, is_searchable=field, search_text=val)

    def process_series(field, fm, name, val):
        if val:
            ifield = field + '_index'
            try:
                ival = float(mi[ifield])
            except Exception:
                ival = 1.0
            ival = fmt_sidx(ival, use_roman=interface_data.use_roman_numerals_for_series_number)
            table.appendChild(E.tr(E.td(name), E.td()))
            s = safe_set_inner_html(E.span(), _('{0} of <a>{1}</a>').format(ival, val))
            a = s.getElementsByTagName('a')
            if a and a.length:
                a = a[0]
                a.setAttribute('href', href_for_search(field, val))
                a.setAttribute('title', _('Click to see books with {0}: {1}').format(name, val))
                a.setAttribute('class', 'blue-link')
            else:
                print('WARNING: Translation of series template is incorrect as it does not have an <a> tag')
            table.lastChild.lastChild.appendChild(s)
            add_note_link(field, name, val, table.lastChild.lastChild)

    def process_field(field, fm):
        name = fm.name or field
        datatype = fm.datatype
        val = mi[field]
        if field is 'comments' or datatype is 'comments' or fm.display?.composite_show_in_comments:
            if not val:
                return
            ias = fm.display?.interpret_as or 'html'
            hp = fm.display?.heading_position or 'hide'
            if ias is 'long-text':
                if hp is 'side':
                    add_row(field, name, val).style.whiteSpace = 'pre-wrap'
                    return
                val = E.pre(val, style='white-space:pre-wrap').outerHTML
            elif ias is 'short-text':
                if hp is 'side':
                    add_row(field, name, val)
                    return
                val = E.span(val).outerHTML
            else:
                if field is 'comments' and '<' not in val:
                    val = '\n'.join(['<p class="description">{}</p>'.format(x.replace(/\n/g, '<br>')) for x in val.split('\n\n')])
            if hp is 'side':
                add_row(field, name, val, is_html=True)
                return
            comments.push([field, val])
            return
        func = None
        if datatype is 'composite':
            func = process_composite
        elif field is 'formats':
            func = process_formats
        elif datatype is 'rating':
            func = process_rating
        elif field is 'identifiers':
            func = process_identifiers
        elif field is 'authors':
            func = process_authors
        elif field is 'publisher':
            func = process_publisher
        elif field is 'languages':
            func = process_languages
        elif field is 'size':
            func = process_size
        elif datatype is 'datetime':
            func = process_datetime
        elif datatype is 'series':
            func = process_series
        if func:
            func(field, fm, name, val)
        else:
            if datatype is 'text' or datatype is 'enumeration':
                if val is not undefined and val is not None:
                    join = fm.is_multiple.list_to_ui if fm.is_multiple else None
                    add_row(field, name, val, join=join, is_searchable=field)
            elif datatype is 'bool':
                if library_data.bools_are_tristate:
                    v = _('Yes') if val else ('' if val is undefined or val is None else _('No'))
                else:
                    v = _('Yes') if val else _('No')
                if v:
                    add_row(field, name, v, is_searchable=field, use_quotes=False)
            elif datatype is 'int' or datatype is 'float':
                if val is not undefined and val is not None:
                    fmt = (fm.display or {}).number_format
                    if fmt:
                        formatted_val = fmt.format(val)
                        if formatted_val:
                            val += ''
                            add_row(field, name, formatted_val, is_searchable=field, search_text=val)
                    else:
                        val += ''
                        add_row(field, name, val, is_searchable=field, search_text=val)

    for field in fields:
        fm = field_metadata[field]
        if not fm:
            continue
        try:
            process_field(field, fm)
        except Exception:
            print('Failed to render metadata field: ' + field)
            traceback.print_exc()

    all_html = ''
    for field, comment in comments:
        if comment:
            fm = field_metadata[field]
            if fm.display?.heading_position is 'above':
                name = fm.name or field
                all_html += f'<h3>{name}</h3>'
            all_html += comment
    iframe = adjusting_sandboxed_html(replace_calibre_links_to_books(all_html), iframe_css)
    iframe.style.marginTop = '2ex'
    table.parentNode.appendChild(iframe)
# }}}


def basic_table_rules(sel):
    style = ''
    style += build_rule(sel + 'table.metadata td', vertical_align='top')
    style += build_rule(
        sel + 'table.metadata td:first-of-type',
        font_style='normal',
        font_weight='700',
        color='var(--calibre-color-faded-text)',
        padding_right='0.5em',
        white_space='nowrap',
        text_align='right'
    )
    # Values column: prefer wrapping long tokens (paths, ids) on narrow screens
    style += build_rule(sel + 'table.metadata td:last-of-type', overflow_wrap='anywhere', word_break='break-word')
    return style


add_extra_css(def ():
    sel = '.' + CLASS_NAME + ' '
    style = basic_table_rules(sel)
    style += build_rule(sel + ' .next-book-button:hover', transform='scale(1.5)')
    # Modern layout for book details page
    style += build_rule(sel + '.cs-bd-wrap',
        padding='1rem',
        max_width='1100px',
        margin_left='auto',
        margin_right='auto'
    )
    style += build_rule(sel + '.cs-bd-initial-loading',
        min_height='min(70vh, 720px)',
        display='flex',
        align_items='center',
        justify_content='center'
    )
    style += build_rule(sel + '.cs-bd-initial-loading-card',
        width='min(820px, 100%)',
        padding='1.1rem 1.1rem',
        border='var(--calibre-border-style-subtle)',
        border_radius='18px',
        background_color='rgba(0,0,0,0.02)',
        box_shadow='0 10px 30px rgba(0,0,0,0.06)'
    )
    style += build_rule(sel + '.cs-bd-initial-loading-row',
        display='flex',
        align_items='center',
        justify_content='center',
        gap='0.6rem',
        opacity='0.9',
        font_weight='700'
    )
    style += build_rule(sel + '.cs-bd-initial-loading-sub',
        margin_top='0.55rem',
        text_align='center',
        opacity='0.65'
    )
    style += build_rule(sel + '.cs-bd-hero',
        display='grid',
        grid_template_columns='220px 1fr',
        gap='1.25rem',
        align_items='start',
        padding='1rem',
        border='var(--calibre-border-style-subtle)',
        border_radius='18px',
        background_color='rgba(0,0,0,0.02)',
        box_shadow='0 10px 30px rgba(0,0,0,0.06)'
    )
    style += build_rule(sel + '.cs-bd-cover',
        position='relative',
        width='220px'
    )
    style += build_rule(sel + '.cs-bd-cover img',
        width='100%',
        height='auto',
        display='block',
        border_radius='18px',
        box_shadow='0 18px 48px rgba(0,0,0,0.18)',
        border='var(--calibre-border-style-subtle)'
    )
    # Prev/next navigation is rendered below the details.
    style += build_rule(sel + '.cs-bd-nav',
        margin_top='1rem',
        display='flex',
        justify_content='space-between',
        gap='0.75rem'
    )
    style += build_rule(sel + '.cs-bd-nav a.calibre-push-button',
        flex='1 1 0',
        display='inline-flex',
        align_items='center',
        justify_content='center',
        gap='0.4rem'
    )
    style += r'''
@media (max-width: 720px) {
  .book-details-panel .cs-bd-nav { gap: 0.6rem; }
  .book-details-panel .cs-bd-nav a.calibre-push-button { justify-content: center; }
}
'''
    style += build_rule(sel + '.cs-bd-info', min_width='0')
    style += build_rule(sel + '.cs-bd-title',
        font_size='1.45em',
        font_weight='800',
        line_height='1.2',
        margin_bottom='0.35rem'
    )
    style += build_rule(sel + '.cs-bd-authors',
        opacity='0.85',
        font_size='1.05em',
        margin_bottom='0.75rem',
        overflow='hidden',
        text_overflow='ellipsis'
    )
    style += build_rule(sel + '.cs-bd-meta',
        display='flex',
        flex_wrap='wrap',
        gap='0.35rem 0.6rem',
        margin_bottom='0.9rem',
        opacity='0.9'
    )
    style += build_rule(sel + '.cs-bd-badge',
        display='inline-flex',
        align_items='center',
        gap='0.25rem',
        padding='0.15rem 0.45rem',
        border_radius='999px',
        border='var(--calibre-border-style-subtle)',
        background='rgba(0,0,0,0.02)',
        max_width='100%',
        white_space='nowrap',
        overflow='hidden',
        text_overflow='ellipsis'
    )
    style += build_rule(sel + '.cs-bd-actions',
        display='flex',
        flex_wrap='wrap',
        gap='0.5rem',
        margin_top='0.2rem'
    )
    style += build_rule(sel + '.cs-bd-details',
        margin_top='1rem',
        padding='1rem',
        border='var(--calibre-border-style-subtle)',
        border_radius='18px',
        background_color=get_color('window-background'),
        box_shadow='0 1px 2px rgba(0,0,0,0.06)',
        min_width='0',
        max_width='100%',
        box_sizing='border-box',
        overflow_x='hidden',
        overflow_wrap='break-word',
        word_break='break-word',
        text_align='left',
    )
    style += build_rule(sel + '.cs-bd-details table.metadata',
        width='100%',
        max_width='100%',
        table_layout='fixed',
        box_sizing='border-box',
    )
    style += build_rule(sel + '.cs-bd-details table.metadata td:first-of-type', text_align='left')
    style += build_rule(sel + '.cs-bd-details pre', white_space='pre-wrap', word_break='break-word', max_width='100%', overflow_x='auto')
    style += build_rule(sel + '.cs-bd-details code', word_break='break-word', overflow_wrap='anywhere')
    style += r'''
@media (max-width: 720px) {
  .book-details-panel .cs-bd-wrap { padding: 0.75rem; min-width: 0; }
  .book-details-panel .cs-bd-hero { grid-template-columns: 1fr; min-width: 0; }
  .book-details-panel .cs-bd-cover { width: min(320px, 100%); margin-left: auto; margin-right: auto; }
  .book-details-panel .cs-bd-title { font-size: 1.25em; }
  .book-details-panel .cs-bd-details { padding: 0.75rem; }
  .book-details-panel .cs-bd-details table.metadata td {
    white-space: normal;
    word-break: break-word;
    overflow-wrap: anywhere;
  }
  .book-details-panel .cs-bd-details table.metadata td:first-of-type {
    white-space: normal;
    text-align: left;
    padding-right: 0.35rem;
  }
}
'''
    # Loading overlay (prevents flicker when fetching metadata)
    style += build_rule('.' + CLASS_NAME + ' .cs-bd-loading-wrap', position='relative')
    style += build_rule('.' + CLASS_NAME + ' .cs-bd-loading-overlay',
        position='absolute', left='0', top='0', right='0', bottom='0',
        display='flex', align_items='center', justify_content='center',
        background_color='rgba(255,255,255,0.55)',
        opacity='0',
        pointer_events='none',
        transition='opacity 160ms ease'
    )
    style += build_rule('.' + CLASS_NAME + ' .cs-bd-loading .cs-bd-loading-overlay', opacity='1')
    style += build_rule('.' + CLASS_NAME + ' .cs-bd-loading .cs-bd-loading-content', opacity='0.45', transition='opacity 160ms ease')
    style += r'''
@keyframes cs_spin_bd { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.book-details-panel .cs-bd-loading-overlay svg { animation: cs_spin_bd 1.2s linear infinite; opacity: 0.85; }
'''
    sel = '.' + SEARCH_INTERNET_CLASS
    style += build_rule(sel, margin='1ex 1em')
    style += build_rule(sel + ' ul > li', list_style_type='none')
    style += build_rule(sel + ' ul > li > a', padding='2ex 1em', display='block', width='100%')
    sel = '.' + COPY_TO_LIBRARY_CLASS
    style += build_rule(sel, margin='1ex 1em')
    sel = '.' + DATA_FILES_CLASS
    style += build_rule(sel, margin='1ex 1em')
    return style
)

current_fetch = None


def no_book(container):
    container.appendChild(E.div(
        style='margin: 1ex 1em',
        _('No book found')
    ))


def on_img_err(err):
    img = err.target
    if img.parentNode:
        img.parentNode.style.display = 'none'


def preferred_format(book_id, for_download):
    interface_data = get_interface_data()
    return get_preferred_format(book_metadata(book_id), interface_data.output_format, interface_data.input_formats, for_download)


def read_format(book_id, fmt):
    open_book(book_id, fmt)


def read_book(book_id):
    fmt = preferred_format(book_id)
    if fmt and fmt.lower() is 'pdf':
        window.open(download_url(book_id, fmt, 'inline'), '_blank')
    else:
        read_format(book_id, fmt)


def download_format(book_id, fmt):
    window.location = download_url(book_id, fmt)


def download_book(book_id):
    fmt = preferred_format(book_id, True)
    download_format(book_id, fmt)


def next_book(book_id, delta):
    next_book_id = book_after(book_id, delta)
    if next_book_id:
        q = parse_url_params()
        q.book_id = next_book_id + ''
        show_panel('book_details', query=q, replace=True)


def render_book(container_id, book_id):
    render_book.book_id = book_id
    render_book.container_id = container_id
    is_random = parse_url_params().book_id is '0'
    c = document.getElementById(container_id)
    if not c:
        return
    install_touch_handlers(c, book_id)
    metadata = book_metadata(book_id)
    render_book.title = metadata.title
    set_title(c, metadata.title)
    authors = metadata.authors.join(' & ') if metadata.authors else _('Unknown')
    alt = _('{} by {}\n\nClick to read').format(metadata.title, authors)
    if is_random:
        pass
    imgdiv = E.div(class_='cs-bd-cover',
        E.img(
            alt=alt, title=alt, data_title=metadata.title, data_authors=authors,
            onclick=read_book.bind(None, book_id),
            style='cursor: pointer;'
        ),
    )
    img = imgdiv.getElementsByTagName('img')[0]
    img.onerror = on_img_err
    img.src = cover_url(book_id)
    c = c.lastChild
    wrap = E.div(class_='cs-bd-wrap')
    c.appendChild(wrap)

    # Hero: cover + basic info
    hero = E.div(class_='cs-bd-hero')
    wrap.appendChild(hero)
    hero.appendChild(imgdiv)
    info = E.div(class_='cs-bd-info', data_book_id='' + book_id)
    hero.appendChild(info)

    info.appendChild(E.div(metadata.title or _('Unknown'), class_='cs-bd-title'))
    info.appendChild(E.div(authors, class_='cs-bd-authors'))

    meta = E.div(class_='cs-bd-meta')
    info.appendChild(meta)

    def add_badge(icon, text, title=None):
        if not text:
            return
        b = E.span(class_='cs-bd-badge')
        if icon:
            b.appendChild(svgicon(icon))
        b.appendChild(E.span(text))
        if title:
            b.title = title
        meta.appendChild(b)

    # Rating, series, publisher, pubdate, formats (high-signal basics)
    if metadata.rating:
        stars = E.span(style='white-space:nowrap')
        add_stars_to(stars, int(metadata.rating or 0), True)
        add_badge(None, stars, _('Rating'))
    if metadata.series:
        try:
            ival = float(metadata.series_index)
        except Exception:
            ival = 1.0
        ival = fmt_sidx(ival, use_roman=get_interface_data().use_roman_numerals_for_series_number)
        add_badge('bookmark', _('{0} of {1}').format(ival, metadata.series), _('Series'))
    if metadata.publisher:
        add_badge('library', metadata.publisher, _('Publisher'))
    if metadata.pubdate:
        try:
            add_badge('date', format_date(metadata.pubdate, 'yyyy-MM-dd'), _('Published'))
        except Exception:
            pass
    if metadata.formats and metadata.formats.length:
        add_badge('book', metadata.formats.slice(0, 4).join(' · '), _('Formats'))

    actions = E.div(class_='cs-bd-actions')
    info.appendChild(actions)
    read_button = create_button(_('Read'), 'book', read_book.bind(None, book_id), _('Read this book [V]'))
    fmt = preferred_format(book_id, True)
    download_button = create_button(_('Download'), 'cloud-download', download_url(book_id, fmt),
        _('Download this book in the {0} format ({1})').format(fmt, human_readable(metadata.format_sizes[fmt] or 0)),
        download_filename=f'{metadata.title}.{fmt.toLowerCase()}')
    row = E.div(read_button, download_button)
    row.classList.add('cs-bd-actions')
    if not metadata.formats or not metadata.formats.length:
        row.style.display = 'none'
    # Put buttons into actions block (keeps hero tidy)
    actions.appendChild(row)

    # Secondary actions: Convert/Edit/Delete + More actions.
    # These are placed right below the Read/Download buttons, and "More actions"
    # opens a floating dialog (not a new page/panel).
    def show_more_actions_modal(ev=None):
        create_custom_dialog(_('More actions…'), def (parent, close_modal):
            if get_session_data().get('show_all_metadata'):
                title, subtitle = _('Show important metadata'), _('Show only the important metadata fields in the book details')
            else:
                title, subtitle = _('Show all metadata'), _('Show all metadata fields in the book details')
            items = [
                create_item(_('Search the internet'),
                    subtitle=_('Search for this author or book on various websites'),
                    action=def ():
                        close_modal()
                        show_subsequent_panel('search_internet', replace=True)
                ),
                create_item(title, subtitle=subtitle,
                    action=def ():
                        close_modal()
                        toggle_fields()
                ),
                create_item(_('Copy to library'),
                    subtitle=_('Copy or move this book to another calibre library'),
                    action=def ():
                        close_modal()
                        show_subsequent_panel('copy_to_library', replace=True)
                ),
                create_item(_('Manage data files'),
                    subtitle=_('Upload or download additional data files to this book'),
                    action=def ():
                        close_modal()
                        show_subsequent_panel('data_files', replace=True)
                ),
            ]
            wrapper = E.div()
            parent.appendChild(wrapper)
            create_item_list(wrapper, items)
        )

    row2 = E.div(style='flex-basis: 100%; width: 100%;')
    row2.classList.add('cs-bd-actions')

    def on_convert(ev):
        convert_book()

    def on_edit(ev):
        edit_metadata()

    def on_delete(ev):
        delete_book()

    def on_more(ev):
        show_more_actions_modal(ev)

    row2.appendChild(create_button(_('Convert'), 'convert', on_convert, _('Convert this book to another format [C]')))
    row2.appendChild(create_button(_('Edit'), 'edit', on_edit, _('Edit the metadata for this book [E]')))
    row2.appendChild(create_button(_('Delete'), 'trash', on_delete, _('Delete this book')))
    row2.appendChild(create_button('', 'ellipsis-v', on_more, _('More actions')))
    actions.appendChild(row2)

    # Details section (full metadata)
    details = E.div(class_='cs-bd-details')
    wrap.appendChild(details)
    md = E.div(style='margin-bottom: 0')
    table = E.table(class_='metadata')
    details.appendChild(md)
    md.appendChild(table)

    render_metadata(metadata, table, book_id)

    # Prev/next navigation should be below the details section.
    def make_nav_button(is_prev):
        delta = -1 if is_prev else 1
        nbid = book_after(book_id, delta)
        if not nbid or nbid is book_id or is_random:
            return None
        ans = create_button(
            _('Previous') if is_prev else _('Next'),
            'chevron-left' if is_prev else 'chevron-right',
            next_book.bind(None, book_id, delta),
            _('Previous book [Ctrl+Left]') if is_prev else _('Next book [Ctrl+Right]')
        )
        if not is_prev:
            ans.insertBefore(ans.firstChild.nextSibling, ans.firstChild)
        ans.style.justifyContent = 'start' if is_prev else 'end'
        return ans

    prev_btn = make_nav_button(True)
    next_btn = make_nav_button(False)
    if prev_btn or next_btn:
        nav = E.div(class_='cs-bd-nav')
        nav.appendChild(prev_btn or E.span())
        nav.appendChild(next_btn or E.span())
        wrap.appendChild(nav)


def add_top_bar_buttons(container_id):
    container = document.getElementById(container_id)
    if container:
        clear_buttons(container)
# Book actions are rendered in the hero section (below Read/Download).


def proceed_after_succesful_fetch_metadata(container_id, book_id):
    render_book(container_id, book_id)
    add_top_bar_buttons(container_id)


def metadata_fetched(container_id, book_id, proceed, end_type, xhr, event):
    nonlocal current_fetch
    if current_fetch is None or current_fetch is not xhr:
        return  # Fetching was aborted
    current_fetch = None
    c = document.getElementById(container_id)
    if not c:
        return
    c = c.lastChild
    # Always cleanup transient loading UI so it never leaves layout artifacts
    # (e.g. empty lines) after the request completes or is aborted.
    try:
        c.classList.remove('cs-bd-loading')
        c.classList.remove('cs-bd-loading-wrap')
    except Exception:
        pass
    try:
        overlay = c.querySelector('.cs-bd-loading-overlay')
        if overlay and overlay.parentNode:
            overlay.parentNode.removeChild(overlay)
    except Exception:
        pass
    try:
        content = c.querySelector('.cs-bd-loading-content')
        if content and content.parentNode:
            # If we created a wrapper to fade existing content, unwrap it now.
            while content.firstChild:
                c.insertBefore(content.firstChild, content)
            content.parentNode.removeChild(content)
    except Exception:
        pass
    if end_type is 'load':
        try:
            data = JSON.parse(xhr.responseText)
        except Exception as err:
            error_dialog(_('Could not fetch metadata for book'), _('Server returned an invalid response'), err.toString())
            return
        clear(c)
        book_id = int(data['id'])
        set_book_metadata(book_id, data)
        proceed(container_id, book_id)
    elif end_type is not 'abort':
        clear(c)
        c.appendChild(E.div(
            style='margin: 1ex 1em',
            _('Could not fetch metadata for book'),
            E.div(style='margin: 1ex 1em')
        ))
        safe_set_inner_html(c.lastChild.lastChild, xhr.error_html)


def fetch_metadata(container_id, book_id, proceed):
    nonlocal current_fetch
    container = document.getElementById(container_id)
    if not container:
        return
    if current_fetch:
        current_fetch.abort()
    current_fetch = ajax('interface-data/book-metadata/' + book_id, metadata_fetched.bind(None, container_id, book_id, proceed),
                         query={'library_id': current_library_id(), 'vl': current_virtual_library()})
    current_fetch.send()
    body = container.lastChild
    # If we already have content, do not clear; show an overlay to prevent flicker.
    if body and body.firstChild:
        body.classList.add('cs-bd-loading-wrap')
        body.classList.add('cs-bd-loading')
        # Ensure content wrapper exists so we can fade it
        content = body.querySelector('.cs-bd-loading-content')
        if not content:
            content = E.div(class_='cs-bd-loading-content')
            # Move existing children into content wrapper
            while body.firstChild:
                content.appendChild(body.firstChild)
            body.appendChild(content)
        overlay = body.querySelector('.cs-bd-loading-overlay')
        if not overlay:
            overlay = E.div(class_='cs-bd-loading-overlay', svgicon('cog'), E.span('\xa0' + _('Fetching metadata…')))
            body.appendChild(overlay)
    else:
        clear(body)
        body.classList.add('cs-bd-loading-wrap')
        wrap = E.div(class_='cs-bd-wrap cs-bd-initial-loading')
        card = E.div(class_='cs-bd-initial-loading-card')
        card.appendChild(E.div(class_='cs-bd-initial-loading-row',
            create_spinner(),
            E.span(_('Fetching metadata…'))
        ))
        card.appendChild(E.div(class_='cs-bd-initial-loading-sub', _('Fetching metadata for the book, please wait') + '…'))
        wrap.appendChild(card)
        body.appendChild(wrap)


def install_touch_handlers(container, book_id):
    ongoing_touches = {}
    gesture_id = 0
    container_id = container.id

    def has_active_touches():
        for tid in ongoing_touches:
            t = ongoing_touches[tid]
            if t.active:
                return True
        return False

    def handle_touch(ev):
        nonlocal gesture_id, ongoing_touches
        container = document.getElementById(container_id)
        if not container:
            return
        if ev.type is 'touchstart':
            for touch in ev.changedTouches:
                ongoing_touches[touch_id(touch)] = copy_touch(touch)
                gesture_id += 1
        elif ev.type is 'touchmove':
            for touch in ev.changedTouches:
                t = ongoing_touches[touch_id(touch)]
                if t:
                    update_touch(t, touch)
        elif ev.type is 'touchcancel':
            for touch in ev.changedTouches:
                v'delete ongoing_touches[touch_id(touch)]'
        elif ev.type is 'touchend':
            for touch in ev.changedTouches:
                t = ongoing_touches[touch_id(touch)]
                if t:
                    t.active = False
                    update_touch(t, touch)
                    if not has_active_touches():
                        touches = ongoing_touches
                        ongoing_touches = {}
                        num = len(touches)
                        if num is 1:
                            gesture = interpret_single_gesture(touches[Object.keys(touches)[0]], gesture_id)
                            if gesture.type is 'swipe' and gesture.axis is 'horizontal':
                                delta = -1 if gesture.direction is 'right' else 1
                                next_book(book_id, delta)

    install_handlers(container, {
        'handle_touchstart': handle_touch,
        'handle_touchmove': handle_touch,
        'handle_touchend': handle_touch,
        'handle_touchcancel': handle_touch,
    }, True)


def create_book_details(container):
    q = parse_url_params()
    current_book_id = q.book_id
    if current_book_id is undefined or current_book_id is None:
        no_book(container)
        return
    current_book_id = int(current_book_id)
    container_id = container.parentNode.id
    if current_book_id is not 0 and book_metadata(current_book_id):
        render_book(container_id, current_book_id)
        add_top_bar_buttons(container_id)
    else:
        fetch_metadata(container_id, current_book_id, proceed_after_succesful_fetch_metadata)


def report_load_failure(container):
    report_a_load_failure(
        container, _('Failed to load books from calibre library, with error:'),
        load_status.error_html)


def check_for_books_loaded():
    container = this
    if load_status.loading:
        conditional_timeout(container.id, 5, check_for_books_loaded)
        return
    container = container.lastChild
    clear(container)
    if not load_status.ok:
        report_load_failure(container)
        return
    create_book_details(container)


def onkeydown(container_id, close_action, ev):
    if render_book.book_id:
        if ev.ctrlKey:
            if ev.key is 'ArrowLeft':
                next_book(render_book.book_id, -1)
                ev.preventDefault(), ev.stopPropagation()
            elif ev.key is 'ArrowRight':
                next_book(render_book.book_id, 1)
                ev.preventDefault(), ev.stopPropagation()
        elif not ev.ctrlKey and not ev.metaKey and not ev.shiftKey:
            if ev.key is 'Escape':
                ev.preventDefault(), ev.stopPropagation()
                close_action()
            elif ev.key is 'Delete':
                ev.preventDefault(), ev.stopPropagation()
                delete_book()
            elif ev.key is 'v' or ev.key is 'V':
                read_book(render_book.book_id)
            elif ev.key is 'c' or ev.key is 'C':
                convert_book()
            elif ev.key is 'e' or ev.key is 'E':
                edit_metadata()


def init(container_id):
    container = document.getElementById(container_id)
    clear(container)
    close_action, close_icon = back, 'close'
    q = parse_url_params()
    ca = q.close_action
    if ca is 'home':
        close_action, close_icon = def (): home();, 'home'
    elif ca is 'book_list':
        close_action = def ():
            show_panel('book_list', {'book_id': q.book_id})
    create_top_bar(container, title='', action=close_action, icon=close_icon)
    window.scrollTo(0, 0)  # Ensure we are at the top of the window
    container.appendChild(E.div(class_=CLASS_NAME, tabindex='0'))
    container.lastChild.addEventListener('keydown', onkeydown.bind(None, container_id, close_action), {'passive': False, 'capture': True})
    container.lastChild.focus()
    container.lastChild.appendChild(E.div(_('Loading books from the calibre library, please wait...'), style='margin: 1ex 1em'))
    conditional_timeout(container_id, 5, check_for_books_loaded)


def show_subsequent_panel(name, replace=False):
    q = parse_url_params()
    q.book_id = (render_book.book_id or q.book_id) + ''
    show_panel('book_details^' + name, query=q, replace=replace)


def edit_metadata():
    q = parse_url_params()
    q.book_id = (render_book.book_id or q.book_id) + ''
    show_panel('edit_metadata', query=q, replace=False)


def convert_book():
    q = parse_url_params()
    q.book_id = (render_book.book_id or q.book_id) + ''
    show_panel('convert_book', query=q, replace=False)


def create_more_actions_panel(container_id):
    container = document.getElementById(container_id)
    clear(container)
    create_top_bar(container, title=_('More actions…'), icon='back')
    if get_session_data().get('show_all_metadata'):
        title, subtitle = _('Show important metadata'), _('Show only the important metadata fields in the book details')
    else:
        title, subtitle = _('Show all metadata'), _('Show all metadata fields in the book details')
    items = [
        create_item(_('Search the internet'), subtitle=_('Search for this author or book on various websites'),
                    action=def ():
                        show_subsequent_panel('search_internet', replace=True)
        ),
        create_item(title, subtitle=subtitle, action=toggle_fields),
        create_item(_('Copy to library'), subtitle=_('Copy or move this book to another calibre library'),
                    action=def ():
                        show_subsequent_panel('copy_to_library', replace=True)
        ),
        create_item(_('Manage data files'), subtitle=_('Upload or download additional data files to this book'),
                    action=def ():
                        show_subsequent_panel('data_files', replace=True)
        ),
    ]
    container.appendChild(E.div())
    create_item_list(container.lastChild, items)


def return_to_book_details():
    q = parse_url_params()
    show_panel('book_details', query=q, replace=True)


def toggle_fields():
    sd = get_session_data()
    sd.set('show_all_metadata', False if sd.get('show_all_metadata') else True)
    return_to_book_details()


def url_for(template, data):
    def eqc(x):
        return encode_query_component(x).replace(/%20/g, '+')
    return template.format(title=eqc(data.title), author=eqc(data.author))


EXTENSIONS_TO_OPEN_IN_BROWSER = {
    'pdf': True,
    'jpeg': True,
    'jpg': True,
    'png': True,
    'gif': True,
    'webp': True,
    'txt': True,
    'html': True,
    'htm': True,
    'xhtml': True,
}


def download_data_file(book_id, relpath):
    ext = relpath.toLowerCase().rpartition('.')[2]
    open_inline = bool(EXTENSIONS_TO_OPEN_IN_BROWSER[ext])
    cd = 'inline' if open_inline else 'attachment'
    url = download_data_file_url(book_id, relpath, cd)
    if open_inline:
        window.open(url)
    else:
        window.location = url


def on_progress(container_id, book_id, loaded, total, xhr):
    container = document.getElementById(container_id)
    if container and total:
        update_status_widget(container, loaded, total)


def data_files_submitted(container_id, book_id, end_type, xhr, ev):
    if end_type is 'abort':
        back()
        return
    if end_type is not 'load':
        error_dialog(_('Failed to upload files to server'), _(
            'Uploading data files for book: {} failed.').format(book_id), xhr.error_html)
        return
    try:
        res = JSON.parse(xhr.responseText)
    except Exception as err:
        error_dialog(_('Could not upload data files for book'), _('Server returned an invalid response'), err.toString())
        return
    mi = book_metadata(book_id)
    if mi:
        mi.data_files = res.data_files
    if res.error:
        error_dialog(_('Could not upload data files for book'), _('There was an error: ') + res.error)
    else:
        back()


def submit_data_files(top_container_id, container_id, book_id, added):
    c = document.getElementById(container_id)
    clear(c)
    c.appendChild(E.div(style='margin: 1ex 1rem', _('Uploading changes to server, please wait...')))
    w = upload_status_widget()
    c.appendChild(w)
    ajax_send(
        f'data-files/upload/{book_id}/{current_library_id()}', added,
        data_files_submitted.bind(None, container_id, book_id), on_progress.bind(None, container_id, book_id))


def files_added(check_existing, top_container_id, book_id, container_id, files):
    container = document.getElementById(container_id)
    mi = book_metadata(book_id)
    if not container or not mi or not files[0]:
        return
    added = []

    def exists(fname):
        if mi.data_files:
            for relpath in mi.data_files:
                q = relpath.partition('/')[2]
                if q is fname:
                    return True
        return False

    if check_existing:
        existing = [file.name for file in files if exists(file.name)]
        if existing.length:
            return question_dialog(
                _('Replace existing data files?'),
                _('The following data files already exist, are you sure you want to replace them?') + ' ' + existing.join(', '),
                def (yes):
                    if yes:
                        files_added(False, top_container_id, book_id, container_id, files)
                    else:
                        back()
            )

    for file in files:
        data = {'name': file.name, 'size': file.size, 'type': file.type, 'data_url': None}
        added.push(data)
        r = FileReader()
        r.onload = def (evt):
            data.data_url = evt.target.result
            for entry in added:
                if not entry.data_url:
                    return
            submit_data_files(top_container_id, container_id, book_id, added)
        r.readAsDataURL(file)


def upload_data_file(container_id):
    if not render_book.book_id or not book_metadata(render_book.book_id):
        return return_to_book_details()
    book_id = render_book.book_id
    container = document.getElementById(container_id)
    clear(container)
    create_top_bar(container, title=_('Upload data files'), icon='back')
    div = E.div(id=unique_id())
    container.appendChild(div)
    upload_files_widget(div, files_added.bind(None, True, container_id, book_id), _(
        'Upload files by <a>selecting the files</a> or drag and drop of the files here.'),
        single_file=False)


def rebuild_data_files_list(container_id, book_id, relpath_being_deleted):
    def delete_data_file(relpath, fname, ev):
        question_dialog(_('Are you sure?'), _('Do you want to permanently delete the data file {0} from the book {1}?').format(
            fname, mi.title), def (yes):
                if yes:
                    data = [relpath]
                    ajax_send(f'data-files/remove/{book_id}/{current_library_id()}',
                        data, data_file_deleted.bind(None, container_id, book_id, relpath))
                    rebuild_data_files_list(container_id, book_id, relpath_being_deleted)
        )

    def ddf(relpath, fname, ev):
        download_data_file(book_id, relpath)

    def upload_data_file(ev):
        show_subsequent_panel('upload_data_file', replace=False)

    container = document.getElementById(container_id)
    mi = book_metadata(book_id)
    if not container or not mi:
        return
    container = container.querySelector('[data-component=data-files-list-container]')
    clear(container)
    items = [create_item(_('Upload new data file'), icon='plus', action=upload_data_file)]
    if mi.data_files:
        fname_map = {relpath: relpath.partition('/')[2] for relpath in mi.data_files}
        df = sorted(Object.keys(fname_map), key=def (x): return fname_map[x].toLowerCase();)
        for relpath in df:
            fname = fname_map[relpath]
            being_deleted = relpath is relpath_being_deleted
            subtitle = None
            side_actions = []
            if being_deleted:
                subtitle = _('This file is being deleted')
            else:
                side_actions.push(create_side_action(
                    'trash', tooltip=_('Delete the file: {}').format(fname), action=delete_data_file.bind(None, relpath, fname)
                ))
            items.push(create_item(fname, icon='cloud-download', subtitle=subtitle, side_actions=side_actions, action=ddf.bind(None, relpath, fname)))
    create_item_list(container, items)


def data_file_deleted(container_id, book_id, relpath, end_type, xhr, ev):
    if end_type is 'abort':
        back()
        return
    if end_type is not 'load':
        error_dialog(_('Failed to delete data file from server'), _(
            'Deleting data file for book: {} failed.').format(book_id), xhr.error_html)
        return
    try:
        res = JSON.parse(xhr.responseText)
    except Exception as err:
        error_dialog(_('Could not delete data file for book'), _('Server returned an invalid response'), err.toString())
        return
    mi = book_metadata(book_id)
    if mi:
        mi.data_files = res.data_files
    if res.errors:
        error_dialog(_('Failed to delete data file'), _('Failed to delete data file: {}').format(relpath), res.errors[relpath])
    rebuild_data_files_list(container_id, book_id)


def data_files(container_id):
    if not render_book.book_id or not book_metadata(render_book.book_id):
        return return_to_book_details()
    book_id = render_book.book_id

    container = document.getElementById(container_id)
    clear(container)
    create_top_bar(container, title=_('Manage data files'), icon='back')
    container.appendChild(E.div(class_=DATA_FILES_CLASS))
    container = container.lastChild
    container.appendChild(E.div(data_component='data-files-list-container'))
    rebuild_data_files_list(container_id, book_id)


def search_internet(container_id):
    if not render_book.book_id or not book_metadata(render_book.book_id):
        return return_to_book_details()
    container = document.getElementById(container_id)
    clear(container)
    create_top_bar(container, title=_('Search the internet'), icon='back')
    mi = book_metadata(render_book.book_id)
    data = {'title': mi.title, 'author': mi.authors[0] if mi.authors else _('Unknown')}
    interface_data = get_interface_data()

    def link_for(name, template):
        return E.a(name, class_='simple-link', href=url_for(template, data), target='_blank')

    author_links = E.ul()
    book_links = E.ul()

    if interface_data.search_the_net_urls:
        for entry in interface_data.search_the_net_urls:
            links = book_links if entry.type is 'book' else author_links
            links.appendChild(E.li(link_for(entry.name, entry.url)))
    for name, url in [
        (_('Goodreads'), 'https://www.goodreads.com/book/author/{author}'),
        (_('Wikipedia'), 'https://en.wikipedia.org/w/index.php?search={author}'),
        (_('Google Books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22'),
    ]:
        author_links.appendChild(E.li(link_for(name, url)))

    for name, url in [
        (_('Goodreads'), 'https://www.goodreads.com/search?q={author}+{title}&search%5Bsource%5D=goodreads&search_type=books&tab=books'),
        (_('Google Books'), 'https://www.google.com/search?tbm=bks&q=inauthor:%22{author}%22+intitle:%22{title}%22'),
        (_('Amazon'), 'https://www.amazon.com/s/ref=nb_sb_noss?url=search-alias%3Dstripbooks&field-keywords={author}+{title}'),
    ]:
        book_links.appendChild(E.li(link_for(name, url)))

    container.appendChild(E.div(class_=SEARCH_INTERNET_CLASS,
        safe_set_inner_html(E.h2(), _('Search for the author <i>{}</i> at:').format(data.author)),
        author_links,
        E.hr(),
        safe_set_inner_html(E.h2(), _('Search for the book <i>{}</i> at:').format(data.title)),
        book_links,
    ))


# Copy to library {{{


def do_copy_to_library(book_id, target_library_id, target_library_name):
    def handle_result(move, close_func, end_type, xhr, ev):
        close_func()
        title = book_metadata(book_id).title
        return_to_book_details()
        if end_type is 'abort':
            return
        if end_type is not 'load':
            error_dialog(_('Failed to copy book'), _(
                'Failed to copy the book "{}". Click "Show details" for more information.').format(title),
                xhr.error_html)
            return
        try:
            response = JSON.parse(xhr.responseText)[book_id]
            if not response:
                raise Exception('bad')
        except:
            error_dialog(_('Failed to copy book'), _(
                'Failed to copy the book "{}" because of an invalid response from calibre').format(title))
            return
        if not response.ok:
            error_dialog(_('Failed to copy book'), _(
                'Failed to copy the book "{}". Click "Show details" for more information.').format(title),
                response.payload
            )
            return
        payload = response.payload
        if payload.action is 'duplicate':
            warning_dialog(_('Book already exists'), _(
                'Could not copy as a book with the same title and authors already exists in the {} library').format(target_library_name))

        elif payload.action is 'automerge':
            warning_dialog(_('Book merged'), _(
                'The files from the book were merged into a book with the same title and authors in the {} library').format(target_library_name))
        if move and payload.action in ('add', 'automerge'):
            refresh_after_delete(book_id, current_library_id())

    def trigger_copy(container_id, move, close_func):
        try:
            choice = document.querySelector(f'#{dupes_id} input[name="dupes"]:checked').value
        except:
            choice = document.querySelector(f'#{dupes_id} input[name="dupes"]').value
        sd.set('copy_to_library_dupes', choice)
        duplicate_action, automerge_action = choice.split(';', 2)

        data = {'book_ids': [book_id], 'move': move, 'duplicate_action': duplicate_action, 'automerge_action': automerge_action}
        container = document.getElementById(container_id)
        clear(container)
        container.appendChild(E.div(
            _('Contacting calibre to copy book, please wait...')))
        ajax_send(f'cdb/copy-to-library/{target_library_id}/{current_library_id()}',
                  data, handle_result.bind(None, move, close_func))

    def radio(value, text):
        return E.div(style='margin-top: 1rem', E.input(type='radio', name='dupes', value=value, checked=value is saved_value), '\xa0', E.span(text))

    title = book_metadata(book_id).title
    dupes_id = unique_id()
    sd = get_session_data()
    saved_value = sd.get('copy_to_library_dupes')
    create_custom_dialog(_(
        'Copy book to "{target_library_name}"').format(target_library_name=target_library_name),
    def (container, close_func):
        container_id = ensure_id(container)
        container.appendChild(E.div(
            E.div(_('Copying: {}').format(title)),
            E.div(id=dupes_id,
                E.p(_('If there are already existing books in "{}" with the same title and authors,'
                      ' how would you like to handle them?').format(target_library_name)),
                radio('add;overwrite', _('Copy anyway')),
                radio('ignore;overwrite', _('Do not copy')),
                radio('add_formats_to_existing;overwrite', _('Merge into existing books, overwriting existing files')),
                radio('add_formats_to_existing;ignore', _('Merge into existing books, keeping existing files')),
                radio('add_formats_to_existing;new record', _('Merge into existing books, putting conflicting files into a new book record')),
            ),
            E.div(
                class_='button-box',
                create_button(_('Cancel'), None, close_func),
                create_button(_('Move'), None, trigger_copy.bind(None, container_id, True, close_func)),
                create_button(_('Copy'), None, trigger_copy.bind(None, container_id, False, close_func), highlight=True),
            )
        ))
        if not container.querySelector(f'#{dupes_id} input[name="dupes"]:checked'):
            container.querySelector(f'#{dupes_id} input[name="dupes"]').checked = True
    )


def copy_to_library(container_id):
    if not render_book.book_id or not book_metadata(render_book.book_id):
        return return_to_book_details()
    container = document.getElementById(container_id)
    clear(container)
    create_top_bar(container, title=_('Copy to library'), icon='back')
    libraries = all_libraries()
    container.appendChild(E.div(class_=COPY_TO_LIBRARY_CLASS))
    container = container.lastChild
    if libraries.length < 2:
        container.appendChild(E.div(_('There are no other calibre libraries available to copy the book to')))
        return
    container.appendChild(E.h2(_('Choose the library to copy to below')))
    items = []
    for library_id, library_name in libraries:
        if library_id is current_library_id():
            continue
        items.push(create_item(library_name, action=do_copy_to_library.bind(None, render_book.book_id, library_id, library_name)))
    container.appendChild(E.div())
    create_item_list(container.lastChild, items)
# }}}


def delete_book():
    start_delete_book(current_library_id(), render_book.book_id, render_book.title or _('Unknown'))


def set_panel_handlers():
    set_panel_handler('book_details', init)
    set_panel_handler('book_details^more_actions', create_more_actions_panel)
    set_panel_handler('book_details^search_internet', search_internet)
    set_panel_handler('book_details^copy_to_library', copy_to_library)
    set_panel_handler('book_details^data_files', data_files)
    set_panel_handler('book_details^upload_data_file', upload_data_file)
