# 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 uuid import short_uuid4

from book_list.theme import color_scheme, get_color, get_color_as_rgba
from dom import add_extra_css, build_rule, clear, ensure_id, svgicon
from iframe_comm import IframeClient, create_wrapped_iframe
from image_popup import fit_image
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='var(--calibre-color-primary)')
    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 button-box--wrap-chips', 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_image(editor):
    acceptable_image_types = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']
    parent_id = ''
    accepted = False
    align_name = 'align-radio-button'

    def on_close(evt):
        parent = document.getElementById(parent_id)
        if parent:
            img = parent.querySelector('img')
            if img:
                if accepted and img.src:
                    align = parent.querySelector(f'input[name="{align_name}"]:checked').value
                    s = style_from_align(align)

                    w = parseInt(parent.querySelector(f'input[name="width"]').value or '0')
                    page_width = (0 if isNaN(w) else w) or 1000000000
                    h = parseInt(parent.querySelector(f'input[name="height"]').value or '0')
                    page_height = (0 if isNaN(h) else h) or 1000000000
                    w, h = img.naturalWidth, img.naturalHeight
                    resized, nw, nh = fit_image(w, h, page_width, page_height)
                    if resized:
                        markup = (
                            f'<img src="{img.src}" data-filename="{html_escape(img.dataset.filename)}"' +
                            f' data-align="{html_escape(align)}" width="{nw}" height="{nh}" style="{s}"></img>'
                        )
                    else:
                        markup = (
                            f'<img src="{img.src}" data-filename="{html_escape(img.dataset.filename)}"' +
                            f' data-align="{html_escape(align)}" style="{s}"></img>'
                        )
                    editor.exec_command('insertHTML', markup)

    def dialog(parent, close_modal):
        nonlocal parent_id
        parent_id = ensure_id(parent)
        container = parent.closest('.modal_height_container')
        max_container_height = container.style.maxHeight

        def action(ok, evt):
            nonlocal accepted
            img = parent.getElementsByTagName('img')[0]
            if ok and not img.src:
                parent.querySelector('.no-image-selected').style.display = 'block'
                return
            accepted = ok
            close_modal()

        def handle_files(files):
            parent.querySelector('.no-image-selected').style.display = 'none'
            parent.querySelector('.image-alignment').style.display = 'none'
            parent.querySelector('.image-shrink').style.display = 'none'
            img = parent.getElementsByTagName('img')[0]
            img.src = ''
            img.dataset.filename = ''
            img.dataset.align = ''
            img.parentElement.style.display = 'none'
            for f in files:
                if acceptable_image_types.indexOf(f.type) > -1:
                    r = new FileReader()
                    r.onload = def ():
                        img.src = r.result
                    r.readAsDataURL(f)
                    h = parent.querySelector('.button-box').offsetHeight
                    img.parentElement.style.display = 'block'
                    non_content_height = (container.offsetHeight - parent.offsetHeight) + 'px'
                    img.style.maxHeight = f'calc({max_container_height} - {non_content_height} - {h}px - 1rem)'
                    if f.name:
                        img.dataset.filename = f.name
                    parent.querySelector('.image-alignment').style.display = 'block'
                    parent.querySelector('.image-shrink').style.display = 'block'
                    return
            alert(_('No valid image file found'))

        parent.appendChild(E.div(
            ondragenter=def (e):
                e.stopPropagation(), e.preventDefault()
            ,
            ondragover = def (e):
                e.stopPropagation(), e.preventDefault()
            ,
            ondrop = def (e):
                e.stopPropagation(), e.preventDefault()
                handle_files(e.dataTransfer.files)
            ,
            E.div(
                style='display: flex;',
                E.div(
                    style='display: none; min-width: 40%; max-width: 40%; margin-right: 0.5rem',
                    E.img(style='max-width: 100%'),
                ),
                E.div(
                    E.input(
                        type='file', accept=','.join(acceptable_image_types),
                        onchange=def (e): handle_files(e.target.files);, style='display: none'
                    ),
                    E.p(E.a(
                        _('Select an image from your files'),
                        class_='blue-link', href='javascript:void(0)', onclick=def ():
                        parent.querySelector('input[type=file]').click()
                    ), E.span(_(' or drag and drop an image here.'))),
                    E.div(
                        style='display: none; margin-top: 0.5rem', class_='image-alignment',
                        _('Place image:'), E.br(),
                        E.label(E.input(type='radio', name=align_name, value='left'), _('Float left'), style='margin-right: 0.5rem'),
                        ' ',
                        E.label(E.input(type='radio', name=align_name, value='inline', checked=True), _('Inline'),
                                title=_('Place the image inline with surrounding text'), style='margin-right: 0.5rem'
                        ),
                        ' ',
                        E.label(E.input(type='radio', name=align_name, value='right'), _('Float right')),
                    ),
                    E.div(
                        style='display: none; margin-top: 0.5rem', class_='image-shrink',
                        _('Shrink image to fit (px):'),
                        E.div(style='display: flex; flex-wrap: wrap',
                        E.label(_('Width:') + '\xa0', E.input(type='number', min='0', max='10000', name='width'),
                                style='margin-left: 0.5rem', title=_('A value of zero means no effect')),
                        E.label(_('Height:') + '\xa0', E.input(type='number', min='0', max='10000', name='height'),
                                style='margin-left: 0.5rem', title=_('A value of zero means no effect')
                        )),
                    ),
                    E.p(_('No image selected!'), class_='no-image-selected', style='color:red; display:none'),
                ),
            ),
            E.div(class_='button-box',
                  create_button(_('Cancel'), action=action.bind(None, False), highlight=False),
                  create_button(_('OK'), action=action.bind(None, True), highlight=True),
            ),
        ))
    create_custom_dialog(_('Insert image'), dialog, on_close=on_close)


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: 100%; max-width: 100%; margin-top: 1ex; table-layout: fixed; box-sizing: border-box',
                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(_('Cancel'), action=action.bind(None, False), highlight=False),
                  create_button(_('OK'), action=action.bind(None, True), highlight=True),
            ),
        ))
    )


