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

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

from ajax import ajax_send
from book_list.add import add_books_panel
from book_list.cover_grid import (
    append_item as cover_grid_append_item, cover_grid_css,
    create_item as create_cover_grid_item, description as COVER_GRID_DESCRIPTION,
    init as init_cover_grid
)
from book_list.custom_list import (
    append_item as custom_list_append_item, create_item as create_custom_list_item,
    custom_list_css, description as CUSTOM_LIST_DESCRIPTION,
    init as init_custom_list
)
from book_list.details_list import (
    append_item as details_list_append_item, create_item as create_details_list_item,
    description as DETAILS_LIST_DESCRIPTION, details_list_css,
    init as init_details_list
)
from book_list.globals import get_session_data
from book_list.item_list import create_item, create_item_list
from book_list.library_data import (
    add_more_books, all_virtual_libraries, book_metadata, current_book_ids,
    current_sorted_field, ensure_current_library_data, library_data, load_status,
    loaded_books_query, thumbnail_cache, url_books_query
)
from book_list.router import back, home, push_state, update_window_title
from book_list.search import (
    init as init_search_panel, set_apply_search, tb_config_panel_handler
)
from book_list.top_bar import add_button, create_top_bar, set_title_tooltip
from book_list.ui import query_as_href, set_panel_handler, show_panel
from dom import add_extra_css, build_rule, clear, ensure_id, set_css
from modals import error_dialog
from session import get_interface_data
from utils import conditional_timeout, parse_url_params, safe_set_inner_html
from widgets import create_button, create_spinner, enable_escape_key

CLASS_NAME = 'book-list-container'
ITEM_CLASS_NAME = 'book-list-item'
ALLOWED_MODES = {'cover_grid', 'details_list', 'custom_list'}
DEFAULT_MODE = 'cover_grid'

add_extra_css(def():
    sel = '.' + CLASS_NAME + ' '
    ans = build_rule(sel + '[data-component="top_message"]', margin='1ex 1em')
    ans += cover_grid_css()
    ans += details_list_css()
    ans += custom_list_css()
    return ans
)

book_list_data = {
    'container_id': None, 'shown_book_ids': set(), 'mode': None, 'fetching_more_books': None,
}

def component(name):
    return document.getElementById(book_list_data.container_id).querySelector(f'[data-component="{name}"]')


def clear_grid():
    container = document.getElementById(book_list_data.container_id)
    # We replace the div entirely so that any styles associated with it are also removed
    e = component('book_list')
    bl = E.div(data_component='book_list')
    container.insertBefore(bl, e)
    container.removeChild(e)
    book_list_data.init_grid(bl)


def has_parent_with_class(elem, cls):
    while elem:
        if elem.classList and elem.classList.contains(cls):
            return elem
        elem = elem.parentNode


def save_scroll_position():
    container = document.getElementById(book_list_data.container_id)
    if container:
        left, top = container.offsetLeft, container.offsetTop
        for y in range(5, 100, 5):
            for x in range(25, 125, 5):
                elem = document.elementFromPoint(left + x, top + y)
                p = has_parent_with_class(elem, ITEM_CLASS_NAME)
                if p:
                    q = parse_url_params()
                    q.book_id = p.dataset.bookId
                    push_state(q, replace=True, call_handler=False)
                    return


def show_subsequent_panel(name, query=None, replace=False):
    save_scroll_position()
    show_panel(name, query, replace)


def on_book_click(book_id, evt):
    evt.preventDefault()
    show_subsequent_panel('book_details', {'book_id':book_id})


def create_image(book_id, max_width, max_height, on_load):
    return thumbnail_cache.get(book_id, max_width, max_height, on_load)


def render_id(book_id):
    l = book_list_data.shown_book_ids.length
    book_list_data.shown_book_ids.add(book_id)
    if l < book_list_data.shown_book_ids.length:
        metadata = book_metadata(book_id)
        ans = book_list_data.render_book(book_id, metadata, create_image, on_book_click.bind(None, book_id), query_as_href({'book_id': book_id + ''}, 'book_details'))
        ans.classList.add(ITEM_CLASS_NAME)
        ans.dataset.bookId = str(book_id)
        return ans


def render_ids(book_ids):
    book_ids = book_ids or current_book_ids()
    div = component('book_list')
    if div:
        for book_id in book_ids:
            child = render_id(book_id)
            if child:
                book_list_data.append_item(div, child)


