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

from dom import clear
from iframe_comm import IframeClient
from read_book.globals import runtime, current_spine_item, set_current_spine_item
from read_book.resources import finalize_resources, unserialize_html
from read_book.settings import (
    apply_settings, set_color_scheme_class, update_settings
)

block_names = dict.fromkeys(['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body'], True).as_object()
block_display_styles = dict.fromkeys(['block', 'list-item', 'table-cell', 'table'], True).as_object()


def elem_roles(elem):
    return {k.toLowerCase(): True for k in (elem.getAttribute('role') or '').split(' ')}


def epub_type(elem):
    for a in elem.attributes:
        if a.nodeName.toLowerCase() == 'epub:type' and a.nodeValue:
            return a.nodeValue


def get_containing_block(node):
    while node and node.tagName and block_names[node.tagName.toLowerCase()] is not True:
        node = node.parentNode
    return node


def is_footnote_link(a, dest_name, dest_frag, src_name, link_to_map):
    roles = elem_roles(a)
    if roles['doc-noteref'] or roles['doc-biblioref'] or roles['doc-glossref']:
        return True
    if roles['doc-link']:
        return False
    if epub_type(a) is 'noteref':
        return True

    # Check if node or any of its first few parents have vertical-align set
    x, num = a, 3
    while x and num > 0:
        style = window.getComputedStyle(x)
        if not is_footnote_link.inline_displays[style.display]:
            break
        if is_footnote_link.vert_aligns[style.verticalAlign]:
            return True
        x = x.parentNode
        num -= 1

    # Check if node has a single child with the appropriate css
    children = [x for x in a.childNodes if x.nodeType is Node.ELEMENT_NODE]
    if children.length == 1:
        style = window.getComputedStyle(children[0])
        if is_footnote_link.inline_displays[style.display] and is_footnote_link.vert_aligns[style.verticalAlign]:
            text_children = [x for x in a.childNodes if x.nodeType is Node.TEXT_NODE and x.nodeValue and /\S+/.test(x.nodeValue)]
            if not text_children.length:
                return True

    eid = a.getAttribute('id') or a.getAttribute('name')
    files_linking_to_self = link_to_map[src_name]
    if eid and files_linking_to_self:
        files_linking_to_anchor = files_linking_to_self[eid] or []
        if files_linking_to_anchor.length > 1 or (files_linking_to_anchor.length == 1 and files_linking_to_anchor[0] is not src_name):
            # An <a href="..." id="..."> link that is linked back from some other
            # file in the spine, most likely an endnote. We exclude links that are
            # the only content of their parent block tag, as these are not likely
            # to be endnotes.
            cb = get_containing_block(a)
            if not cb or cb.tagName.toLowerCase() == 'body':
                return False
            ltext = a.textContent
            if not ltext:
                return False
            ctext = cb.textContent
            if not ctext:
                return False
            if ctext.strip() is ltext.strip():
                return False
            return True

    return False


is_footnote_link.inline_displays = {'inline': True, 'inline-block': True}
is_footnote_link.vert_aligns = {'sub': True, 'super': True, 'top': True, 'bottom': True}


def is_epub_footnote(node):
    et = epub_type(node)
    if et:
        et = et.toLowerCase()
        if et is 'note' or et is 'footnote' or et is 'rearnote':
            return True
    roles = elem_roles(node)
    if roles['doc-note'] or roles['doc-footnote'] or roles['doc-rearnote']:
        return True
    return False


def get_note_container(node):
    while node and block_names[node.tagName.toLowerCase()] is not True and block_display_styles[window.getComputedStyle(node).display] is not True:
        node = node.parentNode
    return node


def get_parents_and_self(node):
    ans = []
    while node and node is not document.body:
        ans.push(node)
        node = node.parentNode
    return ans


def get_page_break(node):
    style = window.getComputedStyle(node)
    return {
        'before': get_page_break.on[style.getPropertyValue('page-break-before')] is True,
        'after': get_page_break.on[style.getPropertyValue('page-break-after')] is True,
    }
get_page_break.on = {'always': True, 'left': True, 'right': True}


def hide_children(node):
    for child in node.childNodes:
        if child.nodeType is Node.ELEMENT_NODE:
            if child.do_not_hide:
                hide_children(child)
                v'delete child.do_not_hide'
            else:
                child.style.display = 'none'


def unhide_tree(elem):
    elem.do_not_hide = True
    for c in elem.getElementsByTagName('*'):
        c.do_not_hide = True


ok_list_types = {'disc': True, 'circle': True, 'square': True}


def is_new_footnote_start(elem, start_elem):
    c = get_note_container(elem)
    return c is not start_elem and c is not start_elem.firstElementChild


