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

import traceback
from elementmaker import E
from gettext import gettext as _, install

import initialize  # noqa: unused-import
from ajax import absolute_path, ajax, workaround_qt_bug
from book_list.globals import get_session_data, set_session_data
from book_list.library_data import library_data
from book_list.theme import css_for_variables, get_color
from date import format_date
from dom import get_widget_css, set_css
from modals import create_modal_container
from qt import from_python, to_python
from read_book.db import new_book
from read_book.footnotes import main as footnotes_main
from read_book.globals import (
    default_color_schemes, runtime, set_system_colors, ui_operations
)
from read_book.iframe import main as iframe_main
from read_book.open_book import remove_recently_opened
from read_book.prefs.head_foot import set_time_formatter
from read_book.view import View
from session import (
    apply_reader_profile, default_interface_data, local_storage, session_defaults, settings_for_reader_profile
)
from utils import debounce, encode_query_with_path, parse_url_params
from viewer.constants import FAKE_HOST, FAKE_PROTOCOL

runtime.is_standalone_viewer = True
runtime.FAKE_HOST = FAKE_HOST
runtime.SANDBOX_HOST = FAKE_HOST.rpartition('.')[0] + '.sandbox'
runtime.FAKE_PROTOCOL = FAKE_PROTOCOL
book = None
view = None


def file_received(name, file_data, proceed, end_type, xhr, ev):
    end_type = workaround_qt_bug(xhr, end_type)
    if end_type is 'abort':
        return
    if end_type is not 'load':
        show_error(_('Failed to load file from book'), _(
            'Could not load the file: {} with error: {}').format(name, xhr.error_html))
        return
    if not xhr.responseType or xhr.responseType is 'text':
        result = xhr.responseText
    else if xhr.responseType is 'blob':
        result = xhr.response
    else:
        show_error(_('Failed to load file from book'), _(
            'Could not load the file: {} unknown response type: {}').format(name, xhr.responseType))
        return

    proceed(result, name, file_data.mimetype, book)


def get_url_for_book_file_name(name):
    return absolute_path('book/' + name)


def get_file(book, name, proceed):
    entry = book.manifest.files[name]
    if not entry:
        raise ValueError(f'No file named {name} in the book manifest')
    xhr = ajax('book/' + name, file_received.bind(None, name, entry, proceed), ok_code=0)
    if entry.is_html or entry.mimetype.startswith('text/') or entry.mimetype is 'application/javascript':
        xhr.responseType = 'text'
    else:
        xhr.responseType = 'blob'
    xhr.send()


profiles_callbacks = {'all': None, 'saved': None}


@from_python
@no_prune
def profile_response(which, x):
    if which is 'all-profiles':
        profiles_callbacks.all(x)
        profiles_callbacks.all = None
    elif which is 'save-profile':
        profiles_callbacks.saved()
        profiles_callbacks.saved = None
    elif which is 'apply-profile':
        sd = get_session_data()
        apply_reader_profile(sd, x)
        if view:
            view.preferences_changed()
        ui_operations.apply_profile(x)
    elif which is 'request-save':
        sd = get_session_data()
        settings = settings_for_reader_profile(sd)
        save_profile(x, settings, def (): pass;)


def get_all_profiles(proceed):
    profiles_callbacks.all = proceed
    to_python.profile_op('all-profiles', '', None)


def save_profile(name, settings, proceed):
    profiles_callbacks.saved = proceed
    to_python.profile_op('save-profile', name, settings)


def get_mathjax_files(proceed):
    proceed({})


def update_url_state(replace):
    if view and view.currently_showing:
        bookpos = view.currently_showing.bookpos
        if bookpos:
            query = {'bookpos': bookpos}
            query = encode_query_with_path(query)
            if replace:
                window.history.replaceState(None, '', query)
            else:
                window.history.pushState(None, '', query)


def on_pop_state():
    if view and view.currently_showing:
        data = parse_url_params()
        if data.bookpos and data.bookpos.startswith('epubcfi(/'):
            view.goto_cfi(data.bookpos)


def show_error(title, msg, details):
    to_python.show_error(title, msg, details)


def manifest_received(key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url, end_type, xhr, ev):
    nonlocal book
    end_type = workaround_qt_bug(xhr, end_type)
    if end_type is 'load':
        book = new_book(key, {})
        data = xhr.response
        book.manifest = data[0]
        book.metadata = book.manifest.metadata = data[1]
        book.manifest.pathtoebook = pathtoebook
        book.highlights = highlights
        book.stored_files = {}
        book.calibre_book_url = book_url
        book.calibre_book_in_library_url = book_in_library_url
        book.saved_reading_rates = reading_rates
        book.is_complete = True
        v'delete book.manifest["metadata"]'
        v'delete book.manifest["last_read_positions"]'
        view.display_book(book, initial_position)
    else:
        show_error(_('Could not open book'), _(
            'Failed to load book manifest, click "Show details" for more info'),
        xhr.error_html or None)


