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

from ajax import ajax, absolute_path
from complete import create_search_bar
from dom import clear, set_css, build_rule, svgicon, add_extra_css, ensure_id
from elementmaker import E
from gettext import gettext as _
from widgets import create_button, create_spinner, Breadcrumbs
from modals import show_modal, create_custom_dialog
from utils import rating_to_stars, safe_set_inner_html, is_ios
from session import get_interface_data

from book_list.library_data import library_data, current_library_id, current_virtual_library
from book_list.ui import show_panel
from book_list.router import back
from book_list.top_bar import create_top_bar, add_button
from book_list.globals import get_session_data
from book_list.theme import get_color, get_font_size
from book_list.prefs import prefs_panel_handler

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 + ' '

    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=get_color('list-hover-foreground'), background_color=get_color('list-hover-background'))
    style += build_rule(sel + ' div.tag-menu:hover', color=get_color('list-hover-foreground'), background_color=get_color('list-hover-background'))
    style += build_rule(sel + ' div.tag-name:active', transform='scale(1.5)')
    style += build_rule(sel + ' div.tag-menu:active', transform='scale(2)')

    # search items list
    style += build_rule(sel + ' ul.search-items', margin_top='1ex', list_style_type='none', text_align='left')
    style += build_rule(sel + ' ul.search-items > li', display='inline-block', cursor='pointer', background_color=get_color('window-background2'), border_radius='10px', padding='0.5ex', margin_right='1em')
    style += build_rule(sel + ' ul.search-items > li:hover', color=get_color('window-hover-foreground'))
    style += build_rule(sel + ' ul.search-items > li:active', transform='scale(1.5)')

    # 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()
    ans = interface_data.icon_map[node.data.category] or 'column.png'
    return absolute_path(interface_data.icon_path + '/' + ans)


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(_('Replace'), None, action.bind(None, True), highlight=True),
                '\xa0',
                create_button(_('Keep'), None, action.bind(None, False)),
            )
        ))
    )




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 = v'[]'
    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 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
        items = []
        if data.count is not undefined:
            items.append(_('Count: ') + data.count)
        if data.avg_rating is not undefined:
            items.append(_('Rating: {:.1f}').format(data.avg_rating))
        suffix = ''
        if items.length:
            suffix = ' [' + items.join(' ') + ']'

        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_for_node(node), style='height:2ex'),
            E.span('\xa0' + name + suffix)
        )
        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:hidden', 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'),
                E.span('\xa0' + 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)
        )
    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)
        div = E.div(
                title=tooltip.lstrip(),
                style="display:flex; align-items: stretch",
                E.div(class_='tag-name',
                    style='border-right:solid 1px currentColor; padding: 1ex; display:flex; align-items: center',
                    E.img(src=icon_for_node(node), style='display:inline-block; max-height:2.5ex'),
                    E.span(data.name),
                ),
                E.div(class_='tag-menu',
                    style='padding: 1ex; display:flex; align-items:center',
                    E.div(svgicon('angle-down'))
                )
        )
        set_css(div, max_width='95vw', border='solid 1px currentColor', border_radius='20px', margin='0.5rem', cursor='pointer', overflow='hidden', user_select='none')
        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 = document.getElementById(state.container_id).lastChild.lastChild
    clear(container)
    set_css(container, padding='1ex 1em', display='flex', flex_wrap='wrap', margin_left='-0.5rem')
    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'
    container = component(container, 'tag_browser')
    container.style.display = 'block'
    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():
    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
    container.appendChild(E.div(
        data_component='search',
        style="text-align:center; padding:1ex 1em; border-bottom: solid 1px currentColor; margin-bottom: 0.5ex"
    ))
    container.appendChild(E.div(
        E.div(data_component='loading'),
        E.ol(style="display:none", data_component='breadcrumbs'),
        E.div(style="display:none", data_component='tag_browser')
    ))

    # Build search input
    # We dont focus the search box because on mobile that will cause the
    # keyboard to popup and obscure the rest of the page
    search_container = component(container, 'search')
    search_button = create_button(_('Search'), icon='search', tooltip=_('Do the search'))
    search_bar = create_search_bar(execute_search_interactive, 'search-books', tooltip=_('Search for books'), placeholder=_('Enter the search query'), button=search_button)
    set_css(search_bar, flex_grow='10', margin_right='0.5em')
    search_container.appendChild(E.div(style="display: flex; width: 100%;", search_bar, search_button))
    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='margin-left:auto; margin-right:auto; font-size: 1.5rem; font-weight; bold; text-align:center; margin-top:30vh')
    )

    # 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'), action=back, icon='close')
    add_button(container, icon='cogs', action=show_panel.bind(None, 'book_list^search^prefs'), tooltip=_('Configure Tag browser'))
    container.appendChild(E.div(class_=CLASS_NAME))
    create_search_panel(container.lastChild)
    container.focus()
