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

from elementmaker import E
from gettext import gettext as _

from complete import create_search_bar
from dom import ensure_id, set_css, svgicon
from modals import error_dialog
from read_book.anchor_visibility import position_for_anchor
from read_book.globals import (
    current_book, current_layout_mode, current_spine_item, get_boss, set_toc_anchor_map,
    toc_anchor_map
)
from read_book.viewport import scroll_viewport
from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view


def update_visible_toc_nodes(visible_anchors):
    update_visible_toc_nodes.data = visible_anchors
update_visible_toc_nodes.data = {}


def iter_toc_descendants(node, callback):
    for child in node.children:
        if callback(child):
            return
        iter_toc_descendants(child, callback)


def get_toc_nodes_bordering_spine_item(toc, csi):
    # Find the ToC entries that point to the closest files on either side of the
    # spine item
    toc = toc or current_book().manifest.toc
    csi = csi or current_spine_item()
    spine = current_book().manifest.spine
    spine_before, spine_after = {}, {}
    which = spine_before
    before = after = prev = None
    for name in spine:
        if name is csi:
            which = spine_after
        else:
            which[name] = True
    iter_toc_descendants(toc, def (node):
        nonlocal prev, before, after
        if node.dest:
            if spine_before[node.dest]:
                prev = node
            elif spine_after[node.dest]:
                if not before:
                    before = prev
                if not after:
                    after = node
                return True
    )
    if not before and prev is not None:
        before = prev
    return before, after


def get_border_nodes(toc, id_map):
    data = update_visible_toc_nodes.data
    before, after = data.before, data.after
    if before:
        before = id_map[before]
    if after:
        after = id_map[after]
    if before and after:
        # Both border nodes are in the current spine item
        return before, after
    sb, sa = get_toc_nodes_bordering_spine_item(toc)
    before = before or sb
    after = after or sa
    return before, after


def family_for_toc_node(toc_node_id, parent_map, id_map):
    if not id_map or not parent_map:
        toc = current_book().manifest.toc
        parent_map, id_map = get_toc_maps(toc)
    family = []
    node = id_map[toc_node_id]
    while node and node.title:
        family.unshift(node)
        parent = parent_map[node.id]
        node = None
        if parent:
            node = id_map[parent.id]
    return family


def get_page_list_id_map(page_list):
    return {x.id: x for x in page_list}


def get_page_list_before(page_list, id_map):
    # Find the page list entries that point to the closest files on either side of the spine item
    csi = csi or current_spine_item()
    spine = current_book().manifest.spine
    spine_before, spine_after = {}, {}
    which = spine_before
    before = prev = None
    for name in spine:
        if name is csi:
            which = spine_after
        else:
            which[name] = True
    for item in page_list:
        if item.dest:
            if spine_before[item.dest]:
                prev = item
            elif spine_after[item.dest]:
                if not before:
                    before = prev
                break
    return before


def get_current_pagelist_items():
    ans = {}
    page_list = current_book().manifest.page_list
    data = update_visible_toc_nodes.data?.page_list
    if not data or not page_list:
        return ans
    id_map = get_page_list_id_map(page_list)
    if data.has_visible:
        ans = data.visible_anchors
    else:
        if data.before?:
            ans[data.before] = True
        else:
            before = get_page_list_before(page_list, id_map)
            if before?:
                ans[before.id] = True
    return [id_map[x] for x in Object.keys(ans)]


def get_current_toc_nodes():
    toc = current_book().manifest.toc
    parent_map, id_map = get_toc_maps(toc)
    data = update_visible_toc_nodes.data
    ans = {}
    if data.has_visible:
        ans = data.visible_anchors
    else:
        if data.before:
            ans[data.before] = True
        else:
            before = get_border_nodes(toc, id_map)[0]
            if before:
                ans[before.id] = True
    r = []
    for x in Object.keys(ans):
        fam = family_for_toc_node(x, parent_map, id_map)
        if fam?.length:
            r.push(fam)
    return r


def get_highlighted_toc_nodes(toc, parent_map, id_map, skip_parents):
    data = update_visible_toc_nodes.data
    ans = {}
    if data.has_visible:
        ans = data.visible_anchors
    else:
        if data.before:
            ans[data.before] = True
        else:
            before = get_border_nodes(toc, id_map)[0]
            if before:
                ans[before.id] = True
    if not skip_parents:
        for node_id in Object.keys(ans):
            p = parent_map[node_id]
            while p and p.title:
                ans[p.id] = True
                p = parent_map[p.id]
    return ans


def get_toc_maps(toc):
    if not toc:
        toc = current_book().manifest.toc
    parent_map, id_map = {}, {}

    def process_node(node, parent):
        id_map[node.id] = node
        parent_map[node.id] = parent
        for c in node.children:
            process_node(c, node)

    process_node(toc)
    return parent_map, id_map


def get_book_mark_title():
    toc = current_book().manifest.toc
    parent_map, id_map = get_toc_maps(toc)
    highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map, True)
    for node_id in Object.keys(highlighted_toc_nodes):
        node = id_map[node_id]
        if node.title:
            return node.title
    return ''


def create_toc_tree(toc, onclick):
    parent_map, id_map = get_toc_maps(toc)
    highlighted_toc_nodes = get_highlighted_toc_nodes(toc, parent_map, id_map)

    def populate_data(node, li, a):
        li.dataset.tocDest = node.dest or ''
        li.dataset.tocFrag = node.frag or ''
        title = node.title or ''
        if highlighted_toc_nodes[node.id]:
            a.appendChild(E.b(E.i(title)))
        else:
            a.textContent = title

    return create_tree(toc, populate_data, onclick)