def show_footnote(target, known_anchors):
    if not target:
        return
    start_elem = document.getElementById(target)
    if not start_elem:
        return
    start_elem = get_note_container(start_elem)
    for elem in get_parents_and_self(start_elem):
        elem.do_not_hide = True
        style = window.getComputedStyle(elem)
        if style.display is 'list-item' and ok_list_types[style.listStyleType] is not True:
            # We cannot display list numbers since they will be
            # incorrect as we are removing siblings of this element.
            elem.style.listStyleType = 'none'
    if is_epub_footnote(start_elem):
        unhide_tree(start_elem)
    else:
        # Try to detect natural boundaries based on markup for this note
        found_note_start = False
        for elem in document.documentElement.getElementsByTagName('*'):
            if found_note_start:
                eid = elem.getAttribute('id')
                if eid is not target and known_anchors[eid] and is_new_footnote_start(elem, start_elem):
                    # console.log('Breaking footnote on anchor: ' + elem.getAttribute('id'))
                    v'delete get_note_container(elem).do_not_hide'
                    break
                pb = get_page_break(elem)
                if pb.before:
                    # console.log('Breaking footnote on page break before')
                    break
                if pb.after:
                    unhide_tree(elem)
                    # console.log('Breaking footnote on page break after')
                    break
                elem.do_not_hide = True
            else if elem is start_elem:
                found_note_start = True

    hide_children(document.body)
    window.location.hash = '#' + target


class PopupIframeBoss:
    def __init__(self):
        handlers = {
            'initialize': self.initialize,
            'clear': self.on_clear,
            'display': self.display,
        }
        self.comm = IframeClient(handlers, 'popup-iframe')
        self.blob_url_map = {}
        self.name = None
        self.frag = None
        self.link_attr = None

    def initialize(self, data):
        window.addEventListener('error', self.on_error)
        window.addEventListener('keydown', self.on_keydown)
        document.addEventListener('contextmenu', def (ev):
            ev.preventDefault()
        )

    def on_keydown(self, ev):
        if ev.key is 'Escape':
            self.comm.send_message('close_requested', from_keypress=True)

    def on_error(self, evt):
        msg = evt.message
        script_url = evt.filename
        line_number = evt.lineno
        column_number = evt.colno
        error_object = evt.error
        evt.stopPropagation(), evt.preventDefault()
        if error_object is None:
            # This happens for cross-domain errors (probably javascript injected
            # into the browser via extensions/ userscripts and the like). It also
            # happens all the time when using Chrome on Safari
            console.log(f'Unhandled error from external javascript, ignoring: {msg} {script_url} {line_number}:{column_number}')
        else:
            console.log(error_object)

    def display(self, data):
        self.book = data.book
        self.name = data.name
        self.frag = data.frag
        self.link_attr = 'data-' + self.book.manifest.link_uid
        spine = self.book.manifest.spine
        index = spine.indexOf(data.name)
        set_current_spine_item({
            'name': data.name,
            'is_first': index is 0,
            'is_last': index is spine.length - 1,
            'index': index,
            'initial_position': data.initial_position
        })
        update_settings(data.settings)
        for name in self.blob_url_map:
            window.URL.revokeObjectURL(self.blob_url_map[name])
        document.body.style.removeProperty('font-family')
        root_data, self.mathjax, self.blob_url_map = finalize_resources(self.book, data.name, data.resource_data)
        self.resource_urls = unserialize_html(root_data, self.content_loaded, self.show_only_footnote, data.name)

    def connect_links(self):
        for a in document.body.querySelectorAll(f'a[{self.link_attr}]'):
            a.addEventListener('click', self.link_activated)
        if runtime.is_standalone_viewer:
            # links with a target get turned into requests to open a new window by Qt
            for a in document.body.querySelectorAll('a[target]'):
                a.removeAttribute('target')

    def link_activated(self, evt):
        try:
            data = JSON.parse(evt.currentTarget.getAttribute(self.link_attr))
        except:
            print('WARNING: Failed to parse link data {}, ignoring'.format(evt.currentTarget?.getAttribute?(self.link_attr)))
            return
        self.activate_link(data.name, data.frag, evt.currentTarget)

    def activate_link(self, name, frag, target_elem):
        if not name:
            name = current_spine_item().name
        try:
            is_popup = is_footnote_link(target_elem, name, frag, current_spine_item().name, self.book.manifest.link_to_map or {})
            title = target_elem.textContent
        except:
            import traceback
            traceback.print_exc()
            is_popup = False
            title = ''
        self.comm.send_message('link_activated', is_popup=is_popup, name=name, frag=frag, title=title)

    def on_clear(self, data):
        clear(document.head)
        clear(document.body)
        document.body.textContent = data.text
        self.name = None
        self.frag = None

    def show_only_footnote(self):
        known_anchors = {}
        ltm = self.book.manifest.link_to_map?[self.name]
        if ltm:
            known_anchors = {k: True for k in Object.keys(ltm)}
        show_footnote(self.frag, known_anchors)

    def content_loaded(self):
        if not self.comm.encrypted_communications:
            window.setTimeout(self.content_loaded, 2)
            return
        self.connect_links()
        # this is the loading styles used to suppress scrollbars during load
        # added in unserialize_html
        document.head.removeChild(document.head.firstChild)
        document.body.classList.add('calibre-footnote-container')
        set_color_scheme_class()
        apply_settings(True)
        self.comm.send_message('content_loaded', height=document.documentElement.scrollHeight + 25)


@no_prune
def main():
    main.boss = PopupIframeBoss()
