# copyright 2003-2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb 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.
#
# CubicWeb 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 CubicWeb.  If not, see <https://www.gnu.org/licenses/>.
"""produces some Ontology Web Language schema and views

"""

from logilab.mtconverter import TransformError

from cubicweb import _
from cubicweb.predicates import none_rset, match_view
from cubicweb_web.view import StartupView, EntityView
from cubicweb_web.action import Action
from cubicweb_web.views import schema

OWL_CARD_MAP = {
    "1": '<rdf:type rdf:resource="&owl;FunctionalProperty"/>',
    "?": '<owl:maxCardinality rdf:datatype="&xsd;int">1</owl:maxCardinality>',
    "+": '<owl:minCardinality rdf:datatype="&xsd;int">1</owl:minCardinality>',
    "*": "",
}

OWL_TYPE_MAP = {
    "String": "xsd:string",
    "Bytes": "xsd:byte",
    "Password": "xsd:byte",
    "Boolean": "xsd:boolean",
    "Int": "xsd:int",
    "BigInt": "xsd:int",
    "Float": "xsd:float",
    "Decimal": "xsd:decimal",
    "Date": "xsd:date",
    "Datetime": "xsd:dateTime",
    "TZDatetime": "xsd:dateTime",
    "Time": "xsd:time",
    "TZTime": "xsd:time",
    "Interval": "xsd:duration",
}

OWL_OPENING_ROOT = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rdf:RDF [
        <!ENTITY owl "http://www.w3.org/2002/07/owl#" >
        <!ENTITY xsd "http://www.w3.org/2001/XMLSchema#" >
]>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
    xmlns:owl="http://www.w3.org/2002/07/owl#"
    xmlns="http://logilab.org/owl/ontologies/%(appid)s#"
    xmlns:%(appid)s="http://logilab.org/owl/ontologies/%(appid)s#"
    xmlns:base="http://logilab.org/owl/ontologies/%(appid)s">

  <owl:Ontology rdf:about="">
    <rdfs:comment>
    %(appid)s Cubicweb OWL Ontology
    </rdfs:comment>
  </owl:Ontology>"""

OWL_CLOSING_ROOT = "</rdf:RDF>"


class OWLView(StartupView):
    """This view export in owl format schema database. It is the TBOX"""

    __regid__ = "owl"
    title = _("owl")
    templatable = False
    content_type = "application/xml"  # 'text/xml'

    def call(self, writeprefix=True):
        skipmeta = int(self._cw.form.get("skipmeta", True))
        if writeprefix:
            self.w(OWL_OPENING_ROOT, {"appid": self._cw.vreg.schema.name})
        self.visit_schema(skiptypes=skipmeta and schema.SKIP_TYPES or ())
        if writeprefix:
            self.w(OWL_CLOSING_ROOT)

    def should_display_rschema(self, eschema, rschema, role):
        return rschema not in self.skiptypes and (
            rschema.may_have_permission("read", self._cw, eschema, role)
        )

    def visit_schema(self, skiptypes):
        """get a layout for a whole schema"""
        self.skiptypes = skiptypes
        entities = sorted(
            eschema
            for eschema in self._cw.vreg.schema.entities()
            if not eschema.final or eschema in skiptypes
        )
        self.w("<!-- classes definition -->")
        for eschema in entities:
            self.visit_entityschema(eschema)
            self.w("<!-- property definition -->")
            self.visit_property_schema(eschema)
            self.w("<!-- datatype property -->")
            self.visit_property_object_schema(eschema)

    def visit_entityschema(self, eschema):
        """get a layout for an entity OWL schema"""
        self.w('<owl:Class rdf:ID="%s">', eschema)
        self.w("<!-- relations -->")
        for rschema, targetschemas, role in eschema.relation_definitions():
            if not self.should_display_rschema(eschema, rschema, role):
                continue
            for oeschema in targetschemas:
                card = rschema.role_relation_definition(
                    eschema, oeschema, role
                ).role_cardinality(role)
                cardtag = OWL_CARD_MAP[card]
                if cardtag:
                    self.w(
                        """<rdfs:subClassOf>
 <owl:Restriction>
  <owl:onProperty rdf:resource="#%s"/>
  %s
 </owl:Restriction>