def do_search(text):
    container = document.getElementById(this)
    a = find_text_in_tree(container, text)
    if not text:
        return
    if not a:
        return error_dialog(_('No matches found'), _(
            'The text "{}" was not found in the Table of Contents').format(text))
    scroll_tree_item_into_view(a)


def create_toc_panel(book, container, onclick):
    def handle_click(event, li):
        if event.button is 0:
            onclick(li.dataset.tocDest, li.dataset.tocFrag)
    toc_panel = create_toc_tree(book.manifest.toc, handle_click)
    toc_panel_id = ensure_id(toc_panel)
    set_css(container, display='flex', flex_direction='column', height='100%', min_height='100%', overflow='hidden', max_height='100vh', max_width='100vw')
    set_css(toc_panel, flex_grow='10')
    container.appendChild(toc_panel)
    search_button = E.a(class_='calibre-icon-button', svgicon('search'), href='javascript:void(0)', title=_('Search'),
                        onmousedown=def (e): e.preventDefault();)
    t = _('Search Table of Contents')
    search_bar = create_search_bar(do_search.bind(toc_panel_id), 'search-book-toc', button=search_button, placeholder=t)
    set_css(search_bar, flex_grow='10', margin_right='1em')
    container.appendChild(E.div(style='margin: 1ex 1em; display: flex; align-items: center', search_bar, search_button))
    for child in container.childNodes:
        child.style.flexShrink = '0'
    toc_panel.style.flexGrow = '100'
    toc_panel.style.flexShrink = '1'
    toc_panel.style.overflow = 'auto'


def recalculate_toc_anchor_positions(tam, plam):
    name = current_spine_item().name
    am = {}
    anchors = []
    pos_map = {}
    for i, anchor in enumerate(tam[name] or []):
        am[anchor.id] = position_for_anchor(anchor.frag)
        anchors.push(anchor.id)
        pos_map[anchor.id] = i
    anchor_funcs = get_boss().anchor_funcs
    # stable sort by position in document
    anchors.sort(def (a, b): return anchor_funcs.cmp(am[a], am[b]) or (pos_map[a] - pos_map[b]);)

    current_map = {
        'layout_mode': current_layout_mode(),
        'width': scroll_viewport.width(),
        'height': scroll_viewport.height(),
        'pos_map': am,
        'sorted_anchors': anchors,
    }
    am = {}
    anchors = []
    pos_map = {}
    if plam:
        for i, anchor in enumerate(plam[name] or []):
            am[anchor.id] = position_for_anchor(anchor.frag, True)
            anchors.push(anchor.id)
            pos_map[anchor.id] = i
    # stable sort by position in document
    anchors.sort(def (a, b): return anchor_funcs.cmp(am[a], am[b]) or (pos_map[a] - pos_map[b]);)
    current_map.page_list_pos_map = am
    current_map.page_list_sorted_anchors = anchors

    set_toc_anchor_map(current_map)
    return current_map


def current_toc_anchor_map(tam, plam):
    current_map = toc_anchor_map()
    if not (current_map and current_map.layout_mode is current_layout_mode()
            and current_map.width is scroll_viewport.width() and current_map.height is scroll_viewport.height()):
        current_map = recalculate_toc_anchor_positions(tam, plam)
    return current_map


def update_visible_toc_anchors(toc_anchor_map, page_list_anchor_map, recalculate):
    if recalculate:
        recalculate_toc_anchor_positions(toc_anchor_map, page_list_anchor_map)
    anchor_funcs = get_boss().anchor_funcs

    tam = current_toc_anchor_map(toc_anchor_map, page_list_anchor_map)
    before = after = None
    visible_anchors = {}
    has_visible = False

    for anchor_id in tam.sorted_anchors:
        pos = tam.pos_map[anchor_id]
        visibility = anchor_funcs.visibility(pos)
        if visibility < 0:
            before = anchor_id
        elif visibility is 0:
            has_visible = True
            visible_anchors[anchor_id] = True
        elif visibility > 0:
            after = anchor_id
            break
    ans = {'visible_anchors': visible_anchors, 'has_visible': has_visible, 'before': before, 'after': after, 'sorted_anchors': tam.sorted_anchors}

    before = after = None
    visible_anchors = {}
    has_visible = False
    for anchor_id in tam.page_list_sorted_anchors:
        pos = tam.page_list_pos_map[anchor_id]
        visibility = anchor_funcs.visibility(pos)
        if visibility < 0:
            before = anchor_id
        elif visibility is 0:
            has_visible = True
            visible_anchors[anchor_id] = True
        elif visibility > 0:
            after = anchor_id
            break
    ans.page_list = {
        'visible_anchors': visible_anchors,
        'has_visible': has_visible,
        'before': before,
        'after': after,
        'sorted_anchors': tam.page_list_sorted_anchors,
    }
    return ans


def find_anchor_before_range(r, toc_anchor_map, page_list_anchor_map):
    name = current_spine_item().name
    prev_anchor = None
    tam = current_toc_anchor_map(toc_anchor_map, page_list_anchor_map)
    anchors = toc_anchor_map[name]
    if anchors:
        amap = {x.id: x for x in anchors}
        for anchor_id in tam.sorted_anchors:
            anchor = amap[anchor_id]
            is_before = True
            if anchor.frag:
                elem = document.getElementById(anchor.frag)
                if elem:
                    q = document.createRange()
                    q.selectNode(elem)
                    if q.compareBoundaryPoints(window.Range.START_TO_START, r) > 0:
                        is_before = False
            if is_before:
                prev_anchor = anchor
            else:
                break
    return prev_anchor