def insert_link_or_image(editor_id, is_image, ok, url, title):
    editor = registry[editor_id]
    if ok:
        if title:
            markup = '<img src="{}" alt="{}"></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(_('Cancel'), action=action.bind(None, False), highlight=False),
                  create_button(_('OK'), action=action.bind(None, True), highlight=True),
            ),
        ))
    )


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):
                    if editor.insert_image_files:
                        insert_image(editor)
                    else:
                        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
# }}}


def style_from_align(align):
    if align is 'left':
        return 'float: left; margin-right: 0.5em'
    if align is 'right':
        return 'float: right; margin-left: 0.5em'
    return ''


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.body.style.fontFamily = window.default_font_family
        document.body.appendChild(E.style('div:focus { outline: none }'))
        document.body.appendChild(E.div(style='width: 100%; padding: 0.5rem; margin: 0; border-width: 0; 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):
        if data.color_scheme:
            document.documentElement.style.colorScheme = data.color_scheme
        document.body.style.color = data.color
        document.body.lastChild.innerHTML = data.html

    def get_html(self, data):
        c = document.body.lastChild.cloneNode(True)
        images = v'{}'
        if data.extract_images:
            for img in c.getElementsByTagName('img'):
                if img.src:
                    key = short_uuid4()
                    images[key] = {'data': img.src, 'filename': img.dataset.filename}
                    if img.dataset.filename:  # this is an added image
                        s = style_from_align(img.dataset.align)
                        if s:
                            img.setAttribute('style', s)
                        else:
                            img.removeAttribute('style')
                    img.src = key
                    v'delete img.dataset.filename'
                    v'delete img.dataset.align'
        self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images, 'text': c.innerText})

    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 destroy_removed_editors():
    for k in Object.keys(registry):
        if not document.getElementById(k):
            registry[k].destroy()
            v'delete registry[k]'


def add_editor(editor):
    destroy_removed_editors()
    registry[editor.id] = editor


def destroy_editor(container):
    v'delete registry[container.querySelector("iframe").id]'


class Editor:
    def __init__(self, iframe_kw, insert_image_files):
        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.pending_get_html = []
        self.get_html_callbacks = []
        self.iframe_obj = iframe
        self.insert_image_files = v'!!insert_image_files'

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

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

    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.pending_get_html.length > 0:
            for x in self.pending_get_html:
                self.get_html(x)

    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_scheme=color_scheme(), 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', extract_images=self.insert_image_files)
        else:
            self.pending_get_html.push(proceed)

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

    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(insert_image_files):
    iframe_kw = {
        'sandbox': 'allow-scripts', 'seamless': True, 'style': 'flex-grow: 10; border: solid 1px currentColor'
    }
    editor = Editor(iframe_kw, insert_image_files)
    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, options):
    options = options or {}
    iframe, editor = create_editor(options.insert_image_files)
    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()


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