#!/usr/pkg/bin/python3.13
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>


from qt.core import (
    QComboBox, QFrame, QHBoxLayout, QIcon, QLabel, QLineEdit, QPushButton,
    QScrollArea, Qt, QToolButton, QVBoxLayout, QWidget, pyqtSignal
)

from calibre import prepare_string_for_xml
from calibre.ebooks.html_transform_rules import (
    ACTION_MAP, MATCH_TYPE_MAP, export_rules, import_rules, transform_html,
    validate_rule
)
from calibre.gui2 import elided_text, error_dialog
from calibre.gui2.convert.xpath_wizard import XPathEdit
from calibre.gui2.css_transform_rules import (
    RulesWidget as RulesWidgetBase, Tester as TesterBase
)
from calibre.gui2.tag_mapper import (
    RuleEditDialog as RuleEditDialogBase, RuleItem as RuleItemBase,
    Rules as RulesBase, RulesDialog as RulesDialogBase
)

# Classes for rule edit widget {{{


class TagAction(QWidget):

    remove_action = pyqtSignal(object)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.l = l = QVBoxLayout(self)
        self.h = h = QHBoxLayout()
        l.addLayout(h)

        english_sentence = '{action_type} {action_data}'
        sentence = _('{action_type} {action_data}')
        if set(sentence.split()) != set(english_sentence.split()):
            sentence = english_sentence
        parts = sentence.split()
        for clause in parts:
            if clause == '{action_data}':
                self.action_data = w = QLineEdit(self)
                w.setClearButtonEnabled(True)
            elif clause == '{action_type}':
                self.action_type = w = QComboBox(self)
                for action, ac in ACTION_MAP.items():
                    w.addItem(ac.short_text, action)
                w.currentIndexChanged.connect(self.update_state)
            h.addWidget(w)
            if clause is not parts[-1]:
                h.addWidget(QLabel('\xa0'))
        self.h2 = h = QHBoxLayout()
        l.addLayout(h)

        self.remove_button = b = QToolButton(self)
        b.setToolTip(_('Remove this action')), b.setIcon(QIcon(I('minus.png')))
        b.clicked.connect(self.request_remove)
        h.addWidget(b)
        self.action_desc = la = QLabel('')
        la.setWordWrap(True)
        la.setTextFormat(Qt.TextFormat.RichText)
        h.addWidget(la)
        self.sep = sep = QFrame(self)
        sep.setFrameShape(QFrame.Shape.HLine)
        l.addWidget(sep)
        self.update_state()

    def request_remove(self):
        self.remove_action.emit(self)

    @property
    def as_dict(self):
        return {'type': self.action_type.currentData(), 'data': self.action_data.text()}

    @as_dict.setter
    def as_dict(self, val):
        self.action_data.setText(val.get('data') or '')
        at = val.get('type')
        if at:
            idx = self.action_type.findData(at)
            if idx > -1:
                self.action_type.setCurrentIndex(idx)

    def update_state(self):
        val = self.as_dict
        ac = ACTION_MAP[val['type']]
        self.action_desc.setText(ac.long_text)
        if ac.placeholder:
            self.action_data.setVisible(True)
            self.action_data.setPlaceholderText(ac.placeholder)
        else:
            self.action_data.setVisible(False)


class ActionsContainer(QScrollArea):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWidgetResizable(True)
        self.w = w = QWidget()
        self.setWidget(w)
        w.l = QVBoxLayout(w)
        w.l.addStretch(1)
        self.all_actions = []
        self.new_action()

    def new_action(self):
        a = TagAction(self)
        self.all_actions.append(a)
        l = self.w.l
        a.remove_action.connect(self.remove_action)
        l.insertWidget(l.count() - 1, a)
        a.action_type.setFocus(Qt.FocusReason.OtherFocusReason)
        return a

    def remove_action(self, ac):
        if ac in self.all_actions:
            self.w.l.removeWidget(ac)
            del self.all_actions[self.all_actions.index(ac)]
            ac.deleteLater()

    def sizeHint(self):
        ans = super().sizeHint()
        ans.setHeight(ans.height() + 200)
        return ans

    @property
    def as_list(self):
        return [t.as_dict for t in self.all_actions]

    @as_list.setter
    def as_list(self, val):
        for ac in tuple(self.all_actions):
            self.remove_action(ac)
        for entry in val:
            self.new_action().as_dict = entry


