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

from elementmaker import E
from gettext import gettext as _

from ajax import ajax, absolute_path
from book_list.globals import get_session_data
from book_list.library_data import library_data, current_library_id, current_virtual_library
from book_list.prefs import prefs_panel_handler
from book_list.router import back, show_note
from book_list.theme import get_color, get_font_size
from book_list.top_bar import create_top_bar
from book_list.ui import show_panel
from complete import create_search_bar
from dom import clear, set_css, build_rule, svgicon, add_extra_css, ensure_id
from modals import show_modal, create_custom_dialog
from session import get_interface_data
from utils import rating_to_stars, safe_set_inner_html, is_ios
from widgets import create_button, create_spinner, Breadcrumbs

apply_search = None


def set_apply_search(func):
    nonlocal apply_search
    apply_search = func

sp_counter = 0
CLASS_NAME = 'book-search-panel'
add_extra_css(def ():
    sel = '.' + CLASS_NAME + ' '
    card_bg = 'var(--calibre-surface-1)'
    card_border = 'var(--calibre-border-style-subtle)'
    style = build_rule(sel,
        padding='0.75rem 1rem 1rem 1rem',
        max_width='980px',
        margin_top='0.75rem',
        margin_bottom='0.75rem',
        margin_left='auto',
        margin_right='auto',
        box_sizing='border-box',
        display='flex',
        flex_direction='column',
        gap='1rem',
    )
    style += r'''
@media (max-width: 720px) {
  .''' + CLASS_NAME + r''' { padding-left: 0.75rem; padding-right: 0.75rem; }
}
'''
    style += build_rule(sel + ' .cs-search-body', margin_top='0', display='flex', flex_direction='column', gap='0.95rem')
    style += build_rule(sel + ' div.tag-name > span', word_break='break-all', hyphens='auto', padding_left='1ex')
    style += build_rule(sel + ' div.tag-name:hover', color='inherit', background_color='var(--calibre-color-primary-light)')
    style += build_rule(sel + ' div.tag-menu:hover', color='inherit', background_color='var(--calibre-color-primary-light)')
    style += build_rule(sel + ' div.tag-name:active', transform='none', opacity='0.92')
    style += build_rule(sel + ' div.tag-menu:active', transform='none', opacity='0.92')
    # Active search terms chips (match details-list badges)
    style += build_rule(sel + ' ul.search-items',
        margin_top='0.65rem',
        margin_bottom='0',
        list_style_type='none',
        text_align='left',
        display='flex',
        flex_wrap='wrap',
        gap='0.35rem',
        padding='0',
    )
    style += build_rule(sel + ' ul.search-items > li',
        display='inline-flex',
        align_items='center',
        gap='0.25rem',
        cursor='pointer',
        padding='0.15rem 0.5rem',
        border_radius='999px',
        border='var(--calibre-border-style-subtle)',
        background='var(--calibre-color-surface-3)',
        font_size='0.92em',
    )
    style += build_rule(sel + ' ul.search-items > li:hover',
        background='var(--calibre-color-primary-light)',
        border_color='var(--calibre-color-border)',
    )
    style += build_rule(sel + ' ul.search-items > li:active',
        background='var(--calibre-color-primary-light-2)',
        transform='none',
    )
    # Tag browser card + breadcrumbs
    style += build_rule(sel + ' .cs-search-tag-card',
        padding='1rem',
        border_radius='18px',
        border=card_border,
        background=card_bg,
        box_shadow='none',
    )
    style += build_rule(sel + ' .cs-search-breadcrumbs.calibre-breadcrumbs',
        background='var(--calibre-color-surface-2)',
        border='var(--calibre-border-style-subtle)',
        border_radius='14px',
        margin='0 0 0.85rem 0',
        padding='0.25rem 0',
        z_index='auto',
    )
    style += build_rule(sel + ' .cs-search-breadcrumbs.calibre-breadcrumbs > li:hover',
        color='inherit',
    )
    style += build_rule(sel + ' .cs-search-tags',
        padding='0',
        display='flex',
        flex_direction='column',
        gap='0.65rem',
        margin='0',
        width='100%',
    )
    style += build_rule(sel + ' .cs-search-tag-row',
        display='flex',
        align_items='stretch',
        max_width='100%',
        border='var(--calibre-border-style-subtle)',
        border_radius='14px',
        background='var(--calibre-color-surface-2)',
        box_shadow='none',
        cursor='pointer',
        overflow='hidden',
        user_select='none',
        transition='background-color 160ms ease, border-color 160ms ease',
    )
    style += build_rule(sel + ' .cs-search-tag-row:hover',
        background_color='var(--calibre-color-primary-light)',
        border_color='var(--calibre-color-border)',
        box_shadow='none',
    )
    style += build_rule(sel + ' .cs-search-tag-row:active',
        background_color='var(--calibre-color-primary-light-2)',
        transform='translateY(0.5px)',
    )
    style += build_rule(sel + ' .cs-search-tag-row .tag-name',
        border_right='var(--calibre-border-style-subtle)',
        padding='0.65rem 0.85rem',
        display='flex',
        align_items='center',
        flex='1 1 auto',
        min_width='0',
    )
    style += build_rule(sel + ' .cs-search-tag-row .tag-menu',
        padding='0.65rem 0.75rem',
        display='flex',
        align_items='center',
        flex='0 0 auto',
        background='var(--calibre-color-surface-3)',
    )
    style += build_rule(sel + ' .cs-search-loading',
        padding='2.5rem 1rem',
        text_align='center',
        border_radius='18px',
        border=card_border,
        background=card_bg,
        box_shadow='none',
    )
    # Actions popup
    style += build_rule(
        '#modal-container ul.tb-action-list > li:hover',
        color=get_color('list-hover-foreground'), background_color=get_color('list-hover-background')
    )
    style += build_rule(
        '#modal-container ul.tb-action-list > li:active',
        color=get_color('window-hover-foreground'), color=get_color('list-hover-foreground'),
        background_color=get_color('list-hover-background')
    )
    return style
)

