# copyright 2015-2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""cubicweb-seda custom fields/widgets"""


from logilab.common.decorators import monkeypatch

from cubicweb import tags, utils
from cubicweb.predicates import specified_etype_implements
from cubicweb.uilib import js
from cubicweb_web import formfields as ff, formwidgets as fw
from cubicweb_web.views import ajaxcontroller, autoform, formrenderers

from cubicweb_relationwidget import views as rwdg

from cubicweb_seda import SEDA_2_2_ONLY_ETYPES

# deactivate relation widget's creation form by default, it causes some js error if e.g. there are
# some calendar widgets. Also, we don't really want that from an UX POV, IMO.
rwdg.SearchForRelatedEntitiesView.has_creation_form = False


def configure_relation_widget(req, div, search_url, title, multiple, validate):
    """Build a javascript link to invoke a relation widget

    Widget will be linked to div `div`, with a title `title`. It will display selectable entities
    matching url `search_url`. bool `multiple` indicates whether several entities can be selected or
    just one, `validate` identifies the javascript callback that must be used to validate the
    selection.
    """
    req.add_js(
        (
            "jquery.ui.js",
            "cubicweb.ajax.js",
            "cubicweb.widgets.js",
            "cubicweb.facets.js",
            "cubicweb_relationwidget.js",
            "cubes.editionext.js",
        )
    )
    return "javascript: %s" % js.editext.relateWidget(
        div, search_url, title, multiple, utils.JSString(validate)
    )


class ConceptOrTextField(ff.Field):
    """Compound field, using the :class:`ConceptAutoCompleteWidget` to handle values which may be
    either a relation to a `Concept` or a string

    It's expected to be set on the text attribute. Also, as you should prefer specifying a field's
    class, you will usually sublcass this field to set the following attributes:

    * 'scheme_relation', relation whose object will be the selected scheme
    * 'concept_relation', relation whose object will be the linked concept

    Example::

        class KeywordConceptOrTextField(ConceptOrTextField):
            scheme_relation = 'keyword_scheme'
            concept_relation = 'keyword_concept'

            def __init__(self, **kwargs):
                super(EquivalentConceptOrTextField, self).__init__(required=True, **kwargs)
                self.help = _('when linked to a vocabulary, value is enforced to the label of a '
                              'concept in this vocabulary. Remove it if you want to type text '
                              'freely.')

        aff = uicfg.autoform_field
        aff.tag_attribute(('Keyword', 'keyword_value'), KeywordConceptOrTextField)

    You may also configure the ajax function that will be used for the completion values, by
    specifying it using the 'ajax_autocomplete_func' attribute (or by overriding this ajax
    func). Default is :func:`scheme_concepts_autocomplete`.
    """

    scheme_relation = concept_relation = None
    ajax_autocomplete_func = "scheme_concepts_autocomplete"

    def get_widget(self, form):
        return ConceptAutoCompleteWidget(
            self.name, self.scheme_relation, self.ajax_autocomplete_func, optional=True
        )

    def has_been_modified(self, form):
        return True  # handled in process_posted below

    def process_posted(self, form):
        posted = form._cw.form
        text_val = posted.get(self.input_name(form, "Label"), "").strip()
        equivalent_eid = posted.get(self.input_name(form), "").strip()
        equivalent_eids = set()
        if equivalent_eid:
            equivalent_eids.add(int(equivalent_eid))
        if self.required and not (text_val or equivalent_eid):
            raise ff.ProcessFormError(form._cw.__("required field"))
        entity = form.edited_entity
        if not entity.has_eid() or getattr(entity, self.name) != text_val:
            yield (ff.Field(name=self.name, role="subject", eidparam=True), text_val)
        if (not entity.has_eid() and equivalent_eids) or (
            entity.has_eid()
            and {x.eid for x in entity.equivalent_concept} != equivalent_eids
        ):
            subfield = ff.Field(
                name=self.concept_relation, role="subject", eidparam=True
            )
            # register the association between the value and the field, because on entity creation,
            # process_posted will be recalled on the newly created field, and if we don't do that it
            # won't find the proper value (this is very nasty)
            form.formvalues[(subfield, form)] = equivalent_eids
            yield (subfield, equivalent_eids)