def clone(x):
    if x:
        x = window.structuredClone(x)
    return x


class SessionData:
    def __init__(self, prefs):
        defaults = session_defaults()
        self.data = {k: clone(defaults[k]) if prefs[k] is undefined else clone(prefs[k]) for k in defaults}

    def get(self, key, defval):
        ans = self.data[key]
        if ans is undefined or ans is None:
            if defval is undefined:
                defval = None
            return defval
        return ans

    def set(self, key, val):
        if val is None:
            self.data[key] = clone(session_defaults()[key])
        else:
            self.data[key] = val
        to_python.set_session_data(key, val)

    def set_bulk(self, changes):
        defaults = session_defaults()
        for key in Object.keys(changes):
            val = changes[key]
            if val is None:
                self.data[key] = clone(defaults[key])
            else:
                self.data[key] = val
        to_python.set_session_data(changes, None)

    def clear(self):
        defaults = session_defaults()
        self.data = {k: clone(defaults[k]) for k in defaults}
        to_python.set_session_data('*', None)


class LocalStorage:
    def __init__(self, data):
        self.data = data

    def get(self, key, defval):
        ans = self.data[key]
        if ans is undefined or ans is None:
            if defval is undefined:
                defval = None
            return defval
        return ans

    def set(self, key, val):
        self.data[key] = val
        to_python.set_local_storage(key, val)

    def clear(self):
        self.data = {}
        to_python.set_local_storage('*', None)


def create_session_data(prefs, local_storage_data):
    sd = SessionData(prefs)
    set_session_data(sd)
    local_storage.storage = LocalStorage(local_storage_data)


@from_python
@no_prune
def create_view(prefs, local_storage, field_metadata, ui_data):
    nonlocal view
    set_system_colors(ui_data.system_colors)
    runtime.all_font_families = ui_data.all_font_families
    library_data.field_metadata = field_metadata
    document.documentElement.style.fontFamily = f'"{ui_data.ui_font_family}", sans-serif'
    document.documentElement.style.fontSize = ui_data.ui_font_sz
    runtime.QT_VERSION = ui_data.QT_VERSION
    set_time_formatter(def (d):
        return format_date(d, ui_data.short_time_fmt)
    )
    if view is None:
        create_session_data(prefs, local_storage)
        view = View(document.getElementById('view'))
        window.addEventListener('resize', debounce(view.on_resize.bind(self), 250))
        to_python.view_created({'default_color_schemes': default_color_schemes})
        default_interface_data.use_roman_numerals_for_series_number = ui_data.use_roman_numerals_for_series_number
        if ui_data.show_home_page_on_ready:
            view.overlay.open_book(False)


@from_python
@no_prune
def set_system_palette(system_colors):
    set_system_colors(system_colors)
    if view:
        view.update_color_scheme()


@from_python
@no_prune
def highlight_action(uuid, which):
    if view:
        view.highlight_action(uuid, which)


@from_python
@no_prune
def generic_action(which, data):
    if which is 'set-notes-in-highlight':
        view.set_notes_for_highlight(data.uuid, data.notes or '')
    elif which is 'show-status-message':
        view.show_status_message(data.text)
    elif which is 'remove-recently-opened':
        remove_recently_opened(data.path)
    elif which is 'add-notes-or-create-highlight':
        view.add_notes_or_create_highlight(data.notes, data.style)


@from_python
@no_prune
def tts_event(which, data):
    view.read_aloud.handle_tts_event(which, data)


@from_python
@no_prune
def show_home_page():
    view.overlay.open_book(False)


@from_python
@no_prune
def redraw_tts_bar(pos):
    sd = get_session_data()
    sd.set('tts_bar_position', pos)
    if view:
        view.read_aloud.build_bar()


@from_python
@no_prune
def start_book_load(key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url):
    xhr = ajax(
        'manifest',
        manifest_received.bind(None, key, initial_position, pathtoebook, highlights, book_url, reading_rates, book_in_library_url),
        ok_code=0
    )
    xhr.responseType = 'json'
    xhr.send()


@from_python
@no_prune
def goto_toc_node(node_id):
    view.goto_toc_node(node_id)


@from_python
@no_prune
def goto_cfi(cfi, add_to_history):
    return view.goto_cfi(cfi, v'!!add_to_history')


@from_python
@no_prune
def full_screen_state_changed(viewer_in_full_screen):
    runtime.viewer_in_full_screen = viewer_in_full_screen