state = {}
last_accepted_search = None


def component(container, name):
    return container.querySelector(f'[data-component="{name}"]')


def icon_for_node(node):
    interface_data = get_interface_data()
    fallback = ''
    if node.data.category is 'formats' and not node.data.is_category:
        fmt = node.data.name.lower()
        if fmt.startswith('original_'):
            fmt = fmt.partition('_')[2]
        ans = f'mimetypes/{fmt}.png'
        fallback = interface_data.icon_map[node.data.category]
    else:
        ans = interface_data.icon_map[node.data.category] or 'column.png'
        if node.data.value_icon:
            fallback = ans
            ans = '_' + node.data.value_icon
    if fallback:
        fallback = absolute_path(interface_data.icon_path + '/' + fallback)
    return absolute_path(interface_data.icon_path + '/' + ans), fallback


def node_for_path(path):
    path = path or state.tag_path
    ans = state.tag_browser_data
    for child_index in path:
        ans = ans.children[child_index]
    return ans


def execute_search(text):
    nonlocal last_accepted_search
    if state.tag_path and state.tag_path.length:
        last_accepted_search = {'library_id': current_library_id(), 'vl': current_virtual_library(), 'tag_path': list(state.tag_path)}
    apply_search(text)


def ask_about_smart_quotes(text):
    create_custom_dialog(_('Search contains smart quotes'), def (parent, close_modal):
        def action(replace):
            nonlocal text
            close_modal()
            if replace:
                text = text.replace(/[“”«»„]/g, '"')
            execute_search(text)
        parent.appendChild(E.div(
            E.div(_(
                'The search expression {} contains smart quotes. By default, Apple inserts smart'
                ' quotes instead of normal quotes when tapping the quote button. This will prevent'
                ' the search from working, if you intended to use normal quotes. You can type'
                ' normal quotes by long-tapping the quotes button. Do you want to keep the smart'
                ' quotes or replace them with normal quotes?'
            ).format(text)),
            E.div(class_='button-box',
                create_button(_('Keep'), None, action.bind(None, False)),
                create_button(_('Replace'), None, action.bind(None, True), highlight=True),
            )
        ))
    )