</rdfs:subClassOf>""",
                        rschema,
                        cardtag,
                    )

        self.w("<!-- attributes -->")
        for rschema, aschema in eschema.attribute_definitions():
            if not self.should_display_rschema(eschema, rschema, "subject"):
                continue
            self.w(
                """<rdfs:subClassOf>
  <owl:Restriction>
   <owl:onProperty rdf:resource="#%s"/>
   <rdf:type rdf:resource="&owl;FunctionalProperty"/>
  </owl:Restriction>
</rdfs:subClassOf>""",
                rschema,
            )
        self.w("</owl:Class>")

    def visit_property_schema(self, eschema):
        """get a layout for property entity OWL schema"""
        for rschema, targetschemas, role in eschema.relation_definitions():
            if not self.should_display_rschema(eschema, rschema, role):
                continue
            for oeschema in targetschemas:
                self.w(
                    """<owl:ObjectProperty rdf:ID="%s">
 <rdfs:domain rdf:resource="#%s"/>
 <rdfs:range rdf:resource="#%s"/>
</owl:ObjectProperty>""",
                    rschema,
                    eschema,
                    oeschema.type,
                )

    def visit_property_object_schema(self, eschema):
        for rschema, aschema in eschema.attribute_definitions():
            if not self.should_display_rschema(eschema, rschema, "subject"):
                continue
            self.w(
                """<owl:DatatypeProperty rdf:ID="%s">
  <rdfs:domain rdf:resource="#%s"/>
  <rdfs:range rdf:resource="%s"/>
</owl:DatatypeProperty>""",
                rschema,
                eschema,
                OWL_TYPE_MAP[aschema.type],
            )


class OWLABOXView(EntityView):
    """This view represents a part of the ABOX for a given entity."""

    __regid__ = "owlabox"
    title = _("owlabox")
    templatable = False
    content_type = "application/xml"  # 'text/xml'

    def call(self):
        self.w(OWL_OPENING_ROOT, {"appid": self._cw.vreg.schema.name})
        for i in range(self.cw_rset.rowcount):
            self.cell_call(i, 0)
        self.w(OWL_CLOSING_ROOT)

    def cell_call(self, row, col):
        self.wview("owlaboxitem", self.cw_rset, row=row, col=col)


class OWLABOXItemView(EntityView):
    """This view represents a part of the ABOX for a given entity."""

    __regid__ = "owlaboxitem"
    templatable = False
    content_type = "application/xml"  # 'text/xml'

    def cell_call(self, row, col):
        entity = self.cw_rset.complete_entity(row, col)
        eschema = entity.e_schema
        self.w('<%s rdf:ID="%s">', eschema, entity.eid)
        self.w("<!--attributes-->")
        for rschema, aschema in eschema.attribute_definitions():
            if rschema.meta:
                continue
            rdef = rschema.relation_definition(eschema, aschema)
            if not rdef.may_have_permission("read", self._cw):
                continue
            aname = rschema.type
            if aname == "eid":
                continue
            try:
                attr = entity.printable_value(aname, format="text/plain")
                if attr:
                    self.w("<%s>%s</%s>", aname, attr, aname)
            except TransformError:
                pass
        self.w("<!--relations -->")
        for rschema, targetschemas, role in eschema.relation_definitions():
            if rschema.meta:
                continue
            for tschema in targetschemas:
                rdef = rschema.role_relation_definition(eschema, tschema, role)
                if rdef.may_have_permission("read", self._cw):
                    break
            else:
                # no read perms to any relation of this type. Skip.
                continue
            if role == "object":
                attr = "reverse_%s" % rschema.type
            else:
                attr = rschema.type
            for x in getattr(entity, attr):
                self.w("<%s>%s %s</%s>", attr, x.__regid__, x.eid, attr)
        self.w("</%s>", eschema)


class DownloadOWLSchemaAction(Action):
    __regid__ = "download_as_owl"
    __select__ = none_rset() & match_view("schema")

    category = "mainactions"
    title = _("download schema as owl")

    def url(self):
        return self._cw.build_url("view", vid="owl")