def get_view_mode():
    idata = get_interface_data()
    mode = get_session_data().get('view_mode', idata.default_book_list_mode)
    if mode not in ('cover_grid', 'details_list', 'custom_list'):
        mode = DEFAULT_MODE
    return mode


def setup_view_mode(mode, book_list_data):
    if mode not in ALLOWED_MODES:
        mode = DEFAULT_MODE
    book_list_data.mode = mode
    if mode is 'cover_grid':
        book_list_data.render_book = create_cover_grid_item
        book_list_data.init_grid = init_cover_grid
        book_list_data.append_item = cover_grid_append_item
    elif mode is 'details_list':
        book_list_data.render_book = create_details_list_item
        book_list_data.init_grid = init_details_list
        book_list_data.append_item = details_list_append_item
    elif mode is 'custom_list':
        book_list_data.render_book = create_custom_list_item
        book_list_data.init_grid = init_custom_list
        book_list_data.append_item = custom_list_append_item
    return mode


def apply_view_mode(mode=DEFAULT_MODE):
    if book_list_data.mode is mode:
        return
    setup_view_mode(mode, book_list_data)
    clear_grid()
    render_ids()


# More button {{{

def update_fetching_status():
    more = component('more_button')
    if not more:
        return
    if book_list_data.fetching_more_books:
        more.firstChild.style.display = 'none'
        more.lastChild.style.display = 'block'
    elif library_data.search_result.total_num > book_list_data.shown_book_ids.length:
        more.firstChild.style.display = 'block'
        more.lastChild.style.display = 'none'
    else:
        more.firstChild.style.display = 'none'
        more.lastChild.style.display = 'none'


def abort_get_more_books(no_update):
    if book_list_data.fetching_more_books:
        book_list_data.fetching_more_books.abort()
        book_list_data.fetching_more_books = None
        if not no_update:
            update_fetching_status()


def got_more_books(end_type, xhr, event):
    if book_list_data.fetching_more_books is not xhr:
        return  # Fetching was aborted
    book_list_data.fetching_more_books = None
    update_fetching_status()
    if end_type is 'load':
        try:
            data = JSON.parse(xhr.responseText)
            if not data.search_result.book_ids:
                raise Exception('No books ids object in search result from server')
            add_more_books(data)
            render_ids(data.search_result.book_ids)
        except Exception:
            error_dialog(_('Could not get more books'), _('Server returned an invalid response'), traceback.format_exc())
    elif end_type is not 'abort':
        error_dialog(_('Could not get more books'), xhr.error_html)


def get_more_books():
    data = {'offset':book_list_data.shown_book_ids.length}
    for key in 'query', 'sort', 'sort_order', 'vl':
        data[key] = library_data.search_result[key]
    book_list_data.fetching_more_books = ajax_send(
        'interface-data/more-books', data, got_more_books,
        query={'library_id':loaded_books_query().library_id}
    )
    update_fetching_status()


def create_more_button(more):
    more.appendChild(create_button(
        _('Show more books'), 'cloud-download', get_more_books
    ))
    more.lastChild.setAttribute('rel', 'next')
    set_css(more.firstChild, display='block', margin_left='auto', margin_right='auto', margin_top='1rem')
    set_css(more, font_size='1.5rem', padding_top='1.5rem', margin_bottom='1.5rem', text_align='center', display='flex')
    more.appendChild(E.div(
        create_spinner(), '\xa0' + _('Fetching metadata for more books, please wait') + '…',
        style='margin-left:auto; margin-right:auto; display:none')
    )
    update_fetching_status()


# }}}

def show_top_message():
    container = component('top_message')
    q = loaded_books_query()
    if not q.search and not q.vl:
        container.style.display = 'none'
        return
    if q.vl:
        container.appendChild(E.span(
            E.span(_('Showing Virtual library: {} ').format(q.vl)),
            create_button(_('Close'), 'close', def(): show_vl();, _('Show all books in library'))
        ))
        if q.search:
            container.appendChild(E.br())
    if q.search:
        c = E.span()
        if library_data.search_result.total_num:
            c.appendChild(E.span(_('Showing books matching:'), ' ', E.i(library_data.search_result.query), ' ', _('(total matches: {}) ').format(library_data.search_result.total_num)))
            c.appendChild(E.span(' ', E.a(_('Clear search'), class_='blue-link', onclick=def(): search();)))
        else:
            c.appendChild(E.span(_('No books matching:'), ' ', E.i(library_data.search_result.query)))

        container.appendChild(c)

