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

from elementmaker import E
from gettext import gettext as _

from book_list.theme import get_color, get_color_as_rgba
from dom import add_extra_css, build_rule, clear, svgicon
from iframe_comm import IframeClient, create_wrapped_iframe
from modals import create_custom_dialog
from utils import html_escape
from widgets import create_button

CLASS_NAME = 'comments-editor'
TOOLBAR_CLASS = 'comments-editor-toolbar'

add_extra_css(def():
    sel = '.' + TOOLBAR_CLASS + ' '
    style = ''
    style += build_rule(sel, display='flex', flex_wrap='wrap', padding_top='0.25ex', padding_bottom='0.25ex')
    sel += ' > div'
    style += build_rule(sel, padding='0.5ex', margin_right='0.5ex', cursor='pointer')
    style += build_rule(sel + ':hover', color='red')
    style += build_rule(sel + '.activated', color=get_color('window-background'), background=get_color('window-foreground'))
    sel += '.sep'
    style += build_rule(sel, border_left='solid 2px currentColor', cursor='auto')
    style += build_rule(sel + ':hover', color='currentColor')
    return style
)


def choose_block_style(proceed):
    create_custom_dialog(_('Choose style'), def(parent, close_modal):

        def action(which):
            close_modal()
            proceed(which)

        def s(text, tag):
            ans = create_button(text, action=action.bind(None, tag))
            ans.style.marginBottom = '1ex'
            return ans

        spc = '\xa0'

        parent.appendChild(E.div(
            E.div(_('Choose the block style you would like to use')),
            E.div(class_='button-box', style='flex-wrap: wrap',
                s(_('Normal'), 'div'), spc, s('H1', 'h1'), spc, s('H2', 'h2'), spc, s('H3', 'h3'), spc, s('H4', 'h4'), spc, s('H5', 'h5'), spc, s('H6', 'h6'), spc, s(_('Quote'), 'blockquote')
            )
        ))
    )


def insert_heading(editor_id, cmd):
    editor = registry[editor_id]
    if cmd:
        editor.exec_command('formatBlock', f'<{cmd}>')
    editor.focus()


def insert_link(title, msg, proceed):
    create_custom_dialog(title, def(parent, close_modal):

        def action(ok, evt):
            close_modal()
            c = evt.currentTarget.closest('.il-modal-container')
            url = c.querySelector('input[name="url"]').value
            name = c.querySelector('input[name="name"]').value
            proceed(ok, url, name)

        parent.appendChild(E.div(class_='il-modal-container',
            E.div(msg),
            E.table(style='width: 95%; margin-top: 1ex',
                E.tr(E.td('URL:'), E.td(style='padding-bottom: 1ex', E.input(type='url', name='url', style='width: 100%'))),
                E.tr(E.td(_('Name (optional):')), E.td(E.input(type='text', name='name', style='width: 100%'))),
            ),
            E.div(class_='button-box',
                  create_button(_('OK'), action=action.bind(None, True), highlight=True),
                  '\xa0',
                  create_button(_('Cancel'), action=action.bind(None, False), highlight=False),
            ),
        ))
    )


def insert_link_or_image(editor_id, is_image, ok, url, title):
    editor = registry[editor_id]
    if ok:
        if title:
            markup = '<img src="{}" title="{}"></img>' if is_image else '<a href="{}">{}</a>'
            editor.exec_command('insertHTML', markup.format(html_escape(url), html_escape(title)))
        else:
            cmd = 'insertImage' if is_image else 'createLink'
            editor.exec_command(cmd, url)
    editor.focus()


def set_color(title, msg, proceed):
    create_custom_dialog(title, def(parent, close_modal):
        def action(ok, evt):
            close_modal()
            c = evt.currentTarget.closest('.sc-modal-container')
            color = c.querySelector('input[name="color"]').value
            proceed(ok, color)

        parent.appendChild(E.div(class_='sc-modal-container',
            E.div(msg),
            E.div(E.input(type='color', name='color')),
            E.div(class_='button-box',
                  create_button(_('OK'), action=action.bind(None, True), highlight=True),
                  '\xa0',
                  create_button(_('Cancel'), action=action.bind(None, False), highlight=False),
            ),
        ))
    )


