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

from gettext import gettext as _

from ajax import absolute_path, ajax
from book_list.globals import get_session_data
from lru_cache import LRUCache
from session import get_interface_data
from utils import parse_url_params

load_status = {'loading':True, 'ok':False, 'error_html':None, 'current_fetch': None, 'http_error_code': 0}
library_data = {'metadata':{}, 'previous_book_ids': v'[]', 'force_refresh': False, 'bools_are_tristate': True}


def current_library_id():
    q = parse_url_params()
    return q.library_id or get_interface_data().default_library_id


def all_libraries():
    interface_data = get_interface_data()
    lids = sorted(interface_data.library_map, key=def(x): return interface_data.library_map[x];)
    return [(lid, interface_data.library_map[lid]) for lid in lids]


def current_virtual_library():
    q = parse_url_params()
    return q.vl or ''


def last_virtual_library_for(library_id):
    if last_virtual_library_for.library_id is library_id:
        return last_virtual_library_for.vl or ''
    return ''


def url_books_query(sd):
    q = parse_url_params()
    sd = sd or get_session_data()
    lid = current_library_id()
    return {
        'library_id': lid,
        'sort': q.sort or sd.get_library_option(lid, 'sort'),
        'search': q.search,
        'vl': current_virtual_library(),
    }


def loaded_books_query():
    sr = library_data.search_result
    sort = None
    if sr:
        sort = [s + '.' + o for s, o in zip(sr.sort.split(','), sr.sort_order.split(','))].join(',')
    return {
        'library_id': sr.library_id if sr else None,
        'sort': sort,
        'search': sr?.query,
        'vl':sr?.vl
    }


def current_sorted_field():
    if library_data.search_result:
        return library_data.search_result.sort, library_data.search_result.sort_order
    sort = url_books_query().sort.partition(',')[0]
    csf = sort.partition('.')[0]
    csfo = sort.partition('.')[2] or 'asc'
    return csf, csfo


def all_virtual_libraries():
    return library_data.virtual_libraries or {}


def update_library_data(data):
    load_status.loading = False
    load_status.ok = True
    load_status.error_html = None
    load_status.http_error_code = 0
    library_data.previous_book_ids = v'[]'
    if library_data.for_library is not current_library_id():
        library_data.field_names = {}
    library_data.for_library = current_library_id()
    for key in 'search_result sortable_fields field_metadata metadata virtual_libraries book_display_fields bools_are_tristate'.split(' '):
        library_data[key] = data[key]
    sr = library_data.search_result
    if sr:
        last_virtual_library_for.library_id = sr.library_id
        last_virtual_library_for.vl = sr.vl
    else:
        last_virtual_library_for.library_id = None
        last_virtual_library_for.vl = None


def add_more_books(data):
    for key in data.metadata:
        library_data.metadata[key] = data.metadata[key]
    sr = library_data.search_result
    if sr and sr.book_ids and sr.book_ids.length > 0:
        library_data.previous_book_ids = library_data.previous_book_ids.concat(sr.book_ids)
    library_data.search_result = data.search_result


def current_book_ids():
    return library_data.previous_book_ids.concat(library_data.search_result.book_ids)


def remove_from_array(array, item):
    while True:
        idx = array.indexOf(item)
        if idx < 0:
            break
        array.splice(idx, 1)


def remove_book(book_id):
    book_id = int(book_id)
    if library_data.previous_book_ids:
        remove_from_array(library_data.previous_book_ids, book_id)
    if library_data.search_result?.book_ids:
        remove_from_array(library_data.search_result.book_ids, book_id)


def book_after(book_id, delta):
    if not delta:
        delta = 1
    ids = current_book_ids()
    idx = ids.indexOf(int(book_id))
    if idx > -1:
        new_idx = (idx + ids.length + delta) % ids.length
        return ids[new_idx]


def on_data_loaded(end_type, xhr, ev):
    load_status.current_fetch = None
    def bad_load(msg):
        load_status.ok = False
        load_status.loading = False
        load_status.error_html = msg or xhr.error_html
        load_status.http_error_code = xhr.status

    if end_type is 'load':
        data = JSON.parse(xhr.responseText)
        if data.bad_restriction:
            bad_load(_('The library restriction for the current user is invalid: {}').format(data.bad_restriction))
        else:
            update_library_data(data)
            sd = get_session_data()
            q = loaded_books_query()
            sd.set_library_option(q.library_id, 'sort', q.sort)
    elif end_type is 'abort':
        pass
    else:
        bad_load()