def create_books_list(container):
    book_list_data.container_id = ensure_id(container)
    book_list_data.shown_book_ids = set()
    book_list_data.mode = None
    abort_get_more_books(True)
    container.appendChild(E.div(data_component='top_message'))
    container.appendChild(E.div(data_component='book_list'))
    container.appendChild(E.div(data_component='more_button'))
    show_top_message()
    apply_view_mode(get_view_mode())
    create_more_button(container.lastChild)
    add_button(container.parentNode, icon='plus', action=show_subsequent_panel.bind(None, 'book_list^add'), tooltip=_('Add books'))
    add_button(container.parentNode, icon='sort-amount-desc', action=show_subsequent_panel.bind(None, 'book_list^sort'), tooltip=_('Sort books'))
    add_button(container.parentNode, icon='search', action=show_subsequent_panel.bind(None, 'book_list^search'), tooltip=_('Search for books'))
    add_button(container.parentNode, icon='ellipsis-v', action=show_subsequent_panel.bind(None, 'book_list^more_actions'), tooltip=_('More actions'))
    q = parse_url_params()
    scrolled = False
    if q.book_id:
        e = container.querySelector(f'.{ITEM_CLASS_NAME}[data-book-id="{q.book_id}"]')
        if e and e.scrollIntoView:
            e.scrollIntoView(True)
            # Now scroll extra corresponding to top bar size
            window.scrollBy(0, -container.offsetTop)
            scrolled = True
    if not scrolled:
        window.scrollTo(0, 0)
    q = loaded_books_query()
    if not q.search and library_data.search_result.total_num < 1:
        div = component('book_list')
        div.appendChild(E.div(_('No books found'), style='margin: 1ex 1em'))


def check_for_books_loaded():
    container = this
    container_id = container.id
    if load_status.loading:
        conditional_timeout(container_id, 5, check_for_books_loaded)
        return
    container = container.lastChild
    clear(container)
    if not load_status.ok:
        if load_status.http_error_code is 401:
            # Unauthorized, keep retrying until the user provides a
            # username/password
            container.appendChild(E.div(_('Username/password required, retrying...')))
            ensure_current_library_data()
            conditional_timeout(container_id, 100, check_for_books_loaded)
            return
        err = E.div()
        safe_set_inner_html(err, load_status.error_html)
        container.appendChild(E.div(
            style='margin: 1ex 1em',
            E.div(_('Failed to load books from calibre library, with error:')),
            err,
            E.div(
                style='margin-top: 1em; border-top: solid 1px currentColor; padding-top: 1ex;',
                E.a(onclick=def(): home(replace=True);, href='javascript: void(0)', class_='blue-link', _('Go back to the home page')))
            ),
        )
        return
    create_books_list(container)
    if library_data.search_result and jstype(library_data.search_result.num_books_without_search) is 'number':
        set_title_tooltip(this, _('Total number of books: {}').format(library_data.search_result.num_books_without_search))


def init(container_id):
    interface_data = get_interface_data()
    ensure_current_library_data()
    container = document.getElementById(container_id)
    lid = container.dataset.library_id = url_books_query().library_id
    title = interface_data.library_map[lid]
    update_window_title(title)
    create_top_bar(container, title=title, action=def(): save_scroll_position(), home();, icon='home')
    container.appendChild(E.div(class_=CLASS_NAME))
    container.lastChild.appendChild(E.div(_('Loading books from the {} calibre library, please wait...').format(title), style='margin: 1ex 1em'))
    conditional_timeout(container_id, 5, check_for_books_loaded)


# Sorting {{{

def change_sort(field, order):
    abort_get_more_books()
    sd = get_session_data()
    so = sd.get('last_sort_order')
    order = order or so[field] or 'asc'
    order = 'asc' if order is 'asc' else 'desc'
    q = loaded_books_query()
    q.sort = field + '.' + order
    so[field] = order
    sd.set('last_sort_order', so)
    sd.set_library_option(q.library_id, 'sort', q.sort)
    show_panel('book_list', query=q, replace=True)