class GenericEdit(QLineEdit):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setClearButtonEnabled(True)

    @property
    def value(self):
        return self.text()

    @value.setter
    def value(self, val):
        self.setText(str(val))


class CSSEdit(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        l = QHBoxLayout(self)
        l.setContentsMargins(0, 0, 0, 0)
        self.edit = le = GenericEdit(self)
        l.addWidget(le)
        l.addSpacing(5)
        self.la = la = QLabel(_('<a href="{}">CSS selector help</a>').format('https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors'))
        la.setOpenExternalLinks(True)
        l.addWidget(la)
        self.setPlaceholderText = self.edit.setPlaceholderText

    @property
    def value(self):
        return self.edit.value

    @value.setter
    def value(self, val):
        self.edit.value = val
# }}}


class RuleEdit(QWidget):  # {{{

    MSG = _('Create the rule to transform HTML tags below')

    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.l = l = QVBoxLayout(self)
        self.h = h = QHBoxLayout()

        self.la = la = QLabel(self.MSG)
        la.setWordWrap(True)
        l.addWidget(la)
        l.addLayout(h)
        english_sentence = '{preamble} {match_type}'
        sentence = _('{preamble} {match_type}')
        if set(sentence.split()) != set(english_sentence.split()):
            sentence = english_sentence
        parts = sentence.split()
        for clause in parts:
            if clause == '{preamble}':
                self.preamble = w = QLabel(_('If the tag'))
            elif clause == '{match_type}':
                self.match_type = w = QComboBox(self)
                for action, m in MATCH_TYPE_MAP.items():
                    w.addItem(m.text, action)
                w.currentIndexChanged.connect(self.update_state)
            h.addWidget(w)
            if clause is not parts[-1]:
                h.addWidget(QLabel('\xa0'))
        h.addStretch(1)
        self.generic_query = gq = GenericEdit(self)
        self.css_query = cq = CSSEdit(self)
        self.xpath_query = xq = XPathEdit(self, object_name='html_transform_rules_xpath', show_msg=False)
        l.addWidget(gq), l.addWidget(cq), l.addWidget(xq)

        self.thenl = QLabel(_('Then:'))
        l.addWidget(self.thenl)
        self.actions = a = ActionsContainer(self)
        l.addWidget(a)
        self.add_button = b = QPushButton(QIcon(I('plus.png')), _('Add another action'))
        b.clicked.connect(self.actions.new_action)
        l.addWidget(b)
        self.update_state()

    def sizeHint(self):
        a = QWidget.sizeHint(self)
        a.setHeight(a.height() + 375)
        a.setWidth(a.width() + 125)
        return a

    @property
    def current_query_widget(self):
        return {'css': self.css_query, 'xpath': self.xpath_query}.get(self.match_type.currentData(), self.generic_query)

    def update_state(self):
        r = self.rule
        mt = r['match_type']
        self.generic_query.setVisible(False), self.css_query.setVisible(False), self.xpath_query.setVisible(False)
        self.current_query_widget.setVisible(True)
        self.current_query_widget.setPlaceholderText(MATCH_TYPE_MAP[mt].placeholder)

    @property
    def rule(self):
        try:
            return {
                'match_type': self.match_type.currentData(),
                'query': self.current_query_widget.value,
                'actions': self.actions.as_list,
            }
        except Exception:
            import traceback
            traceback.print_exc()
            raise

    @rule.setter
    def rule(self, rule):
        def sc(name):
            c = getattr(self, name)
            c.setCurrentIndex(max(0, c.findData(str(rule.get(name, '')))))
        sc('match_type')
        self.current_query_widget.value = str(rule.get('query', '')).strip()
        self.actions.as_list = rule.get('actions') or []
        self.update_state()

    def validate(self):
        rule = self.rule
        title, msg = validate_rule(rule)
        if msg is not None and title is not None:
            error_dialog(self, title, msg, show=True)
            return False
        return True
# }}}


class RuleEditDialog(RuleEditDialogBase):  # {{{

    PREFS_NAME = 'edit-html-transform-rule'
    DIALOG_TITLE = _('Edit rule')
    RuleEditClass = RuleEdit
# }}}


class RuleItem(RuleItemBase):  # {{{

    @staticmethod
    def text_from_rule(rule, parent):
        try:
            query = elided_text(rule['query'], font=parent.font(), width=200, pos='right')
            text = _('If the tag <b>{match_type}</b> <b>{query}</b>').format(
                match_type=MATCH_TYPE_MAP[rule['match_type']].text, query=prepare_string_for_xml(query))
            for action in rule['actions']:
                text += '<br>' + ACTION_MAP[action['type']].short_text
                if action.get('data'):
                    ad = elided_text(action['data'], font=parent.font(), width=200, pos='right')
                    text += f' <code>{prepare_string_for_xml(ad)}</code>'
        except Exception:
            import traceback
            traceback.print_exc()
            text = _('This rule is invalid, please remove it')
        return text
# }}}


class Rules(RulesBase):  # {{{

    RuleItemClass = RuleItem
    RuleEditDialogClass = RuleEditDialog
    ACTION_KEY = 'actions'
    MSG = _('You can specify rules to transform HTML here. Click the "Add rule" button'
            ' below to get started.')
# }}}


class Tester(TesterBase):  # {{{

    DIALOG_TITLE = _('Test HTML transform rules')
    PREFS_NAME = 'test-html-transform-rules'
    LABEL = _('Enter an HTML document below and click the "Test" button')
    SYNTAX = 'html'
    RESULTS = '<!-- %s -->\n\n' % _('Resulting HTML')

    def compile_rules(self, rules):
        return rules

    def do_test(self):
        changed, html = transform_html('\n' + self.value + '\n', self.rules)
        self.set_result(html)
# }}}


class RulesDialog(RulesDialogBase):  # {{{

    DIALOG_TITLE = _('Edit HTML transform rules')
    PREFS_NAME = 'edit-html-transform-rules'
    PREFS_OBJECT_NAME = 'html-transform-rules'
    RulesClass = Rules
    TesterClass = Tester

    def extra_bottom_widget(self):
        self.scope_cb = cb = QComboBox()
        cb.addItem(_('Current HTML file'), 'current')
        cb.addItem(_('All HTML files'), 'all')
        cb.addItem(_('Open HTML files'), 'open')
        cb.addItem(_('Selected HTML files'), 'selected')
        return cb

    @property
    def transform_scope(self):
        return self.scope_cb.currentData()

    @transform_scope.setter
    def transform_scope(self, val):
        idx = self.scope_cb.findData(val)
        self.scope_cb.setCurrentIndex(max(0, idx))

# }}}


class HtmlRulesWidget(RulesWidgetBase):  # {{{
    PREFS_NAME = 'html-transform-rules'
    INITIAL_FILE_NAME = 'html-rules.txt'
    DIR_SAVE_NAME = 'export-html-transform-rules'
    export_func = export_rules
    import_func = import_rules
    TesterClass = Tester
    RulesClass = Rules
# }}}


if __name__ == '__main__':
    from calibre.gui2 import Application
    app = Application([])
    d = RulesDialog()
    d.rules = [
        {'match_type':'xpath', 'query':'//h:h2', 'actions':[{'type': 'remove'}]},
    ]
    d.exec()
    from pprint import pprint
    pprint(d.rules)
    del d, app
