# 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': [],
    'force_refresh': False,
    'bools_are_tristate': True,
    'field_names': {},
    'available_browse_fields': [],
    'browse_fields': [],
}
BROWSE_VISIBLE_FIELDS_SETTING = 'browse_visible_fields'


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 = []
    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 book_details_vertical_categories fts_enabled fields_that_support_notes'
        ' categories_using_hierarchy'
    ).split(' '):
        library_data[key] = data[key]
    library_data.available_browse_fields = data.browse_fields or []
    update_visible_browse_fields()
    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
    refresh_sidebar_navigation()
    refresh_browse_field_settings()


def configured_browse_fields():
    by_library = get_session_data().get(BROWSE_VISIBLE_FIELDS_SETTING) or {}
    return by_library[current_library_id()]


def configured_browse_field_map():
    configured = configured_browse_fields()
    if not Array.isArray(configured):
        return None
    ans = {}
    for key in configured:
        ans[key] = True
    return ans


def update_visible_browse_fields():
    fields = library_data.available_browse_fields or []
    selected = configured_browse_field_map()
    if selected is None:
        library_data.browse_fields = [field for field in fields if field.default_visible is not False]
    else:
        library_data.browse_fields = [field for field in fields if selected[field.key]]


def set_browse_fields(field_keys):
    by_library = get_session_data().get(BROWSE_VISIBLE_FIELDS_SETTING) or {}
    by_library[current_library_id()] = field_keys
    get_session_data().set(BROWSE_VISIBLE_FIELDS_SETTING, by_library)
    update_visible_browse_fields()
    refresh_sidebar_navigation()


def refresh_sidebar_navigation():
    try:
        refresh = window.cs_refresh_sidebar_navigation
    except Exception:
        refresh = None
    if refresh:
        refresh()


def refresh_browse_field_settings():
    try:
        refresh = window.cs_refresh_browse_field_settings
    except Exception:
        refresh = None
    if refresh:
        refresh()


def browse_field_settings():
    ans = []
    selected = configured_browse_field_map()
    for field in library_data.available_browse_fields or []:
        f = Object.assign({}, field)
        f.enabled = (field.default_visible is not False) if selected is None else bool(selected[f.key])
        ans.push(f)
    return ans


def browse_fields():
    return library_data.browse_fields or []


def browse_field_for(field):
    for f in browse_fields():
        if f.key is field:
            return f


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 not library_data.field_names:
        library_data.field_names = {}
    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': []}
        library_data.field_names[field].loading = True


def thumbnail_url(book_id, width, height):
    query = f'sz={Math.ceil(width * window.devicePixelRatio)}x{Math.ceil(height * window.devicePixelRatio)}'
    prefix = f'get/thumb/{book_id}'
    lid = loaded_books_query().library_id or current_library_id()
    if lid:
        prefix += f'/{lid}'
    return absolute_path(f'{prefix}?{query}')


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


def download_url(book_id, fmt, content_disposition):
    lid = current_library_id()
    ans = absolute_path(f'get/{fmt}/{book_id}/{lid}')
    if content_disposition:
        ans += f'?content_disposition={content_disposition}'
    return ans


def download_data_file_url(book_id, relpath, content_disposition):
    lid = current_library_id()
    rpath = encodeURIComponent(relpath)
    ans = absolute_path(f'data-files/get/{book_id}/{rpath}/{lid}')
    if content_disposition:
        ans += f'?content_disposition={content_disposition}'
    return ans


def web_search_url(book_id, field, item_val):
    return absolute_path(
        f'web-search/{book_id}/{encodeURIComponent(field)}/{encodeURIComponent(item_val)}'
        + f'?library_id={encodeURIComponent(current_library_id())}'
    )


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=256):
        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': [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 = 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 = []
    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()
