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

from elementmaker import E

import traceback
from ajax import ajax, ajax_send
from book_list.constants import read_book_container_id
from book_list.library_data import current_library_id, library_data
from book_list.router import home, push_state, read_book_mode, update_window_title
from book_list.ui import show_panel
from dom import clear
from gettext import gettext as _
from modals import create_simple_dialog_markup, error_dialog
from read_book.db import get_db
from read_book.globals import ui_operations
from read_book.tts import Client
from read_book.view import View
from session import get_interface_data
from utils import debounce, full_screen_element, human_readable, request_full_screen
from widgets import create_button

RENDER_VERSION = __RENDER_VERSION__
MATHJAX_VERSION = "__MATHJAX_VERSION__"

class ReadUI:

    def __init__(self):
        self.base_url_data = {}
        self.current_metadata = {'title': _('Unknown book')}
        self.current_book_id = None
        self.manifest_xhr = None
        self.pending_load = None
        self.downloads_in_progress = []
        self.progress_id = 'book-load-progress'
        self.display_id = 'book-iframe-container'
        self.error_id = 'book-global-error-container'
        self.stacked_widgets = [self.progress_id, self.display_id, self.error_id]
        container = document.getElementById(read_book_container_id)

        container.appendChild(E.div(
            id=self.progress_id, style='display:none; text-align: center',
            E.h3(style='margin-top:30vh; margin-bottom: 1ex;'),
            E.progress(style='margin: 1ex'),
            E.div(style='margin: 1ex')
        ))

        container.appendChild(E.div(
            id=self.error_id, style='display:none;',
        ))

        container.appendChild(E.div(
            id=self.display_id, style='display:none',
        ))
        self.view = View(container.lastChild)
        self.tts_client = Client()
        self.windows_to_listen_for_messages_from = []
        window.addEventListener('resize', debounce(self.on_resize.bind(self), 250))
        window.addEventListener('message', self.message_from_other_window.bind(self))
        self.db = get_db(self.db_initialized.bind(self), self.show_error.bind(self))
        ui_operations.get_file = self.db.get_file
        ui_operations.get_mathjax_files = self.db.get_mathjax_files
        ui_operations.update_url_state = self.update_url_state.bind(self)
        ui_operations.update_last_read_time = self.db.update_last_read_time
        ui_operations.show_error = self.show_error.bind(self)
        ui_operations.update_reading_rates = self.update_reading_rates.bind(self)
        ui_operations.redisplay_book = self.redisplay_book.bind(self)
        ui_operations.reload_book = self.reload_book.bind(self)
        ui_operations.forward_gesture = self.forward_gesture.bind(self)
        ui_operations.update_color_scheme = self.update_color_scheme.bind(self)
        ui_operations.update_font_size = self.update_font_size.bind(self)
        ui_operations.goto_cfi = self.goto_cfi.bind(self)
        ui_operations.goto_frac = self.goto_frac.bind(self)
        ui_operations.goto_book_position = self.goto_book_position.bind(self)
        ui_operations.goto_reference = self.goto_reference.bind(self)
        ui_operations.delete_book = self.delete_book.bind(self)
        ui_operations.focus_iframe = self.focus_iframe.bind(self)
        ui_operations.toggle_toc = self.toggle_toc.bind(self)
        ui_operations.toggle_full_screen = self.toggle_full_screen.bind(self)
        ui_operations.annots_changed = self.annots_changed.bind(self)
        ui_operations.annotations_synced = self.annotations_synced.bind(self)
        ui_operations.wait_for_messages_from = self.wait_for_messages_from.bind(self)
        ui_operations.stop_waiting_for_messages_from = self.stop_waiting_for_messages_from.bind(self)
        ui_operations.update_metadata = self.update_metadata.bind(self)
        ui_operations.close_book = self.close_book.bind(self)
        ui_operations.copy_image = self.copy_image.bind(self)
        ui_operations.view_image = self.view_image.bind(self)
        ui_operations.speak_simple_text = self.speak_simple_text.bind(self)
        ui_operations.tts = self.tts.bind(self)
        ui_operations.search_result_discovered = self.view.search_overlay.search_result_discovered
        ui_operations.search_result_not_found = self.view.search_overlay.search_result_not_found
        ui_operations.find_next = self.view.search_overlay.find_next
        ui_operations.open_url = def(url):
            window.open(url, '_blank')
        ui_operations.show_help = def(which):
            if which is 'viewer':
                path = '/viewer.html'
            else:
                return
            if get_interface_data().lang_code_for_user_manual:
                path = f'/{get_interface_data().lang_code_for_user_manual}{path}'
            url = 'https://manual.calibre-ebook.com' + path
            window.open(url, '_blank')

        ui_operations.copy_selection = def(text, html):
            # try using document.execCommand which on chrome allows the
            # copy on non-secure origin if this is close to a user
            # interaction event
            active_elem = document.activeElement
            ta = document.createElement("textarea")
            ta.value = text
            ta.style.position = 'absolute'
            ta.style.top = window.innerHeight + 'px'
            ta.style.left = window.innerWidth + 'px'
            document.body.appendChild(ta)
            ta.focus()
            ta.select()
            ok = False
            try:
                ok = document.execCommand('copy')
            except:
                pass
            document.body.removeChild(ta)
            if active_elem:
                active_elem.focus()
            if ok:
                return
            if not window.navigator.clipboard:
                return error_dialog(_('No clipboard access'), _(
                    'Your browser requires you to access the Content server over an HTTPS connection'
                    ' to be able to copy to clipboard.'))
            window.navigator.clipboard.writeText(text or '').then(def (): pass;, def():
                error_dialog(_('Could not copy to clipboard'), _('No permission to write to clipboard'))
            )

    def on_resize(self):
        self.view.on_resize()

    def show_stack(self, name):
        ans = None
        for w in self.stacked_widgets:
            d = document.getElementById(w)
            v = 'none'
            if name is w:
                ans = d
                v = 'block'
            d.style.display = v
        return ans

    def show_error(self, title, msg, details):
        div = self.show_stack(self.error_id)
        clear(div)
        if self.current_metadata:
            book = self.current_metadata.title
        elif self.current_book_id:
            book = _('Book id #{}').format(self.current_book_id)
        else:
            book = _('book')

        div.appendChild(E.div(style='padding: 2ex 2rem; display:table; margin: auto'))
        div = div.lastChild
        dp = E.div()
        create_simple_dialog_markup(title, _('Could not open {}. {}').format(book, msg), details, 'bug', '', dp)
        div.appendChild(dp)
        div.appendChild(E.div(
            style='margin-top: 1ex; padding-top: 1ex; border-top: solid 1px currentColor',
            _('Go back to:'), E.br(), E.br(),
            create_button(_('Home'), 'home', def(): home(True);),
        ))
        q =  {}
        if self.current_book_id:
            q.book_id = self.current_book_id + ''
        q.close_action = 'home'
        div.lastChild.appendChild(E.span(
            '\xa0',
            create_button(_('Book list'), 'library', def(): show_panel('book_list', q, True);),
        ))
        if self.current_book_id:
            q.close_action = 'book_list'
            div.lastChild.appendChild(E.span(
                '\xa0',
                create_button(_('Book details'), 'book', def(): show_panel('book_details', q, True);),
            ))

    def init_ui(self):
        div = self.show_stack(self.progress_id)
        if self.current_metadata:
            div.firstChild.textContent = _(
                'Downloading {0} for offline reading, please wait...').format(self.current_metadata.title)
        else:
            div.firstChild.textContent = ''
        pr = div.firstChild.nextSibling
        pr.removeAttribute('value'), pr.removeAttribute('max')
        div.lastChild.textContent = _('Downloading book manifest...')

    def show_progress_message(self, msg):
        div = document.getElementById(self.progress_id)
        div.lastChild.textContent = msg or ''

    def close_book(self):
        self.base_url_data = {}

    def copy_image(self, image_file_name):
        if not self.view?.book:
            return
        ui_operations.get_file(
            self.view.book, image_file_name, def(blob, name, mimetype):
                try:
                    window.navigator.clipboard.write([new ClipboardItem({mimetype: blob})])  # noqa
                except Exception as err:
                    error_dialog(_('Could not copy image'), str(err))
        )

    def view_image(self, image_file_name):
        if not self.view?.book:
            return
        ui_operations.get_file(
            self.view.book, image_file_name, def(blob, name, mimetype):
                url = window.URL.createObjectURL(blob)
                window.open(url)
        )

    def load_book(self, library_id, book_id, fmt, metadata, force_reload):
        self.base_url_data = {'library_id': library_id, 'book_id':book_id, 'fmt':fmt}
        if not self.db.initialized:
            self.pending_load = [book_id, fmt, metadata, force_reload]
            return
        if not self.db.is_ok:
            self.show_error(_('Cannot read books'), self.db.initialize_error_msg)
            return
        self.start_load(book_id, fmt, metadata, force_reload)

    def reload_book(self):
        library_id, book_id, fmt = self.base_url_data.library_id, self.base_url_data.book_id, self.base_url_data.fmt
        metadata = self.metadata or library_data.metadata[book_id]
        self.load_book(library_id, book_id, fmt, metadata, True)

    def redisplay_book(self):
        self.view.redisplay_book()

    def forward_gesture(self, gesture):
        self.view.forward_gesture(gesture)

    def update_font_size(self):
        self.view.update_font_size()

    def goto_cfi(self, bookpos):
        return self.view.goto_cfi(bookpos)

    def goto_frac(self, frac):
        return self.view.goto_frac(frac)

    def goto_book_position(self, bpos):
        return self.view.goto_book_position(bpos)

    def goto_reference(self, ref):
        return self.view.goto_reference(ref)

    def delete_book(self, book, proceed):
        self.db.delete_book(book, proceed)

    def focus_iframe(self):
        self.view.focus_iframe()

    def toggle_toc(self):
        self.view.overlay.show_toc()

    def toggle_full_screen(self):
        if full_screen_element():
            document.exitFullscreen()
        else:
            request_full_screen(document.documentElement)

    def update_color_scheme(self):
        self.view.update_color_scheme()

    def annots_changed(self, amap):
        library_id = self.base_url_data.library_id
        book_id = self.base_url_data.book_id
        fmt = self.base_url_data.fmt
        self.db.update_annotations_data_from_key(library_id, book_id, fmt, {'annotations_map': amap})
        ajax_send(f'book-update-annotations/{library_id}/{book_id}/{fmt}', amap, def (): pass;)

    def annotations_synced(self, amap):
        library_id = self.base_url_data.library_id
        book_id = self.base_url_data.book_id
        fmt = self.base_url_data.fmt
        self.db.update_annotations_data_from_key(library_id, book_id, fmt, {'annotations_map': amap})

    @property
    def url_data(self):
        ans = {'library_id':self.base_url_data.library_id, 'book_id':self.base_url_data.book_id, 'fmt': self.base_url_data.fmt}
        bookpos = self.view.currently_showing.bookpos
        if bookpos:
            ans.bookpos = bookpos
        return ans

    def db_initialized(self):
        if not self.db.is_ok:
            error_dialog(_('Could not initialize database'), self.db.initialize_error_msg)
            return
        if self.pending_load is not None:
            pl, self.pending_load = self.pending_load, None
            if self.db.initialize_error_msg:
                self.show_error(_('Failed to initialize IndexedDB'), self.db.initialize_error_msg)
            else:
                self.start_load(*pl)

    def start_load(self, book_id, fmt, metadata, force_reload):
        self.current_book_id = book_id
        metadata = metadata or library_data.metadata[book_id]
        self.current_metadata = metadata or {'title':_('Book id #') + book_id}
        update_window_title('', self.current_metadata.title)
        self.init_ui()
        if jstype(self.db) is 'string':
            self.show_error(_('Cannot read book'), self.db)
            return
        self.db.get_book(current_library_id(), book_id, fmt, metadata, self.got_book.bind(self, force_reload))

    def got_book(self, force_reload, book):
        if not book.manifest or book.manifest.version is not RENDER_VERSION or not book.is_complete:
            # We re-download the manifest when the book is not complete to ensure we have the
            # correct manifest, even though doing so is not strictly necessary
            self.get_manifest(book, force_reload)
        else:
            self.display_book(book)

    def get_manifest(self, book, force_reload):
        library_id, book_id, fmt = book.key
        if self.manifest_xhr:
            self.manifest_xhr.abort()
        query = {'library_id': library_id}
        if force_reload:
            query.force_reload = '1'
        self.manifest_xhr = ajax(('book-manifest/' + encodeURIComponent(book_id) + '/' + encodeURIComponent(fmt)),
                                 self.got_manifest.bind(self, book), query=query)
        self.manifest_xhr.send()

    def got_manifest(self, book, end_type, xhr, ev):
        self.manifest_xhr = None
        if end_type is 'abort':
            return
        if end_type is not 'load':
            return self.show_error(_('Failed to load book manifest'),
                _('The book manifest failed to load, click "Show details" for more information.').format(title=self.current_metadata.title),
                                xhr.error_html)
        try:
            manifest = JSON.parse(xhr.responseText)
        except Exception:
            return self.show_error(_('Failed to load book manifest'),
                _('The manifest for {title} is not valid').format(title=self.current_metadata.title),
                traceback.format_exc())
        if manifest.version is not undefined:
            if manifest.version is not RENDER_VERSION:
                print('calibre upgraded: RENDER_VERSION={} manifest.version={}'.format(RENDER_VERSION, manifest.version))
                return self.show_error(_('calibre upgraded!'), _(
                    'A newer version of calibre is available, please click the Reload button in your browser.'))
            self.current_metadata = manifest.metadata
            self.db.save_manifest(book, manifest, self.download_book.bind(self, book))
            return
        # Book is still being processed
        msg = _('Downloading book manifest...')
        if manifest.job_status is 'finished':
            if manifest.aborted:
                return self.show_error(_('Failed to prepare book for reading'), _('Preparation of book for reading was aborted because it took too long'))
            if manifest.traceback:
                return self.show_error(_('Failed to prepare book for reading'), _(
                    'There was an error processing the book, click "Show details" for more information'), manifest.traceback or '')
        elif manifest.job_status is 'waiting':
            msg = _('Book is queued for processing on the server...')
        elif manifest.job_status is 'running':
            msg = _('Book is being prepared for reading on the server...')
        self.show_progress_message(msg)
        setTimeout(self.get_manifest.bind(self, book), 100)

    def download_book(self, book):
        files = book.manifest.files
        total = 0
        cover_total_updated = False
        for name in files:
            total += files[name].size
        files_left = set(book.manifest.files)
        failed_files = []
        for xhr in self.downloads_in_progress:
            xhr.abort()
        self.downloads_in_progress = []
        progress = document.getElementById(self.progress_id)
        pbar = progress.firstChild.nextSibling
        library_id, book_id, fmt = book.key
        base_path = 'book-file/{}/{}/{}/{}/'.format(encodeURIComponent(book_id), encodeURIComponent(fmt),
            encodeURIComponent(book.manifest.book_hash.size), encodeURIComponent(book.manifest.book_hash.mtime))
        query = {'library_id': library_id}
        progress_track = {}
        pbar.setAttribute('max', total + '')
        raster_cover_name = book.manifest.raster_cover_name
        if not raster_cover_name:
            nnum = 1
            base = 'raster-cover-image-{}.jpg'
            inserted_name = base.format(nnum)
            while inserted_name in files_left:
                nnum += 1
                inserted_name = base.format(nnum)
            raster_cover_name = inserted_name
            files_left.add(raster_cover_name)

        raster_cover_size = 0

        def update_progress():
            x = 0
            for name in progress_track:
                x += progress_track[name]
            pbar.setAttribute('value', x + '')
            if x is total:
                msg = _('Downloaded {}, saving to disk. This may take a few seconds...').format(human_readable(total))
            else:
                msg = _('Downloaded {0}, {1} left').format(human_readable(x), human_readable(total - x))
            progress.lastChild.textContent = msg

        def show_failure():
            det = ['<h4>{}</h4><div>{}</div><hr>'.format(fname, err_html) for fname, err_html in failed_files].join('')
            self.show_error(_('Could not download book'), _(
                'Failed to download some book data, click "Show details" for more information'), det)

        def on_stored(err):
            files_left.discard(this)
            if err:
                failed_files.append([this, err])
            if len(files_left):
                return
            if failed_files.length:
                return show_failure()
            self.db.finish_book(book, self.display_book.bind(self, book))

        def on_complete(end_type, xhr, ev):
            self.downloads_in_progress.remove(xhr)
            progress_track[this] = raster_cover_size if this is raster_cover_name else files[this].size
            update_progress()
            if end_type is 'abort':
                files_left.discard(this)
                return
            if end_type is 'load':
                self.db.store_file(book, this, xhr, on_stored.bind(this), this is raster_cover_name)
            else:
                failed_files.append([this, xhr.error_html])
                files_left.discard(this)
                if not len(files_left):
                    show_failure()

        def on_progress(loaded, ftotal):
            nonlocal total, cover_total_updated, raster_cover_size
            if this is raster_cover_name and not cover_total_updated:
                raster_cover_size = ftotal
                cover_total_updated = True
                total = total - (files[raster_cover_name]?.size or 0) + raster_cover_size
                pbar.setAttribute('max', total + '')
            progress_track[this] = loaded
            update_progress()

        def start_download(fname, path):
            xhr = ajax(path, on_complete.bind(fname), on_progress=on_progress.bind(fname), query=query, progress_totals_needed=fname is raster_cover_name)
            xhr.responseType = 'text'
            if not book.manifest.files[fname]?.is_virtualized:
                xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer'
            xhr.send()
            self.downloads_in_progress.append(xhr)

        if raster_cover_name:
            start_download(raster_cover_name, 'get/cover/' + book_id + '/' + encodeURIComponent(library_id))

        for fname in files_left:
            if fname is not raster_cover_name:
                start_download(fname, base_path + encodeURIComponent(fname).replace(/%2[fF]/g, '/'))

    def ensure_maths(self, proceed):
        self.db.get_mathjax_info(def(mathjax_info):
            if mathjax_info.version is MATHJAX_VERSION:
                return proceed()
            print('Upgrading MathJax, previous version:', mathjax_info.version)
            self.db.clear_mathjax(def():
                self.get_mathjax_manifest(mathjax_info, proceed)
            )
        )

    def get_mathjax_manifest(self, mathjax_info, proceed):
        ajax('mathjax', def(end_type, xhr, event):
            if end_type is 'abort':
                return
            if end_type is not 'load':
                return self.show_error(_('Failed to load MathJax manifest'),
                    _('The MathJax manifest failed to load, click "Show details" for more information.').format(title=self.current_metadata.title),
                                    xhr.error_html)
            try:
                manifest = JSON.parse(xhr.responseText)
            except Exception:
                return self.show_error(_('Failed to load MathJax manifest'),
                    _('The MathJax manifest is not valid'), traceback.format_exc())
            if manifest.etag is not MATHJAX_VERSION:
                print('calibre upgraded: MATHJAX_VERSION={} manifest.etag={}'.format(MATHJAX_VERSION, manifest.etag))
                return self.show_error(_('calibre upgraded!'), _(
                    'A newer version of calibre is available, please click the Reload button in your browser.'))
            mathjax_info.version = manifest.etag
            mathjax_info.files = manifest.files
            self.download_mathjax(mathjax_info, proceed)
        ).send()

    def download_mathjax(self, mathjax_info, proceed):
        files = mathjax_info.files
        total = 0
        progress_track = {}
        files_left = set()
        failed_files = []
        for key in files:
            total += files[key]
            progress_track[key] = 0
            files_left.add(key)
        progress = document.getElementById(self.progress_id)
        progress.firstChild.textContent = _(
            'Downloading MathJax to render mathematics in this book...')
        pbar = progress.firstChild.nextSibling
        pbar.setAttribute('max', total + '')
        for xhr in self.downloads_in_progress:
            xhr.abort()
        self.downloads_in_progress = []

        def update_progress():
            x = 0
            for name in progress_track:
                x += progress_track[name]
            pbar.setAttribute('value', x + '')
            if x is total:
                msg = _('Downloaded {}, saving to disk. This may take a few seconds...').format(human_readable(total))
            else:
                msg = _('Downloaded {0}, {1} left').format(human_readable(x), human_readable(total - x))
            progress.lastChild.textContent = msg

        def on_progress(loaded, ftotal):
            progress_track[this] = loaded
            update_progress()

        def show_failure():
            det = ['<h4>{}</h4><div>{}</div><hr>'.format(fname, err_html) for fname, err_html in failed_files].join('')
            self.show_error(_('Could not download MathJax'), _(
                'Failed to download some MathJax data, click "Show details" for more information'), det)

        def on_complete(end_type, xhr, ev):
            self.downloads_in_progress.remove(xhr)
            progress_track[this] = files[this]
            update_progress()
            if end_type is 'abort':
                files_left.discard(this)
                return
            if end_type is 'load':
                self.db.store_mathjax_file(this, xhr, on_stored.bind(this))
            else:
                failed_files.append([this, xhr.error_html])
                files_left.discard(this)
                if not len(files_left):
                    show_failure()

        def on_stored(err):
            files_left.discard(this)
            if err:
                failed_files.append([this, err])
            if len(files_left):
                return
            if failed_files.length:
                return show_failure()
            self.db.finish_mathjax(mathjax_info, proceed)

        def start_download(name):
            path = 'mathjax/' + name
            xhr = ajax(path, on_complete.bind(name), on_progress=on_progress.bind(name), progress_totals_needed=False)
            xhr.responseType = 'blob' if self.db.supports_blobs else 'arraybuffer'
            xhr.send()
            self.downloads_in_progress.push(xhr)

        for fname in files_left:
            start_download(fname)

    def display_book(self, book):
        if book.manifest.has_maths:
            self.ensure_maths(self.display_book_stage2.bind(self, book))
        else:
            self.display_book_stage2(book)

    def display_book_stage2(self, book):
        self.current_metadata = book.metadata
        update_window_title('', self.current_metadata.title)
        self.show_stack(self.display_id)
        self.view.display_book(book)

    def apply_url_state(self, current_query):
        same = True
        current_state = self.url_data
        same = current_query.library_id is current_state.library_id and str(current_query.book_id) is str(current_state.book_id) and current_query.fmt is current_state.fmt
        self.view.overlay.hide()
        window.scrollTo(0, 0)  # Ensure we are at the top of the window
        if same:
            if current_state.bookpos is not current_query.bookpos and current_query.bookpos:
                self.view.goto_cfi(current_query.bookpos)
        else:
            self.load_book(current_query.library_id, int(current_query.book_id), current_query.fmt, library_data.metadata[current_query.book_id])

    def update_url_state(self, replace):
        push_state(self.url_data, replace=replace, mode=read_book_mode)

    def update_metadata(self, book, metadata):
        self.db.update_metadata(book, metadata)

    def wait_for_messages_from(self, w, callback):
        self.windows_to_listen_for_messages_from.push(v'[w, callback]')

    def stop_waiting_for_messages_from(self, w):
        self.windows_to_listen_for_messages_from = [x for x in self.windows_to_listen_for_messages_from if x[0] is not w]

    def message_from_other_window(self, msg):
        if not self.windows_to_listen_for_messages_from.length:
            return
        old = self.windows_to_listen_for_messages_from
        for x in old:
            w, callback = x
            if w is msg.source:
                callback(msg)

    def check_for_speech_capability(self):
        if not window.speechSynthesis:
            error_dialog(_('No speech support'), _(
                'Your browser does not have support for Text-to-Speech'))
            return False
        return True

    def speak_simple_text(self, text):
        if not self.check_for_speech_capability():
            return
        self.tts_client.speak_simple_text(text)

    def tts(self, event, data):
        if not self.check_for_speech_capability():
            return
        if event is 'play':
            self.tts_client.speak_marked_text(data.marked_text, self.view.read_aloud.handle_tts_event)
        else:
            getattr(self.tts_client, event)()

    def update_reading_rates(self, rates):
        if not self.view?.book:
            return
        book = self.view.book
        self.db.save_reading_rates(book, rates)