def execute_search_interactive(text):
    if not text:
        container = document.getElementById(state.container_id)
        search_control = container.querySelector('input[name="search-books"]')
        text = search_control.value or ''
    if is_ios and /[“”«»„]/.test(text):
        ask_about_smart_quotes(text)
    else:
        execute_search(text)


def search_expression_for_item(node, node_state):
    item = node.data
    if item.is_searchable is False or not node_state or node_state is 'clear':
        return ''

    search_state = {'plus': 'true', 'plusplus': '.true', 'minus': 'false', 'minusminus': '.false'}[node_state]
    stars = rating_to_stars(3, True)

    if item.is_category:
        category = item.category

        if item.is_first_letter:
            letters_seen = {}
            for child in node.children:
                if child.data.sort:
                    letters_seen[child.data.sort[0]] = True
            letters_seen = Object.keys(letters_seen)
            if letters_seen.length:
                charclass = letters_seen.join('')
                if category is 'authors':
                    expr = r'author_sort:"~(^[{0}])|(&\s*[{0}])"'.format(charclass)
                elif category is 'series':
                    expr = r'series_sort:"~^[{0}]"'.format(charclass)
                else:
                    expr = r'{0}:"~^[{1}]"'.format(category, charclass)
            else:
                expr = '{}:false'.format(category)

        elif category is 'news':
            expr = 'tags:"={}"'.format(item.name)

        else:
            return '{}:{}'.format(category, search_state)

        if 'false' in search_state:
            expr = '(not ' + expr + ')'
        return expr

    category = 'tags' if item.category is 'news' else item.category
    if item.name and item.name[0] in stars:
        # Assume ratings
        rnum = item.name.length
        if item.name.endswith(stars[-1]):
            rnum = '{}.5'.format(rnum - 1)
        expr = '{}:{}'.format(category, rnum)
    else:
        fm = library_data.field_metadata[item.category]
        suffix = ':' if fm and fm.is_csp else ''
        name = item.original_name or item.name or item.sort
        if not name:
            return ''
        name = str.replace(name, '"', r'\"')
        if name[0] is '.':
            name = '.' + name
        if search_state is '.true' or search_state is '.false':
            name = '.' + name
        expr = '{}:"={}{}"'.format(category, name, suffix)

    if 'false' in search_state:
        expr = '(not ' + expr + ')'
    return expr


def node_clicked(i):
    node = node_for_path().children[i]
    if node.children and node.children.length:
        state.tag_path.append(i)
        render_tag_browser()
    else:
        expr = search_expression_for_item(node, 'plus')
        execute_search(expr)


def add_to_search(node, search_type):
    anded = get_session_data().get('and_search_terms')
    state.active_nodes[node.id] = [search_type, anded]
    render_search_expression()


def remove_expression(node_id):
    v'delete state.active_nodes[node_id]'
    render_search_expression()


def render_search_expression():
    parts = []
    container = document.getElementById(state.container_id)
    sic = component(container, 'search_expression')
    clear(sic)
    for node_id in Object.keys(state.active_nodes):
        search_type, anded = state.active_nodes[node_id]
        node = state.node_id_map[node_id]
        expr = search_expression_for_item(node, search_type)
        name = node.data.original_name or node.data.name or node.data.sort or ''
        if expr:
            c = E.li(svgicon('remove'), '\xa0' + name)
            sic.appendChild(c)
            c.addEventListener('click', remove_expression.bind(None, node_id))
            if parts.length:
                expr = ('and' if anded else 'or') + ' ' + expr
            parts.push(expr)
    search_control = container.querySelector('input[name="search-books"]')
    search_control.value = parts.join(' ')


def show_fallback(fallback, ev):
    if fallback:
        if ev.target.src is not fallback:
            ev.target.src = fallback


