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

from gettext import gettext as _

from read_book.cfi import create_cfi_cmp, cfi_sort_key
from read_book.globals import ui_operations


no_cfi = '/99999999'


def bookmark_get_cfi(b):
    if b.pos_type is 'epubcfi':
        return b.pos[8:-1]
    return no_cfi


def highlight_get_cfi(hl):
    cfi = hl.start_cfi
    if cfi:
        si = f'/{hl.spine_index}' if hl.spine_index? else no_cfi
        return si + cfi
    return no_cfi


def sort_annot_list(annots, get_cfi_func):
    key_map = {no_cfi: cfi_sort_key(no_cfi)}
    for annot in annots:
        cfi = get_cfi_func(annot)
        if cfi and not key_map[cfi]:
            key_map[cfi] = cfi_sort_key(cfi)
    cfi_cmp = create_cfi_cmp(key_map)
    annots.sort(def (a, b):
        acfi = get_cfi_func(a)
        bcfi = get_cfi_func(b)
        return cfi_cmp(acfi, bcfi)
    )


def annots_descending_cmp(a, b):
    return -1 if a.timestamp > b.timestamp else (1 if a.timestamp < b.timestamp else 0)


def merge_annots_with_identical_field(annots_a, annots_b, field, cfi_getter_func):
    title_groups = {}
    changed = False
    all_annots = annots_a.concat(annots_b)
    for a in all_annots:
        q = title_groups[a[field]]
        if not q:
            q = title_groups[a[field]] = []
        q.push(a)
    for tg in Object.values(title_groups):
        tg.sort(annots_descending_cmp)
    seen = {}
    ans = []
    for a in all_annots:
        title = a[field]
        if not seen[title]:
            seen[title] = True
            candidates = title_groups[title]
            if not changed and candidates.length > 1 and candidates[0].timestamp is not candidates[1].timestamp:
                changed = True
            ans.push(title_groups[title][0])
    if ans.length is not annots_a.length or ans.length is not annots_b.length:
        changed = True
    if changed:
        sort_annot_list(ans, cfi_getter_func)
    return changed, ans


field_map = {'bookmark': 'title', 'highlight': 'uuid'}
getter_map = {'bookmark': bookmark_get_cfi, 'highlight': highlight_get_cfi}


def merge_annot_lists(a, b, field):
    key_field = field_map[field]
    return merge_annots_with_identical_field(a, b, key_field, getter_map[field])


def merge_annotation_maps(a, b):
    # If you make changes to this algorithm also update the
    # implementation in calibre.db.annotations
    updated = False
    ans = {}
    for field in field_map:
        a_items = a[field] or []
        b_items = b[field] or []
        if not a_items.length:
            ans[field] = b_items
            updated = True
            continue
        if not b_items.length:
            ans[field] = a_items
            continue
        changed, ans[field] = merge_annot_lists(a_items, b_items, field)
        if changed:
            updated = True

    return updated, ans