def fetch_init_data():
    if load_status.current_fetch:
        load_status.current_fetch.abort()
    query = url_books_query()
    load_status.loading = True
    load_status.ok = False
    load_status.error_html = None
    load_status.http_error_code = 0
    load_status.current_fetch = ajax('interface-data/books-init', on_data_loaded, query=query)
    load_status.current_fetch.send()


def field_names_received(library_id, field, proceed, end_type, xhr, event):
    if library_id is not current_library_id():
        return
    if end_type is not 'load':
        if end_type is not 'abort':
            proceed(False, field, xhr.error_html)
        return
    try:
        names = JSON.parse(xhr.responseText)
    except Exception:
        import traceback
        traceback.print_exc()
        proceed(False, field, 'Invalid JSON from server')
        return
    library_data.field_names[field] = {
        'loading': False,
        'loaded': True,
        'last_updated_at': Date.now(),
        'names': names
    }
    proceed(True, field, names)


def field_names_for(field, proceed):
    if library_data.field_names[field] and library_data.field_names[field].loaded:
        proceed(True, field, library_data.field_names[field].names)
    if not library_data.field_names[field] or (not library_data.field_names[field].loading and Date.now() - library_data.field_names[field].last_updated_at > 3600 * 1000):

        ajax(f'interface-data/field-names/{encodeURIComponent(field)}', field_names_received.bind(None, current_library_id(), field, proceed), query={'library_id': current_library_id()}).send()
        if not library_data.field_names[field]:
            library_data.field_names[field] = {'last_updated_at': 0, 'loaded': False, 'names': v'[]'}
        library_data.field_names[field].loading = True


def thumbnail_url(book_id, width, height):
    return absolute_path(
        'get/thumb/{}/{}?sz={}x{}'.format(
            book_id, loaded_books_query().library_id,
            Math.ceil(width * window.devicePixelRatio), Math.ceil(height * window.devicePixelRatio)
        ))


def cover_url(book_id):
    lid = current_library_id()
    return absolute_path(f'get/cover/{book_id}/{lid}')


def download_url(book_id, fmt):
    lid = current_library_id()
    return absolute_path(f'get/{fmt}/{book_id}/{lid}')


def book_metadata(book_id):
    return library_data.metadata[book_id]


def set_book_metadata(book_id, value):
    library_data.metadata[book_id] = value


def loaded_book_ids():
    return Object.keys(library_data.metadata)


def force_refresh_on_next_load():
    library_data.force_refresh = True


def ensure_current_library_data():
    fr = library_data.force_refresh
    library_data.force_refresh = False

    def is_same(a, b):
        if not a and not b:
            return True
        return a is b


    if fr:
        matches = False
    else:
        q = url_books_query()
        loaded = loaded_books_query()
        matches = True

        for key in q:
            if not is_same(q[key], loaded[key]):
                matches = False
                break
    if not matches:
        fetch_init_data()


class ThumbnailCache:

    # Cache to prevent browser from issuing HTTP requests when thumbnails pages
    # are destroyed/rebuilt.

    def __init__(self, size=250):
        self.cache = LRUCache(size)

    def get(self, book_id, width, height, callback):
        url = thumbnail_url(book_id, width, height)
        item = self.cache.get(url)
        if not item:
            img = new Image()
            item = {'img':img, 'load_type':None, 'callbacks':v'[callback]'}
            img.onerror = self.load_finished.bind(None, item, 'error')
            img.onload = self.load_finished.bind(None, item, 'load')
            img.onabort = self.load_finished.bind(None, item, 'abort')
            img.dataset.bookId = str(book_id)
            img.src = url
            self.cache.set(url, item)
            return img
        if item.load_type is None:
            if item.callbacks.indexOf(callback) < 0:
                item.callbacks.push(callback)
        else:
            callback(item.img, item.load_type)
        return item.img

    def load_finished(self, item, load_type):
        item.load_type = load_type
        img = item.img
        img.onload = img.onerror = img.onabort = None
        for callback in item.callbacks:
            callback(img, load_type)

thumbnail_cache = ThumbnailCache()


def sync_library_books(library_id, to_sync, callback):
    url = f'book-get-annotations/{library_id}/'
    which = v'[]'
    lrmap = {}
    for key, last_read in to_sync:
        library_id, book_id, fmt = key
        fmt = fmt.upper()
        which.push(f'{book_id}-{fmt}')
        lrmap[f'{book_id}:{fmt}'] = last_read
    url += which.join('_')
    ajax(url, callback.bind(None, library_id, lrmap)).send()