def menu_clicked(i):
    def create_details(container, hide_modal):
        node = node_for_path().children[i]
        data = node.data
        name = data.original_name or data.name or data.sort
        icon, fallback = icon_for_node(node)
        title = E.h2(
            style='display:flex; align-items: center; border-bottom: solid 1px currentColor; font-weight:bold; font-size:' + get_font_size('title'),
            E.img(src=icon, onerror=show_fallback.bind(None, fallback), style='height:2ex; margin-right: 0.5rem'),
            E.span(name)
        )

        def edit_note(field, item_name):
            hide_modal()
            show_note(field, 0, item_name, panel='edit_note')

        if data.category and data.name and library_data.fields_that_support_notes.indexOf(data.category):
            title.appendChild(E.a(svgicon('pencil'),
                class_='blue-link', style='margin-left: 0.5em', href='javascript:void(0)',
                onclick=edit_note.bind(None, data.category, data.name),
                title=_('Edit or add notes for {}').format(data.name),
            ))
        container.appendChild(title)
        container.appendChild(E.div(
            style='margin-top:1ex; margin-bottom: 1ex',
            _('Search for books based on this category (a search term will be added to the search field)')
        ))

        ul = E.ul(style='list-style:none; overflow-x:auto; overflow-y:visible', class_='tb-action-list')
        container.appendChild(ul)
        items = [
            (_('Books matching this category'), 'plus'),
            (_('Books that do not match this category'), 'minus'),
        ]
        if node.data.is_hierarchical is 5:
            items.extend([
                (_('Books that match this category and all sub-categories'), 'plusplus'),
                (_('Books that do not match this category or any of its sub-categories'), 'minusminus'),
            ])
        interface_data = get_interface_data()
        for text, search_type in items:
            li = E.li(
                style='display:flex; align-items: center; margin-bottom:0.5ex; padding: 0.5ex; cursor:pointer',
                E.img(src=absolute_path('{}/{}.png'.format(interface_data.icon_path, search_type)), style='max-height: 2.5ex; margin-right:0.5rem'),
                E.span(text)
            )
            li.addEventListener('click', add_to_search.bind(None, node, search_type))
            li.addEventListener('click', hide_modal)
            ul.appendChild(li)
        f = E.form(
                style='text-align:left; border-top: solid 1px currentColor; padding-top:1ex; margin-top:0.5ex; display:flex; align-items:center',
                E.span(_('Add to the search expression with:')),
                E.input(type='radio', name='expr_join', value='OR', checked=''),
                E.span('\xa0OR\xa0'),
                E.input(type='radio', name='expr_join', value='AND'),
                E.span('\xa0AND')
        )
        and_control = f.lastChild.previousSibling
        and_control.checked = get_session_data().get('and_search_terms')
        container.appendChild(f)
        and_control.addEventListener('change', def (ev):
            get_session_data().set('and_search_terms', bool(ev.target.checked))
        )
        f.firstChild.nextSibling.addEventListener('change', def (ev):
            get_session_data().set('and_search_terms', not ev.target.checked)
        )
        about_items = []
        if data.count is not undefined:
            about_items.push(_('Number of books: {}').format(data.count))
        if data.avg_rating is not undefined:
            about_items.push(_('Average rating of books: {:.1f}').format(data.avg_rating))
        footer = E.div(
            style='text-align:left; border-top: solid 1px currentColor; padding-top:1ex; margin-top:0.5ex;',
        )
        if about_items.length:
            footer.appendChild(E.div(style='font-size: smaller', ' '.join(about_items)))
        if footer.firstChild:
            container.appendChild(footer)

    show_modal(create_details)


def render_children(container, children):
    for i, node in enumerate(children):
        data = node.data
        tooltip = ''
        if data.count is not undefined:
            tooltip += '\n' + _('Number of books in this category: {}').format(data.count)
        if data.avg_rating is not undefined:
            tooltip += '\n' + _('Average rating for books in this category: {:.1f}').format(data.avg_rating)
        icon, fallback = icon_for_node(node)
        div = E.div(
            title=tooltip.lstrip(),
            class_='cs-search-tag-row',
            E.div(
                class_='tag-name',
                E.img(src=icon, onerror=show_fallback.bind(None, fallback),
                      style='display:inline-block; max-height:2.5ex; margin-right:0.5rem; flex-shrink:0'),
                E.span(data.name),
            ),
            E.div(class_='tag-menu', E.div(svgicon('angle-down')))
        )
        div.firstChild.addEventListener('click', node_clicked.bind(None, i))
        div.lastChild.addEventListener('click', menu_clicked.bind(None, i))
        container.appendChild(div)