def change_color(editor_id, which, ok, color):
    editor = registry[editor_id]
    if ok:
        editor.exec_command(which, color)
    editor.focus()



def all_editor_actions():  # {{{
    if not all_editor_actions.ans:
        all_editor_actions.ans = {
            'select-all': {
                'icon': 'select-all',
                'title': _('Select all'),
                'execute': def (editor, activated):
                    editor.exec_command('selectAll')
            },
            'remove-format': {
                'icon': 'eraser',
                'title': _('Remove formatting'),
                'execute': def (editor, activated):
                    editor.exec_command('removeFormat')
            },
            'undo': {
                'icon': 'undo',
                'title': _('Undo'),
                'execute': def (editor, activated):
                    editor.exec_command('undo')
            },
            'redo': {
                'icon': 'redo',
                'title': _('Redo'),
                'execute': def (editor, activated):
                    editor.exec_command('redo')
            },
            'bold': {
                'icon': 'bold',
                'title': _('Bold'),
                'execute': def (editor, activated):
                    editor.exec_command('bold')
            },
            'italic': {
                'icon': 'italic',
                'title': _('Italic'),
                'execute': def (editor, activated):
                    editor.exec_command('italic')
            },
            'underline': {
                'icon': 'underline',
                'title': _('Underline'),
                'execute': def (editor, activated):
                    editor.exec_command('underline')
            },
            'strikethrough': {
                'icon': 'strikethrough',
                'title': _('Strikethrough'),
                'execute': def (editor, activated):
                    editor.exec_command('strikeThrough')
            },
            'superscript': {
                'icon': 'superscript',
                'title': _('Superscript'),
                'execute': def (editor, activated):
                    editor.exec_command('superscript')
            },
            'subscript': {
                'icon': 'subscript',
                'title': _('Subscript'),
                'execute': def (editor, activated):
                    editor.exec_command('subscript')
            },
            'hr': {
                'icon': 'hr',
                'title': _('Insert separator'),
                'execute': def (editor, activated):
                    editor.exec_command('insertHorizontalRule')
            },
            'format-block': {
                'icon': 'heading',
                'title': _('Style the selected text block'),
                'execute': def (editor, activated):
                    choose_block_style(insert_heading.bind(None, editor.id))
            },
            'ul': {
                'icon': 'ul',
                'title': _('Unordered list'),
                'execute': def (editor, activated):
                    editor.exec_command('insertUnorderedList')
            },
            'ol': {
                'icon': 'ol',
                'title': _('Ordered list'),
                'execute': def (editor, activated):
                    editor.exec_command('insertOrderedList')
            },
            'indent': {
                'icon': 'indent',
                'title': _('Increase indentation'),
                'execute': def (editor, activated):
                    editor.exec_command('indent')
            },
            'outdent': {
                'icon': 'outdent',
                'title': _('Decrease indentation'),
                'execute': def (editor, activated):
                    editor.exec_command('outdent')
            },
            'justify-left': {
                'icon': 'justify-left',
                'title': _('Align left'),
                'execute': def (editor, activated):
                    editor.exec_command('justifyLeft')
            },
            'justify-full': {
                'icon': 'justify-full',
                'title': _('Align justified'),
                'execute': def (editor, activated):
                    editor.exec_command('justifyFull')
            },
            'justify-center': {
                'icon': 'justify-center',
                'title': _('Align center'),
                'execute': def (editor, activated):
                    editor.exec_command('justifyCenter')
            },
            'justify-right': {
                'icon': 'justify-right',
                'title': _('Align right'),
                'execute': def (editor, activated):
                    editor.exec_command('justifyRight')
            },
            'insert-link': {
                'icon': 'insert-link',
                'title': _('Insert a link or linked image'),
                'execute': def (editor, activated):
                    insert_link(_('Insert a link'), _('Enter the link URL and optionally the link name'), insert_link_or_image.bind(None, editor.id, False))
            },
            'insert-image': {
                'icon': 'image',
                'title': _('Insert an image'),
                'execute': def (editor, activated):
                    insert_link(_('Insert an image'), _('Enter the image URL and optionally the image name'), insert_link_or_image.bind(None, editor.id, True))
            },
            'fg': {
                'icon': 'fg',
                'title': _('Change the text color'),
                'execute': def (editor, activated):
                    set_color(_('Choose text color'), _('Choose the color below'), change_color.bind(None, editor.id, 'foreColor'))
            },
            'bg': {
                'icon': 'bg',
                'title': _('Change the highlight color'),
                'execute': def (editor, activated):
                    set_color(_('Choose highlight color'), _('Choose the color below'), change_color.bind(None, editor.id, 'hiliteColor'))
            },
        }
    return all_editor_actions.ans