class KeywordTypeMasterWidget(fw.Select):
    """
    Usage::

      affk = uicfg.autoform_field_kwargs
      affk.set_field_kwargs('KeywordType', 'keyword_type_to',
                            widget=widgets.KeywordTypeMasterWidget(
                            slave_base_name='seda_keyword_reference_to_scheme'))
    """

    def __init__(self, slave_base_name, **kwargs):
        super().__init__(**kwargs)
        self.slave_base_name = slave_base_name

    def _render(self, form, field, render):
        req = form._cw

        vocabularies_data = []
        for eid, title, uri, keyword_type in req.execute(
            "Any CS,CST,CSU,CSKT WHERE CS title CST, CS cwuri CSU, CS code_keyword_type CSKT?"
        ):
            vocabularies_data.append(
                {"eid": eid, "title": title or uri, "keyword_type": keyword_type}
            )
        vocabularies_data.sort(key=lambda x: x["title"])

        req.add_js(("cubicweb.js", "cubicweb.ajax.js", "cubes.skoscomplete.js"))
        req.add_onload(
            js.typed_vocabularies.initKeywordTypeMasterWidget(
                field.dom_id(form, self.suffix), self.slave_base_name, vocabularies_data
            )
        )

        return super()._render(form, field, render)


class ConceptAutoCompleteWidget(fw.TextInput):
    """Derive from simple text input to create an autocompletion widget if a scheme is specified,
    otherwise free text is fine if `optional` argument is true. In such case:

    * `slave_name` is expected to be the name of the concept attribute, or the text attribute if
      optional is true),

    * you'll have to use this widget from a custom field that will handle the relation to the
      concept (e.g. :class:`ConceptOrTextField`).

    When optional is false, a regular :class:`RelationField` is fine.

    You may configure the ajax function that will be used for the completion values by specifying it
    using the 'ajax_autocomplete_func' argument (or by overriding this ajax func). Default is
    :func:`scheme_concepts_autocomplete`.

    Usage::

      affk = uicfg.autoform_field_kwargs
      affk.set_field_kwargs('Keyword', 'keyword_value',
                            widget=ConceptAutoCompleteWidget(slave_name='keyword_value',
                                                             master_name='keyword_scheme',
                                                             optional=True))
    """

    needs_css = ("jquery.ui.css",)
    needs_js = (
        "jquery.ui.js",
        "cubicweb.js",
        "cubicweb.ajax.js",
        "cubes.skoscomplete.js",
    )

    def __init__(
        self,
        slave_name,
        master_name,
        ajax_autocomplete_func="scheme_concepts_autocomplete",
        optional=False,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.slave_name = slave_name
        self.master_name = master_name
        self.ajax_autocomplete_func = ajax_autocomplete_func
        self.optional = optional

    def _render(self, form, field, render):
        entity = form.edited_entity
        slave_id = field.dom_id(form, self.suffix)
        master_id = slave_id.replace(self.slave_name, self.master_name)
        if entity.has_eid():
            concept = entity.concept
        else:
            concept = None
        req = form._cw
        req.add_onload(
            js.concept_autocomplete.initConceptAutoCompleteWidget(
                master_id, slave_id, self.ajax_autocomplete_func
            )
        )
        if concept is None:
            value = getattr(entity, self.slave_name) if self.optional else None
            eid = ""
        else:
            value = concept.label()
            eid = str(concept.eid)
        # we need an hidden input to handle the value while the text input display the label
        inputs = [
            tags.input(
                name=field.input_name(form, "Label"),
                id=slave_id + "Label",
                klass="form-control",
                type="text",
                value=value,
            ),
            tags.input(
                name=field.input_name(form), id=slave_id, type="hidden", value=eid
            ),
        ]
        return "\n".join(inputs)


@ajaxcontroller.ajaxfunc(output_type="json")
def scheme_concepts_autocomplete(self):
    assert self._cw.form["scheme"]
    scheme = int(self._cw.form["scheme"])
    term = self._cw.form["q"]
    limit = self._cw.form.get("limit", 50)
    return [
        {"value": eid, "label": label}
        for eid, label in self._cw.execute(
            "DISTINCT Any C,N ORDERBY N LIMIT %s WHERE C in_scheme S, S eid %%(s)s, "
            "C preferred_label L, L label N, L label ILIKE %%(term)s" % limit,
            {"s": scheme, "term": f"%{term}%"},
        )
    ]


@monkeypatch(formrenderers.EntityInlinedFormRenderer)
def render_title(self, w, form, values):
    """Monkey-patched to remove counter"""
    w('<div class="iformTitle">')
    w(f"<span>{values['title']}</span>")
    if form.edited_entity.cw_etype in SEDA_2_2_ONLY_ETYPES:
        w("&nbsp;<span class='badge badge-warning'>SEDA 2.2</span>")
    if values["removejs"]:
        values["removemsg"] = self._cw.__("remove-inlined-entity-form")
        w(
            f" [<a href=\"javascript: {values['removejs']};$.noop();\">{values['removemsg']}</a>]"
        )
    w("</div>")


class SEDAMetaField(ff.StringField):
    """Extends user_cardinality field to allow edition of a `user_annotation` all together."""

    @staticmethod
    def annotation_field(form):
        """Return the field for `user_annotation` entity."""
        eschema = form.edited_entity.e_schema
        rschema = eschema.subjrels["user_annotation"]
        return ff.guess_field(eschema, rschema, "subject", form._cw)

    def actual_fields(self, form):
        """Overriden from :class:`Field`"""
        yield self
        yield self.annotation_field(form)

    def render(self, form, renderer):
        """Overriden from :class:`Field` to render fields in a div which is hidden by default"""
        form._cw.add_js("cubes.seda.js")
        wdgs = [self.get_widget(form).render(form, self, renderer)]
        self._render_hidden_section(wdgs, form, renderer)
        return "\n".join(wdgs)

    def _render_hidden_section(self, wdgs, form, renderer):
        divid = f"{self.input_name(form)}-advanced"
        if form.edited_entity.has_eid() and form.edited_entity.user_annotation:
            hidden = ""
            icon = "icon-up-open"
        else:
            hidden = " hidden"
            icon = "icon-list-add"
        wdgs.append(
            tags.a(
                "",
                onclick=str(js.seda.toggleFormMetaVisibility(divid)),
                href="javascript:$.noop()",
                title=form._cw._("show/hide meta fields"),
                # take care, related js relies on the icon class position
                klass=icon + " metaFieldSwitch",
            )
        )
        wdgs.append(f'<div id="{divid}" class="metaField{hidden}">')
        wdgs.append(self._render_subfield(form, self.annotation_field(form), renderer))
        wdgs.append("</div>")

    @staticmethod
    def _render_subfield(form, field, renderer):
        """Render a sub-field: label + widget + help + EOL"""
        data = utils.UStringIO()
        w = data.write
        w('<div class="row">')
        w(renderer.render_label(form, field))
        w('<div class="col-md-9">')
        w(field.render(form, renderer))
        w("</div>")
        w("</div>")
        w('<div class="row">')
        w('<div class="col-md-offset-3 col-md-9">')
        w(renderer.render_help(form, field))
        w("</div>")
        w("</div>")
        return data.getvalue()


class SimplifiedAutomaticEntityForm(autoform.AutomaticEntityForm):
    """Custom autoform, forcing display of creation form instead of add new link."""

    __abstract__ = True

    def should_display_inline_creation_form(self, rschema, existing, card):
        return not existing

    def should_display_add_new_relation_link(self, rschema, existing, card):
        return False


class NoTitleEntityInlinedFormRenderer(formrenderers.EntityInlinedFormRenderer):
    """Custom inlined form renderer that doesn't display any title nor remove link.

    This is intended to be subclassed with a custom selector.
    """

    __abstract__ = True

    def render_title(self, w, form, values):
        pass


class ConcretNoTitleEntityInlinedFormRenderer(NoTitleEntityInlinedFormRenderer):
    """Concret implementation of `NoTitleEntityInlinedFormRenderer` with a custom regid.

    Use this one by specifying the renderer id explicitly, for case where you can't easily specify a
    selector.
    """

    __regid__ = "notitle"


class SEDAWarningExportInlineAddNewLinkView(autoform.InlineAddNewLinkView):
    """
    Add export warning tag on SEAD 2.2 relations
    """

    __select__ = autoform.InlineAddNewLinkView.__select__ & specified_etype_implements(
        *SEDA_2_2_ONLY_ETYPES
    )

    def call(self, i18nctx, **kwargs):
        self._cw.set_varmaker()
        divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
        self.w('<div class="inlinedform" id="%s" cubicweb:limit="true">', divid)
        js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s', '%s')" % (
            self.peid,
            self.petype,
            self.etype,
            self.rtype,
            self.role,
            i18nctx,
        )
        if self.pform.should_hide_add_new_relation_link(self.rtype, self.card):
            js = "toggleVisibility('%s'); %s" % (divid, js)
        __ = self._cw.pgettext
        self.w(
            '<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>',
            self.rtype,
            self.peid,
            js,
            __(i18nctx, "add a %s" % self.etype),
        )
        self.w("<span class='badge badge-warning inlined'>SEDA 2.2</span>")
        self.w("</div>")
