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

import traceback
from aes import GCM
from elementmaker import E
from gettext import install

from book_list.globals import get_translations, main_js
from book_list.theme import get_font_family
from dom import ensure_id

LOADING_DOC = '''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- script must be on a single line so that source map matches up -->
<script type="text/javascript" id="bootstrap">window.iframe_entry_point = '__ENTRY_POINT__'; window.default_font_family = '__FONT__';  __SCRIPT__
end_script
</head>
<body>
<div style="font-size:larger; font-weight: bold; margin-top:48vh; text-align:center">
__BS__
</div>
</body>
</html>
'''.replace('end_script', '<' + '/script>')  # cannot have a closing script tag as this is embedded inside a script tag in index.html


class Messenger:
    def __init__(self):
        self.secret = Uint8Array(64)

    def reset(self):
        window.crypto.getRandomValues(self.secret)
        self.gcm_to_iframe = GCM(self.secret.subarray(0, 32))
        self.gcm_from_iframe = GCM(self.secret.subarray(32))

    def encrypt(self, data):
        return self.gcm_to_iframe.encrypt(JSON.stringify(data))

    def decrypt(self, data):
        return JSON.parse(self.gcm_from_iframe.decrypt(data))


class IframeWrapper:
    def __init__(self, handlers, iframe, entry_point, bootstrap_text):
        self.messenger = Messenger()
        self.iframe_id = ensure_id(iframe, 'content-iframe')
        self.reload_count = 0
        if ':' in entry_point:
            self.needs_init = iframe.src is not entry_point
            self.srcdoc_created = True
            self.constructor_url = entry_point
            self.entry_point = None
        else:
            self.needs_init = True
            self.srcdoc_created = False
            self.constructor_url = None
            self.entry_point = entry_point
        self.ready = False
        self.encrypted_communications = False
        self.bootstrap_text = bootstrap_text or ''
        self.handlers = {k: handlers[k] for k in handlers}
        self.on_ready_handler = self.handlers.ready
        self.handlers.ready = self.on_iframe_ready
        window.addEventListener('message', self.handle_message, False)

    def destroy(self):
        window.removeEventListener('message', self.handle_message, False)

    @property
    def iframe(self):
        return document.getElementById(self.iframe_id)

    def create_srcdoc(self):
        r = /__([A-Z][A-Z_0-9]*[A-Z0-9])__/g
        if self.entry_point:
            data = {
                'BS': self.bootstrap_text,
                'SCRIPT': main_js(),
                'FONT': get_font_family(),
                'ENTRY_POINT': self.entry_point,
            }
            self.iframe.srcdoc = LOADING_DOC.replace(r, def (match, field): return data[field];)
        else:
            self.iframe.src = self.constructor_url
        self.srcdoc_created = True

    def init(self):
        if not self.needs_init:
            return
        self.needs_init = False
        iframe = self.iframe
        if self.srcdoc_created:
            if self.entry_point:
                sdoc = iframe.srcdoc
                iframe.srcdoc = '<p>&nbsp;</p>'
                iframe.srcdoc = sdoc
            else:
                self.reload_count += 1
                ch = '&' if '?' in self.constructor_url else '?'
                iframe.src = self.constructor_url + f'{ch}rc={self.reload_count}'
        else:
            self.create_srcdoc()

    def reset(self):
        self.ready = False
        self.needs_init = True
        self.encrypted_communications = False

    def _send_message(self, action, encrypted, data):
        if not self.ready:
            return
        data.action = action
        msg = {'data': data, 'encrypted': encrypted}
        if encrypted:
            msg.data = self.messenger.encrypt(data)
        self.iframe.contentWindow.postMessage(msg, '*')

    def send_message(self, action, **data):
        self._send_message(action, self.encrypted_communications, data)

    def send_unencrypted_message(self, action, **data):
        self._send_message(action, False, data)

    def handle_message(self, event):
        if event.source is not self.iframe?.contentWindow:
            return
        data = event.data
        if self.encrypted_communications:
            if data.tag is undefined:
                print('Ignoring unencrypted message from iframe:', data)
                return
            try:
                data = self.messenger.decrypt(data)
            except Exception as e:
                print('Could not decrypt message from iframe:')
                console.log(e)
                traceback.print_exc()
                return
        if not data.action:
            return
        func = self.handlers[data.action]
        if func:
            func(data)
        else:
            print('Unknown action in message from iframe to parent:', data.action)

    def on_iframe_ready(self, data):
        self.messenger.reset()
        msg = {'secret': self.messenger.secret, 'translations': get_translations()}
        self.ready = True
        callback = None
        if self.on_ready_handler:
            callback = self.on_ready_handler(msg)
        self._send_message('initialize', False, msg)
        self.encrypted_communications = True
        if callback:
            callback()


def create_wrapped_iframe(handlers, bootstrap_text, entry_point, kw):
    if ':' in entry_point:
        # this is a document loaded via the fake protocol from web_view.py
        kw.src = entry_point
        kw.sandbox = (kw.sandbox or '') + ' allow-same-origin'
    iframe = E.iframe(**kw)
    ans = IframeWrapper(handlers, iframe, entry_point, bootstrap_text)
    return iframe, ans


instance_numbers = {}


class IframeClient:
    def __init__(self, handlers, name):
        self.encrypted_communications = False
        self.name = name
        if not instance_numbers[self.name]:
            instance_numbers[self.name] = 0
        instance_numbers[self.name] += 1
        self.instance_num = instance_numbers[self.name]
        self.handlers = {k: handlers[k] for k in handlers}
        self.initialize_handler = handlers.initialize
        self.handlers.initialize = self.initialize
        self.ready_sent = False
        window.addEventListener('message', self.handle_message, False)
        window.addEventListener('load', self.send_ready, {'once': True})

    def send_ready(self):
        if not self.ready_sent:
            self.send_message('ready', {})
            self.ready_sent = True

    def initialize(self, data):
        nonlocal print
        self.gcm_from_parent, self.gcm_to_parent = GCM(data.secret.subarray(0, 32)), GCM(data.secret.subarray(32))
        self.encrypted_communications = True
        if data.translations:
            install(data.translations)
        print = self.print_to_parent
        if self.initialize_handler:
            self.initialize_handler(data)

    def print_to_parent(self, *args):
        self.send_message('print', string=' '.join(map(str, args)))

    def handle_message(self, event):
        if event.source is not window.parent:
            return
        msg = event.data
        data = msg.data
        if msg.encrypted:
            if not self.gcm_from_parent:
                print(f'the iframe {self.name}-{self.instance_num} got an encrypted message from its parent without being initialized')
                return
            # We cannot use self.encrypted_communications as the 'display'
            # message has to be unencrypted as it transports Blob objects
            try:
                data = JSON.parse(self.gcm_from_parent.decrypt(data))
            except Exception as e:
                print('Could not process message from parent:')
                console.log(e)
                return
        if not data or not data.action:
            console.log('Got a null message from parent in iframe, ignoring')
            return
        func = self.handlers[data.action]
        if func:
            try:
                func(data)
            except Exception as e:
                console.log('Error in iframe message handler {}:'.format(data?.action))
                console.log(e)
                details = traceback.format_exc()
                console.log(details)
                self.send_message('error', title='Error in message handler', details=details, msg=e.toString())
        else:
            print('Unknown action in message to iframe from parent: ' + data.action)

    def send_message(self, action, data):
        data.action = action
        data.iframe_id = f'{self.name}-{self.instance_num}'
        if self.encrypted_communications:
            data = self.gcm_to_parent.encrypt(JSON.stringify(data))
        window.parent.postMessage(data, '*')