@from_python
@no_prune
def get_current_cfi(request_id):
    view.get_current_cfi(request_id, ui_operations.report_cfi)


@from_python
@no_prune
def goto_frac(frac):
    if view:
        view.goto_frac(frac)


@from_python
@no_prune
def background_image_changed(img_id, url):
    img = document.getElementById(img_id)
    if img:
        img.src = f'{url}?{Date().getTime()}'
        img.dataset.url = url


@from_python
@no_prune
def trigger_shortcut(which):
    if view:
        view.on_handle_shortcut({'name': which})


@from_python
@no_prune
def show_search_result(sr):
    if view:
        if sr.on_discovery:
            view.discover_search_result(sr)
        else:
            view.show_search_result(sr)


@from_python
@no_prune
def prepare_for_close():
    if view:
        view.prepare_for_close()
    else:
        ui_operations.close_prep_finished(None)


@from_python
@no_prune
def repair_after_fullscreen_switch():
    if view:
        view.show_status_message('...', 200)


@from_python
@no_prune
def viewer_font_size_changed():
    if view:
        view.viewer_font_size_changed()


@from_python
@no_prune
def native_gesture(gesture):
    if view:
        view.native_gesture(gesture)


source_map_set = False


def source_map_received(data, end_type, xhr, ev):
    end_type = workaround_qt_bug(xhr, end_type)
    if end_type is 'abort':
        return
    if end_type is 'load' and xhr.responseType is 'json':
        traceback.set_source_map_data(xhr.response)
    error_object = traceback.unserialize_error_for_formatting(data.error)
    details = traceback.format_exception(error_object).join('')
    show_error(data.title, data.msg, details)


def load_source_map(data):
    nonlocal source_map_set
    xhr = ajax('viewer.js.map', source_map_received.bind(None, data))
    xhr.responseType = 'json'
    xhr.send()
    source_map_set = True


def onerror(msg, script_url, line_number, column_number, error_object):
    if not error_object:
        # cross domain error
        return False
    fname = script_url.rpartition('/')[-1] or script_url
    msg += '<br><span style="font-size:smaller">' + 'Error at {}:{}:{}'.format(fname, line_number, column_number or '') + '</span>'
    details = ''
    console.log(error_object)
    title = _('Unhandled error')
    if source_map_set:
        details = traceback.format_exception(error_object).join('')
        show_error(title, msg, details)
    else:
        load_source_map({'title': title, 'msg': msg, 'error': traceback.serialize_error_for_formatting(error_object)})
    return True