def render_breadcrumbs():
    container = state.breadcrumbs.container
    if not state.tag_path.length:
        container.style.display = 'none'
        return
    container.style.display = 'inline-block'
    state.breadcrumbs.reset()

    def onclick(i):
        return def (ev):
            state.tag_path = state.tag_path[:i + 1]
            render_tag_browser()
            ev.preventDefault()
            return True

    def create_breadcrumb(index=-1, item=None):
        li = state.breadcrumbs.add_crumb(onclick(index))
        if item:
            li.appendChild(E.span(item.name))
        else:
            li.appendChild(svgicon('home', '2.2ex', '2.2ex'))

    create_breadcrumb()
    parent = state.tag_browser_data
    for i, index in enumerate(state.tag_path):
        parent = parent.children[index]
        create_breadcrumb(i, parent.data)


def render_tag_browser():
    container = component(document.getElementById(state.container_id), 'tag_browser')
    clear(container)
    set_css(container, padding='0', display='flex', flex_direction='column', gap='0.65rem', width='100%')
    try:
        node = node_for_path()
    except:
        # If a saved tag_path is no longer valid
        state.tag_path = []
        node = node_for_path()
    render_children(container, node.children)
    render_breadcrumbs()


def on_data_fetched(end_type, xhr, ev):
    state.currently_loading = None
    if end_type is 'abort':
        return
    container = document.getElementById(state.container_id)
    if not container:
        return

    loading_panel = component(container, 'loading')
    loading_panel.style.display = 'none'
    tc = component(container, 'tag_card')
    if tc:
        tc.style.display = 'block'
    container = component(container, 'tag_browser')
    container.style.display = 'flex'
    clear(container)

    def show_error(error_html):
        ediv = E.div()
        container.appendChild(ediv)
        safe_set_inner_html(ediv, '<h3>' + _('Failed to load Tag browser data') + '</h3>' + error_html)

    def process_node(node, item_map):
        state.node_id_map[node.id] = node
        node.data = item_map[node.id]
        for child in node.children:
            child.parent = node
            process_node(child, item_map)

    if end_type is 'load':
        try:
            tag_browser_data = JSON.parse(xhr.responseText)
        except Exception as err:
            show_error(err + '')
            return
        state.tag_browser_data = tag_browser_data.root
        state.node_id_map = {}
        state.active_nodes = {}
        process_node(state.tag_browser_data, tag_browser_data.item_map)
        if last_accepted_search and last_accepted_search.library_id is current_library_id() and last_accepted_search.vl is current_virtual_library():
            if last_accepted_search.tag_path.length:
                state.tag_path = list(last_accepted_search.tag_path)
        render_tag_browser()
    else:
        show_error(xhr.error_html)


def refresh():
    root = document.getElementById(state.container_id)
    if root:
        lp = component(root, 'loading')
        if lp:
            lp.style.display = 'block'
        tc = component(root, 'tag_card')
        if tc:
            tc.style.display = 'none'
    if state.currently_loading is not None:
        state.currently_loading.abort()
        state.currently_loading = None
    sd = get_session_data()
    query = {'library_id': current_library_id(), 'vl': current_virtual_library()}
    for k in 'sort_tags_by partition_method collapse_at dont_collapse hide_empty_categories'.split(' '):
        query[k] = sd.get(k) + ''
    xhr = ajax('interface-data/tag-browser', on_data_fetched, query=query, bypass_cache=False)
    xhr.send()
    state.currently_loading = xhr