def create_sort_panel(container_id):
    if not library_data.sortable_fields:
        show_panel('book_list', replace=True)
        return
    container = document.getElementById(container_id)
    close_action, close_icon = back, 'close'
    if parse_url_params().close_action is 'home':
        close_action, close_icon = def(): home();, 'home'
    create_top_bar(container, title=_('Sort books by…'), action=close_action, icon=close_icon)
    items = []
    csf, csf_order = current_sorted_field()
    new_sort_order = 'desc' if csf_order is 'asc' else 'asc'
    if csf is 'date':
        csf = 'timestamp'
    for field, name in library_data.sortable_fields:
        subtitle = icon_name = None
        if field is csf:
            subtitle = _('Reverse current sort order')
            icon_name = 'sort-amount-asc' if csf_order is 'asc' else 'sort-amount-desc'
            action = change_sort.bind(None, field, new_sort_order)
        else:
            action = change_sort.bind(None, field, None)
        items.push(create_item(name, subtitle=subtitle, icon=icon_name, action=action))
    container.appendChild(E.div())
    create_item_list(container.lastChild, items, _('Change how the list of books is sorted'))
    enable_escape_key(container, close_action)
# }}}

# Searching {{{


def search_query_for(query):
    q = loaded_books_query()
    if query:
        q.search = query
    else:
        v'delete q.search'
    return q


def search(query, replace=False):
    show_panel('book_list', query=search_query_for(query), replace=replace)

set_apply_search(def(query): search(query, True);)

# }}}

# Virtual libraries {{{
def create_vl_panel(container_id):
    if not library_data.sortable_fields:
        show_panel('book_list', replace=True)
        return
    container = document.getElementById(container_id)
    create_top_bar(container, title=_('Choose Virtual library…'), action=back, icon='close')
    items = []
    vls = all_virtual_libraries()
    vl_names = Object.keys(vls).sort(def (a, b): return a.toLowerCase().localeCompare(b.toLowerCase());)
    for name in vl_names:
        items.push(create_item(name, subtitle=vls[name], action=show_vl.bind(None, name, True)))
    container.appendChild(E.div())
    create_item_list(container.lastChild, items, _('Choose a Virtual library to browse from the list below'))
    enable_escape_key(container, back)


def show_vl(vl_name, replace):
    q = loaded_books_query()
    q.vl = vl_name or None
    show_panel('book_list', query=q, replace=replace or False)

# }}}

# Modes {{{
def create_mode_panel(container_id):
    if not library_data.sortable_fields:
        show_panel('book_list', replace=True)
        return
    container = document.getElementById(container_id)
    create_top_bar(container, title=_('Choose book list mode…'), action=back, icon='close')
    items = []
    current_mode = get_view_mode()

    def ci(title, desc, name):
        ic = 'check' if name is current_mode else None
        items.push(create_item(title, subtitle=desc, icon=ic, action=def():
            if name is not current_mode:
                get_session_data().set('view_mode', name)
            back()
        ))

    ci(_('Cover grid'), COVER_GRID_DESCRIPTION(), 'cover_grid')
    ci(_('Detailed list'), DETAILS_LIST_DESCRIPTION(), 'details_list')
    ci(_('Custom list'), CUSTOM_LIST_DESCRIPTION(), 'custom_list')
    container.appendChild(E.div())
    create_item_list(container.lastChild, items, _('Choose a display mode for the list of books from below'))
    enable_escape_key(container, back)

# }}}

# More actions {{{

def create_more_actions_panel(container_id):
    container = document.getElementById(container_id)
    create_top_bar(container, title=_('More actions…'), action=back, icon='close')
    items = [
        create_item(_('Book list mode'), subtitle=_('Change how the list of books is displayed'), action=def():
                    show_panel('book_list^choose_mode', replace=True)
        ),
        create_item(_('A random book'), subtitle=_('Choose a random book from the library'), action=def():
                    query = {'book_id':'0'}
                    show_panel('book_details', query=query, replace=True)
        ),
    ]
    vls = all_virtual_libraries()
    if vls and Object.keys(vls).length > 0:
        items.insert(1, create_item(_('Choose Virtual library'), subtitle=_('Browse books in a Virtual library'), action=def():
                     show_panel('book_list^vl', replace=True)
        ))
    container.appendChild(E.div())
    create_item_list(container.lastChild, items)
    enable_escape_key(container, back)
# }}}

# Adding books {{{
def create_add_panel(container_id):
    if not library_data.sortable_fields:
        show_panel('book_list', replace=True)
        return
    add_books_panel(container_id)

# }}}


set_panel_handler('book_list', init)
set_panel_handler('book_list^add', create_add_panel)
set_panel_handler('book_list^sort', create_sort_panel)
set_panel_handler('book_list^vl', create_vl_panel)
set_panel_handler('book_list^search', init_search_panel)
set_panel_handler('book_list^choose_mode', create_mode_panel)
set_panel_handler('book_list^search^prefs', tb_config_panel_handler())
set_panel_handler('book_list^more_actions', create_more_actions_panel)