if window is window.top:
    # main
    if window.calibre_translations_data:
        install(window.calibre_translations_data)
        window.calibre_translations_data = None
    ui_operations.get_all_profiles = get_all_profiles
    ui_operations.save_profile = save_profile
    ui_operations.apply_profile = def (profile):
        to_python.profile_op('apply-profile-to-viewer-ui', '', profile)
    ui_operations.get_file = get_file
    ui_operations.get_url_for_book_file_name = get_url_for_book_file_name
    ui_operations.get_mathjax_files = get_mathjax_files
    ui_operations.update_url_state = update_url_state
    ui_operations.show_error = show_error
    ui_operations.redisplay_book = def ():
        view.redisplay_book()
    ui_operations.reload_book = def ():
        to_python.reload_book()
    ui_operations.forward_gesture = def (gesture):
        view.forward_gesture(gesture)
    ui_operations.update_color_scheme = def ():
        view.update_color_scheme()
    ui_operations.update_font_size = def ():
        view.update_font_size()
    ui_operations.focus_iframe = def ():
        view.focus_iframe()
    ui_operations.goto_cfi = def (cfi):
        return view.goto_cfi(cfi)
    ui_operations.goto_frac = def (frac):
        return view.goto_frac(frac)
    ui_operations.goto_book_position = def (bpos):
        return view.goto_book_position(bpos)
    ui_operations.goto_reference = def (ref):
        return view.goto_reference(ref)
    ui_operations.toggle_toc = def ():
        to_python.toggle_toc()
    ui_operations.toggle_bookmarks = def ():
        to_python.toggle_bookmarks()
    ui_operations.toggle_highlights = def ():
        to_python.toggle_highlights()
    ui_operations.new_bookmark = def (request_id, data):
        if request_id is 'new-bookmark':
            to_python.new_bookmark(data)
    ui_operations.toggle_inspector = def ():
        to_python.toggle_inspector()
    ui_operations.content_file_changed = def (name):
        to_python.content_file_changed(name)
    ui_operations.update_last_read_position = def (cfi, pos_frac):
        to_python.update_last_read_position(cfi, pos_frac)
    ui_operations.show_search = def (text, trigger):
        to_python.show_search(text, v'!!trigger')
    ui_operations.find_next = def (previous):
        to_python.find_next(previous)
    ui_operations.reset_interface = def ():
        sd = get_session_data()
        defaults = session_defaults()
        m = sd.get('standalone_misc_settings', {})
        v'delete m.show_actions_toolbar'
        sd.set('standalone_misc_settings', m)
        sd.set('book_scrollbar', False)
        view.book_scrollbar.apply_visibility()
        sd.set('header', defaults.header)
        sd.set('footer', defaults.footer)
        view.update_header_footer()
        to_python.reset_interface()
    ui_operations.open_url = def (url):
        to_python.open_url(url)
    ui_operations.quit = def ():
        to_python.quit()
    ui_operations.toggle_lookup = def (force_show):
        to_python.toggle_lookup(v'!!force_show')
    ui_operations.selection_changed = def (selected_text, annot_id):
        to_python.selection_changed(selected_text, annot_id or None)
    ui_operations.update_current_toc_nodes = def (families):
        to_python.update_current_toc_nodes(families)
    ui_operations.toggle_full_screen = def ():
        to_python.toggle_full_screen()
    ui_operations.report_cfi = def (request_id, data):
        to_python.report_cfi(request_id, data)
    ui_operations.ask_for_open = def (path):
        to_python.ask_for_open(path)
    ui_operations.copy_selection = def (text, html):
        to_python.copy_selection(text or None, html or None)
    ui_operations.view_image = def (name):
        to_python.view_image(name)
    ui_operations.copy_image = def (name):
        to_python.copy_image(name)
    ui_operations.change_background_image = def (img_id):
        to_python.change_background_image(img_id)
    ui_operations.quit = def ():
        to_python.quit()
    ui_operations.overlay_visibility_changed = def (visible):
        to_python.overlay_visibility_changed(visible)
    ui_operations.reference_mode_changed = def (enabled):
        to_python.reference_mode_changed(enabled)
    ui_operations.show_loading_message = def (msg):
        to_python.show_loading_message(msg)
    ui_operations.export_shortcut_map = def (smap):
        to_python.export_shortcut_map(smap)
    ui_operations.print_book = def ():
        to_python.print_book()
    ui_operations.clear_history = def ():
        to_python.clear_history()
    ui_operations.customize_toolbar = def ():
        to_python.customize_toolbar()
    ui_operations.autoscroll_state_changed = def (active):
        to_python.autoscroll_state_changed(active)
    ui_operations.read_aloud_state_changed = def (active):
        to_python.read_aloud_state_changed(active)
    ui_operations.search_result_not_found = def (sr):
        to_python.search_result_not_found(sr)
    ui_operations.search_result_discovered = def (sr):
        to_python.search_result_discovered(sr)
    ui_operations.scrollbar_context_menu = def (x, y, frac):
        to_python.scrollbar_context_menu(x, y, frac)
    ui_operations.close_prep_finished = def (cfi):
        to_python.close_prep_finished(cfi)
    ui_operations.annots_changed = def (amap):
        if amap.highlight:
            to_python.highlights_changed(amap.highlight)
    ui_operations.speak_simple_text = def (text):
        to_python.speak_simple_text(text)
    ui_operations.tts = def (action, data):
        to_python.tts(action, data or v'{}')
    ui_operations.edit_book = def (spine_name, frac, selected_text):
        to_python.edit_book(spine_name, frac, selected_text or '')
    ui_operations.show_book_folder = def ():
        to_python.show_book_folder()
    ui_operations.show_help = def (which):
        to_python.show_help(which)
    ui_operations.on_iframe_ready = def ():
        to_python.on_iframe_ready()
    ui_operations.update_reading_rates = def (rates):
        to_python.update_reading_rates(rates)
    ui_operations.reset_reading_rates = def ():
        to_python.reset_reading_rates()
    document.addEventListener('focusin', def (ev):
        elem = ev.target
        if elem and (elem.isContentEditable or (elem.nodeName is 'TEXTAREA' or elem.nodeName is 'INPUT')):
            to_python.html_input_focusin()
    , True)
    document.body.appendChild(E.div(id='view'))
    window.onerror = onerror
    create_modal_container()
    document.head.appendChild(E.style(css_for_variables() + '\n\n' + get_widget_css()))
    set_css(document.body, background_color=get_color('window-background'), color=get_color('window-foreground'))
    setTimeout(def ():
            window.onpopstate = on_pop_state
        , 0)  # We do this after event loop ticks over to avoid catching popstate events that some browsers send on page load
else:
    # iframe
    if document.location.pathname.endsWith('/__index__'):
        iframe_main()
    elif document.location.pathname.endsWith('/__popup__'):
        footnotes_main()
