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

from elementmaker import E

import traceback
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
)
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, get_font_size
)
from book_list.top_bar import add_button, 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 gettext import gettext as _
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 v'[]'
    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),
                '\xa0',
                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 v'[]')}
    if not fields or not fields.length or get_session_data().get('show_all_metadata'):
        fields = sorted(filter(allowed_fields, mi), key=field_sorter(field_metadata))
    else:
        fields = filter(allowed_fields, fields)
    comments = v'[]'
    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:
                text_rep = search_text or v
                parent.appendChild(E.a(
                    v,
                    title=_('Click to see books with {0}: {1}').format(name, text_rep), class_='blue-link',
                    href=href_for_search(is_searchable, text_rep, use_quotes=use_quotes)
                ))
                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='_new', class_='blue-link'))
            else:
                if v.appendChild:
                    parent.appendChild(v)
                else:
                    parent.appendChild(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(v'[mi.title, mi.format_sizes[fmt] || 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[0] or '').toLowerCase()

        if val and mi.urls_from_identifiers and mi.urls_from_identifiers.length > 0:
            td = E.td()
            for text, k, idval, url in sorted(mi.urls_from_identifiers or v'[]', key=ids_sorter):
                if td.childNodes.length:
                    td.appendChild(document.createTextNode(', '))
                td.appendChild(E.a(class_='blue-link', title='{}:{}'.format(k, idval), target='_new', href=url, text))
            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(v'[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='italic', color='var(--calibre-color-faded-text)', padding_right='0.5em', white_space='nowrap', text_align='right')
    style += build_rule(sel + 'table.metadata td:last-of-type', overflow_wrap='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)')

    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)
    border_radius = 20
    button_style = f'cursor: pointer; border-radius: {border_radius//5}px;'
    button_style += 'color: #ccc; background-color:rgba(0, 0, 0, 0.75);'
    button_style += 'display: flex; justify-content: center; align-items: center; padding: 2px;'
    if is_random:
        button_style += 'display: none;'

    def prev_next_button(is_prev):
        delta = -1 if is_prev else 1
        nbid = book_after(book_id, delta)
        s = button_style
        if not nbid or nbid is book_id:
            s += '; display: none'
        return E.div(
            style=s,
            title=_('Previous book [Ctrl+Left]') if is_prev else _('Next book [Ctrl+Right]'),
            class_='next-book-button',
            svgicon('chevron-left' if is_prev else 'chevron-right'),
            onclick=next_book.bind(None, book_id, delta)
        )

    imgdiv = E.div(style='position: relative',
        E.img(
            alt=alt, title=alt, data_title=metadata.title, data_authors=authors,
            onclick=read_book.bind(None, book_id),
            style='cursor: pointer; border-radius: 20px; max-width: calc(100vw - 2em); max-height: calc(100vh - 4ex - {}); display: block; width:auto; height:auto; border-radius: {}px'.format(get_font_size('title'), border_radius
        )),
        E.div(
            style=f'position: absolute; top:0; width: 100%; padding: {border_radius//2}px; box-sizing: border-box; font-size: 2rem; display: flex; justify-content: space-between; color: {get_color("button-text")}',
            prev_next_button(True),
            prev_next_button(),
        ),
    )
    img = imgdiv.getElementsByTagName('img')[0]
    img.onerror = on_img_err
    img.src = cover_url(book_id)
    c = c.lastChild
    c.appendChild(E.div(
        style='display:flex; padding: 1ex 1em; align-items: flex-start; justify-content: flex-start; flex-wrap: wrap',
        E.div(style='margin-right: 1em; flex-grow: 3; max-width: 500px', data_book_id='' + book_id),
        imgdiv
    ))
    container = c.lastChild.firstChild
    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, '\xa0\xa0\xa0', download_button, style='margin-bottom: 1ex')
    if not metadata.formats or not metadata.formats.length:
        row.style.display = 'none'
    container.appendChild(row)
    md = E.div(style='margin-bottom: 2ex')
    table = E.table(class_='metadata')
    container.appendChild(md)
    md.appendChild(table)

    render_metadata(metadata, table, book_id)


def add_top_bar_buttons(container_id):
    container = document.getElementById(container_id)
    if container:
        clear_buttons(container)
        add_button(container, 'convert', action=convert_book, tooltip=_('Convert this book to another format [C]'))
        add_button(container, 'edit', action=edit_metadata, tooltip=_('Edit the metadata for this book [E]'))
        add_button(container, 'trash', action=delete_book, tooltip=_('Delete this book'))
        book_id = parse_url_params().book_id
        if book_id is '0':
            add_button(container, 'random', tooltip=_('Show a random book'), action=def():
                fetch_metadata(container_id, 0, proceed_after_succesful_fetch_metadata)
            )
        add_button(container, 'ellipsis-v', action=show_subsequent_panel.bind(None, 'more_actions'), tooltip=_('More actions'))


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
    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()
    container = container.lastChild
    clear(container)
    container.appendChild(E.div(
        style='margin: 1ex 1em',
        create_spinner(), '\xa0' + _('Fetching metadata for the book, please wait') + '…',
    ))


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)
    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=_('Book details'), 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)
    create_top_bar(container, title=_('More actions…'), action=back, icon='close')
    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 = v'[]'

    def exists(fname):
        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)
    create_top_bar(container, title=_('Upload data files'), action=back, icon='close')
    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 = v'[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)
    create_top_bar(container, title=_('Manage data files'), action=back, icon='close')
    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)
    create_top_bar(container, title=_('Search the internet'), action=back, icon='close')
    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
        if response.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 response.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:
            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':v'[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(_('Copy'), None, trigger_copy.bind(None, container_id, False, close_func), highlight=True),
                '\xa0',
                create_button(_('Move'), None, trigger_copy.bind(None, container_id, True, close_func)),
                '\xa0',
                create_button(_('Cancel'), None, close_func),
            )
        ))
        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)
    create_top_bar(container, title=_('Copy to library'), action=back, icon='close')
    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'))

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)