# }}}


class CommentsEditorBoss:

    def __init__(self):
        handlers = {
            'initialize': self.initialize,
            'set_html': self.set_html,
            'get_html': self.get_html,
            'exec_command': self.exec_command,
            'focus': self.focus,
        }
        self.comm = IframeClient(handlers, 'comments-editor-iframe')

    def initialize(self, data):
        window.onerror = self.onerror
        clear(document.body)
        document.execCommand("defaultParagraphSeparator", False, "div")
        document.execCommand("styleWithCSS", False, False)
        document.body.style.margin = '0'
        document.body.style.padding = '0'
        document.documentElement.style.height = document.body.style.height = '100%'
        document.documentElement.style.overflow = document.body.style.overflow = 'hidden'
        document.body.style.fontFamily = window.default_font_family
        document.body.appendChild(E.div(style='width: 100%; height: 100%; padding: 0; margin: 0; border: solid 3px transparent; box-sizing: border-box'))
        document.body.lastChild.contentEditable = True
        document.body.lastChild.addEventListener('keyup', self.update_state)
        document.body.lastChild.addEventListener('mouseup', self.update_state)
        document.body.lastChild.focus()
        self.update_state()

    def focus(self):
        document.body.lastChild.focus()

    def onerror(self, msg, script_url, line_number, column_number, error_object):
        if error_object is None:
            # This happens for cross-domain errors (probably javascript injected
            # into the browser via extensions/ userscripts and the like). It also
            # happens all the time when using Chrome on Safari
            console.log(f'Unhandled error from external javascript, ignoring: {msg} {script_url} {line_number}')
        else:
            console.log(error_object)

    def set_html(self, data):
        document.body.style.color = data.color
        document.body.lastChild.innerHTML = data.html

    def get_html(self, data):
        self.comm.send_message('html', html=document.body.lastChild.innerHTML)

    def exec_command(self, data):
        document.execCommand(data.name, False, data.value)
        self.update_state()

    def update_state(self):
        state = {name: document.queryCommandState(name) for name in 'bold italic underline superscript subscript'.split(' ')}
        state.strikethrough = document.queryCommandState('strikeThrough')
        self.comm.send_message('update_state', state=state)



registry = {}

def add_editor(editor):
    for k in Object.keys(registry):
        if not document.getElementById(k):
            registry[k].destroy()
            v'delete registry[k]'
    registry[editor.id] = editor


class Editor:

    def __init__(self, iframe_kw):
        handlers = {
            'ready': self.on_iframe_ready,
            'html': self.on_html_received,
            'update_state': self.update_state,
        }
        iframe, self.iframe_wrapper = create_wrapped_iframe(
            handlers, _('Loading comments editor...'), 'book_list.comments_editor', iframe_kw)
        self.id = iframe.id
        self.ready = False
        self.pending_set_html = None
        self.get_html_callbacks = v'[]'
        self.iframe_obj = iframe

    def init(self):
        self.iframe_wrapper.init()

    def destroy(self):
        self.iframe_wrapper.destroy()
        self.get_html_callbacks = v'[]'

    def focus(self):
        self.iframe.contentWindow.focus()
        if self.ready:
            self.iframe_wrapper.send_message('focus')

    @property
    def iframe(self):
        return self.iframe_wrapper.iframe

    def on_iframe_ready(self, msg):
        self.ready = True
        return self.after_iframe_initialized

    def after_iframe_initialized(self):
        if self.pending_set_html is not None:
            self.set_html(self.pending_set_html)
            self.pending_set_html = None
        if self.get_html_callbacks.length:
            self.get_html(self.get_html_callback)

    def set_html(self, html):
        if not self.ready:
            self.pending_set_html = html
            return
        rgba = get_color_as_rgba('window-foreground')
        self.iframe_wrapper.send_message('set_html', html=html, color=f'rgba({rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]})')

    def get_html(self, proceed):
        self.get_html_callbacks.push(proceed)
        if self.ready:
            self.iframe_wrapper.send_message('get_html')

    def on_html_received(self, data):
        if self.get_html_callbacks.length:
            for f in self.get_html_callbacks:
                f(data.html)
            self.get_html_callbacks = v'[]'

    def exec_command(self, name, value=None):
        if self.ready:
            self.iframe_wrapper.send_message('exec_command', name=name, value=value)

    def update_state(self, data):
        c = self.iframe.closest('.' + CLASS_NAME)
        for name in Object.keys(data.state):
            div = c.querySelector(f'.{TOOLBAR_CLASS} > [name="{name}"]')
            if div:
                if data.state[name]:
                    div.classList.add('activated')
                else:
                    div.classList.remove('activated')