def create_search_panel(container):
    nonlocal state
    state = {}
    container.classList.add(CLASS_NAME)
    # search input container
    search_container = E.div(class_='calibre-group-box', data_component='search')
    container.appendChild(search_container)
    container.appendChild(E.div(class_='cs-search-body',
        E.div(data_component='loading', class_='cs-search-loading'),
        E.div(data_component='tag_card', class_='cs-search-tag-card', style='display:none',
            E.ol(style='display:none', data_component='breadcrumbs', class_='cs-search-breadcrumbs'),
            E.div(style='display:none', data_component='tag_browser', class_='cs-search-tags'),
        ),
    ))

    # Build search input
    # We don't focus the search box because on mobile that will cause the
    # keyboard to popup and obscure the rest of the page

    search_bar = create_search_bar(
        execute_search_interactive, 'search-books', tooltip=_('Search for books'),
        placeholder=_('Enter the search query'), button='icon-button')
    search_container.appendChild(search_bar)
    search_container.appendChild(E.div(
        E.a(class_='blue-link', svgicon('fts'), '\xa0', _('Search the full text of books instead'),
            href='javascript: void(0)', onclick=show_panel.bind(None, 'fts')),
        style='margin-top: 1rem'
    ))
    search_container.appendChild(E.ul(class_='search-items', data_component='search_expression'))

    # Build loading panel
    loading_panel = component(container, 'loading')
    loading_panel.appendChild(E.div(
        create_spinner(), '\xa0' + _('Fetching data for the Tag browser, please wait') + '…',
        style='font-size: 1.15rem; font-weight: 700; opacity: 0.9')
    )

    # Build breadcrumbs
    state.breadcrumbs = Breadcrumbs(component(container, 'breadcrumbs'))

    # Init state
    state.currently_loading = None
    state.tag_browser_data = None
    state.node_id_map = {}
    state.active_nodes = {}
    state.tag_path = []
    state.container_id = ensure_id(container)
    refresh()


# Tag browser prefrences {{{


def get_prefs():
    return [
        {
            'name': 'sort_tags_by',
            'text': _('Sort tags by'),
            'choices': [('name', _('Name')), ('popularity', _('Popularity (number of books)')), ('rating', _('Average rating'))],
            'tooltip': _('Change how the tags/authors/etc. are sorted in the "Tag browser"'),
        },
        {
            'name': 'partition_method',
            'text': _('Category partitioning method'),
            'choices': [('first letter', _('First Letter')), ('disable', _('Disable')), ('partition', _('Partition'))],
            'tooltip': _('Choose how Tag browser subcategories are displayed when'
                        ' there are more items than the limit. Select by first'
                        ' letter to see an A, B, C list. Choose partitioned to'
                        ' have a list of fixed-sized groups. Set to disabled'
                        ' if you never want subcategories.'),
        },
        {
            'name': 'collapse_at',
            'text': _('Collapse when more items than'),
            'min': 5, 'max': 10000, 'step': 5,
            'from_storage': int, 'to_storage': int,
            'tooltip': _('If a "Tag browser" category has more than this number of items, it is divided'
                            ' up into subcategories. If the partition method is set to disable, this value is ignored.'),
        },
        {
            'name': 'dont_collapse',
            'text': _('Categories not to partition'),
            'tooltip': _('A comma-separated list of categories in which items containing'
                            ' periods are displayed in the Tag browser trees. For example, if'
                            " this box contains 'tags' then tags of the form 'Mystery.English'"
                            " and 'Mystery.Thriller' will be displayed with English and Thriller"
                            " both under 'Mystery'. If 'tags' is not in this box,"
                            ' then the tags will be displayed each on their own line.'),
        },
        {
            'name': 'hide_empty_categories',
            'text': _('Hide empty categories (columns)'),
            'from_storage': def (x): return x.toLowerCase() is 'yes';,
            'to_storage': def (x): return 'yes' if x else 'no';,
            'tooltip': _('When checked, calibre will automatically hide any category'
                        ' (a column, custom or standard) that has no items to show. For example, some'
                        ' categories might not have values when using Virtual libraries. Checking this'
                        ' box will cause these empty categories to be hidden.'),
        },
    ]


def tb_config_panel_handler():
    return prefs_panel_handler(def (): return _('Configure Tag browser');, get_prefs)
# }}}


def onkeydown(container_id, close_action, ev):
    if ev.key is 'Escape':
        ev.preventDefault(), ev.stopPropagation()
        close_action()


def init(container_id):
    if not library_data.sortable_fields:
        show_panel('book_list', replace=True)
        return
    container = document.getElementById(container_id)
    container.setAttribute('tabindex', '0')
    container.addEventListener('keydown', onkeydown.bind(None, container_id, back), {'passive': False, 'capture': True})
    create_top_bar(container, title=_('Search for books'), icon='back')
    container.appendChild(E.div(class_=CLASS_NAME))
    create_search_panel(container.lastChild)
    container.focus()