class AnnotationsManager:  # {{{
    def __init__(self, view):
        self.view = view
        self.set_highlights()
        self.set_bookmarks()

    def sync_annots_to_server(self, which):
        if ui_operations.annots_changed:
            amap = {}
            if not which or which is 'bookmarks':
                amap.bookmark = self.bookmarks.as_array()
            if not which or which is 'highlights':
                amap.highlight = Object.values(self.highlights)
            ui_operations.annots_changed(amap)

    def set_bookmarks(self, bookmarks):
        bookmarks = bookmarks or []
        self.bookmarks = list(bookmarks)

    def all_bookmarks(self):
        return self.bookmarks

    def add_bookmark(self, title, cfi):
        if not title or not cfi:
            return
        self.bookmarks = [b for b in self.bookmarks if b.title is not title]
        self.bookmarks.push({
            'type': 'bookmark',
            'timestamp': Date().toISOString(),
            'pos_type': 'epubcfi',
            'pos': cfi,
            'title': title,
        })
        sort_annot_list(self.bookmarks, bookmark_get_cfi)
        self.sync_annots_to_server('bookmarks')

    def remove_bookmark(self, title):
        changed = False
        for b in self.bookmarks:
            if b.title is title:
                b.removed = True
                b.timestamp = Date().toISOString()
                changed = True
        if changed:
            self.sync_annots_to_server('bookmarks')
        return changed

    def edit_bookmark(self, title, new_title):
        changed = False
        for b in self.bookmarks:
            if b.title is title:
                b.title = new_title
                b.timestamp = Date().toISOString()
                changed = True
        if changed:
            self.sync_annots_to_server('bookmarks')
        return changed

    def default_bookmark_title(self, base_default_title):
        all_titles = {bm.title: True for bm in self.bookmarks if not bm.removed}
        base_default_title = base_default_title or _('Bookmark')
        c = 0
        while True:
            c += 1
            default_title = f'{base_default_title} #{c}'
            if not all_titles[default_title]:
                return default_title

    def set_highlights(self, highlights):
        highlights = highlights or []
        self.highlights = {h.uuid: h for h in highlights}

    def all_highlights(self):
        ans = [h for h in Object.values(self.highlights) if not h.removed].as_array()
        sort_annot_list(ans, highlight_get_cfi)
        return ans

    def merge_highlights(self, highlights):
        highlights = highlights or []
        updated = False
        if highlights.length:
            base = {'highlight': Object.values(self.highlights)}
            newvals = {'highlight': highlights}
            updated, ans = merge_annotation_maps(base, newvals)
            if updated:
                self.set_highlights(ans.highlight)
        return updated

    def remove_highlight(self, uuid):
        h = self.highlights[uuid]
        if h:
            h.timestamp = Date().toISOString()
            h.removed = True
            v'delete h.style'
            v'delete h.highlighted_text'
            v'delete h.start_cfi'
            v'delete h.end_cfi'
            v'delete h.notes'
            v'delete h.spine_name'
            v'delete h.spine_index'
            v'delete h.toc_family_titles'
            return True

    def delete_highlight(self, uuid):
        if self.remove_highlight(uuid):
            self.sync_annots_to_server('highlights')

    def notes_for_highlight(self, uuid):
        h = self.highlights[uuid] if uuid else None
        if h:
            return h.notes

    def text_for_highlight(self, uuid):
        h = self.highlights[uuid] if uuid else None
        if h:
            return h.highlighted_text

    def set_notes_for_highlight(self, uuid, notes):
        h = self.highlights[uuid]
        if h:
            if notes:
                h.notes = notes
            else:
                v'delete h.notes'
            self.sync_annots_to_server('highlights')
            return True
        return False

    def style_for_highlight(self, uuid):
        h = self.highlights[uuid]
        if h:
            return h.style

    def data_for_highlight(self, uuid):
        return self.highlights[uuid]

    def spine_index_for_highlight(self, uuid, spine):
        h = self.highlights[uuid]
        if not h:
            return -1
        ans = h.spine_index
        name = h.spine_name
        if name:
            idx = spine.indexOf(name)
            if idx > -1:
                ans = idx
        return ans

    def cfi_for_highlight(self, uuid, spine_index):
        h = self.highlights[uuid]
        if h:
            x = 2 * (spine_index + 1)
            return f'epubcfi(/{x}{h.start_cfi})'

    def add_highlight(self, msg, style, notes, toc_family):
        now = Date().toISOString()
        for uuid in msg.removed_highlights:
            self.remove_highlight(uuid)

        annot = self.highlights[msg.uuid] = {
            'type': 'highlight',
            'timestamp': now,
            'uuid': msg.uuid,
            'highlighted_text': msg.highlighted_text,
            'start_cfi': msg.bounds.start,
            'end_cfi': msg.bounds.end,
            'style': style,  # dict with color and background-color
            'spine_name': self.view.currently_showing.name,
            'spine_index': self.view.currently_showing.spine_index,
        }
        if notes:
            annot.notes = notes
        if toc_family?.length:
            toc_family_titles = []
            for x in toc_family:
                if x.title:
                    toc_family_titles.push(x.title)
            annot.toc_family_titles = toc_family_titles
        self.sync_annots_to_server('highlights')

    def highlights_for_currently_showing(self):
        name = self.view.currently_showing.name
        ans = []
        for h in Object.values(self.highlights):
            if h.spine_name is name and not h.removed and h.start_cfi:
                ans.push(h)
        return ans
        # }}}