def create_editor():
    iframe_kw = {
        'sandbox': 'allow-scripts', 'seamless': True, 'style': 'flex-grow: 10; border: solid 1px currentColor'
    }
    editor = Editor(iframe_kw)
    iframe = editor.iframe_obj
    v'delete editor.iframe_obj'
    add_editor(editor)
    return iframe, editor


def action_activated(editor_id, ac_name, evt):
    editor = registry[editor_id]
    if not editor:
        return
    action = all_editor_actions()[ac_name]
    if not action:
        return
    button = evt.currentTarget
    action.execute(editor, button.classList.contains('activated'))
    editor.focus()


def add_action(toolbar, ac_name, action, editor_id):
    b = E.div(svgicon(action.icon), title=action.title, onclick=action_activated.bind(None, editor_id, ac_name), name=ac_name)
    toolbar.appendChild(b)


def create_comments_editor(container):
    iframe, editor = create_editor()
    toolbars = E.div(style='flex-grow: 0')
    toolbar1 = E.div(class_=TOOLBAR_CLASS)
    toolbar2 = E.div(class_=TOOLBAR_CLASS)
    toolbar3 = E.div(class_=TOOLBAR_CLASS)
    toolbars.appendChild(toolbar1), toolbars.appendChild(toolbar2), toolbars.appendChild(toolbar3)
    acmap = all_editor_actions()

    def add(toolbar, ac_name):
        if ac_name:
            if acmap[ac_name]:
                add_action(toolbar, ac_name, acmap[ac_name], editor.id)
            else:
                toolbar.appendChild(E.div(class_='sep'))

    for ac_name in 'undo redo select-all remove-format bold italic underline strikethrough'.split(' '):
        add(toolbar1, ac_name)

    for ac_name in 'hr superscript subscript format-block ul ol indent outdent'.split(' '):
        add(toolbar2, ac_name)

    for ac_name in 'justify-left justify-center justify-right justify-full insert-link insert-image fg bg'.split(' '):
        add(toolbar3, ac_name)

    container.setAttribute('style', (container.getAttribute('style') or '') + ';display: flex; flex-direction: column; align-items: stretch')
    container.appendChild(toolbars)
    container.appendChild(iframe)
    container.classList.add(CLASS_NAME)
    return editor


def focus_comments_editor(container):
    iframe = container.querySelector('iframe')
    editor = registry[iframe.getAttribute('id')]
    if editor:
        editor.focus()


def set_comments_html(container, html):
    iframe = container.querySelector('iframe')
    eid = iframe.getAttribute('id')
    editor = registry[eid]
    editor.set_html(html or '')


def get_comments_html(container, proceed):
    iframe = container.querySelector('iframe')
    eid = iframe.getAttribute('id')
    editor = registry[eid]
    editor.get_html(proceed)


def develop(container):
    container.setAttribute('style', 'width: 100%; min-height: 90vh; display: flex; flex-direction: column; align-items: stretch')
    editor = create_comments_editor(container)
    set_comments_html(container, '<p>Testing, <i>testing</i> 123...')
    focus_comments_editor(container)
    editor.init()


def main():
    main.boss = CommentsEditorBoss()
