"""
_____________________________________________________________________.

:PROJECT: LARAsuite

*lara_django_base models *

:details: lara_django_base database models.
         -
         - fixtures can be generated with
         lara-django-dev dumpdata lara_django_base.itemstatus --indent 2 --natural-primary > 2_itemstatus_fix.json

         model_curi are compact URIs (CURIs) for the model, e.g. lara:base/ItemStatus.
         They are used for semantic representation.

:authors: mark doerr <mark.doerr@uni-greifswald.de>
          David Lacko @deiwo
          Jan Velecký
________________________________________________________________________
"""

import hashlib
import json
import uuid
from typing import Any, ClassVar
from urllib.parse import urlparse

from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.core.validators import RegexValidator
from django.db import models
from django.utils import timezone

settings.FIXTURES += [
    "1_currency_fix",
    "2_datatypes_fix",
    "2_itemstatus_fix",
    "2_tags_fix",
    "5_namespace_fix",
    "6_iana_media_type_fix",
    "9_physical_unit_fix",
    "9_physical_state_matter_fix",
    "9_scientific_constants",
]


class URNField(models.CharField):
    """URN field that accepts URLs that adhere the scheme for URNs  only."""

    default_validators : ClassVar[list[RegexValidator]] = [
        RegexValidator(
            regex=r"^urn:[a-z0-9][a-z0-9-]{1,31}:([a-z0-9()+,-.:=@;$_!*\'%/?#]|%[0-9a-f][0-9a-f])*$",
            message="Please enter a valid URN, s. https://en.wikipedia.org/wiki/Uniform_Resource_Name",
            code="invalid_URN",
        )
    ]  # [URLValidator(schemes=['urn'])]

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.max_length = 512


class MediaType(models.Model):
    """
    Generic media type (IANA MIME system).

    See https://www.iana.org/assignments/media-types/media-types.xhtml for media files, like
    e.g. svg, JSON, png, mp3, pdf, ...
    Format:
     type / subtype *(; parameter) application/x-animl, application/json
    """

    model_curi = "lara:base/MediaType"

    mediatype_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(help_text="name of the IANA application")
    # TODO: check for uniqueness of csv file
    media_type = models.TextField(
        unique=True,
        help_text="""name of IANA/MIME media type in the format:
                    type '/' [tree '.'] subtype ['+' suffix] *[';' parameter]""",
    )  # unique=True,
    def_filename_extension_regex = models.TextField(
        blank=True,
        unique=True,
        help_text=r"""default filename extension regular expression,
                     e.g. ^.*\ .(jpg|JPG)$ ,  ^.*\ .(gif|GIF|doc|DOC|pdf|PDF)$""",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation",
    )
    description = models.TextField(blank=True, help_text="description of file type")

    class Meta:
        """Model metadata."""

        ordering: ClassVar[list[str]] = ["name"]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=["name", "media_type"], name="name_media_type_idx")
        ]
        db_table_comment = (
            "Generic media Type (IANA type/subtype) for media files, like, "
            "e.g., application/text, application/json, image/svg+xml, image/png, audio/mp3, application/pdf, "
        )

    def __str__(self):
        """Return the media type string."""
        return self.media_type or ""

    def __repr__(self):
        """Return the media type representation."""
        return self.media_type or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/MediaType/"
                + self.name.replace(" ", "_").lower().strip()
            )
        if self.mediatype_id is None:
            if self.media_type is not None:
                self.mediatype_id = uuid.uuid5(uuid.NAMESPACE_URL, self.media_type)
            else:
                self.mediatype_id = uuid.uuid4()

        super().save(*args, **kwargs)


class DataType(models.Model):
    """
    Generic DataType that can be used to classify ExtraData (s. below).

    e.g. email_home, email_lab, telephone_number_home1, company_x_customer_number,
    sequence data, absorption data ...
    """

    model_curi = "lara:base/DataType"

    datatype_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.TextField(
        unique=True, db_index=True, help_text="name of the data type"
    )
    media_type = models.ForeignKey(
        MediaType,
        related_name="%(app_label)s_%(class)s_file_types_related",
        related_query_name="%(app_label)s_%(class)s_file_types_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="IANA media type of extra data file",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        blank=True, help_text="description of extra data type"
    )

    class Meta:
        """Model metadata."""

        ordering: ClassVar[list[str]] = ["name"]
        db_table_comment = (
            "Generic DataType that can be used to classify Data (s. below) e.g. email_home,"
            "email_lab, telephone_number_home1, company_x_customer_number, sequence data, absorption data ..."
        )

    def __str__(self):
        """Return the data type name."""
        return self.name or ""

    def __repr__(self):
        """Return the data type representation."""
        return self.name or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/DataType/"
                + self.name.replace(" ", "_").lower().strip()
            )
        if self.datatype_id is None:
            if self.name is not None:
                self.datatype_id = uuid.uuid5(uuid.NAMESPACE_URL, self.name)
            else:
                self.datatype_id = uuid.uuid4()

        super().save(*args, **kwargs)


class Namespace(models.Model):
    """
    Generic namespace model.

    Namespaces can be used to specify the scope or provenience of data, entities, etc. and help to make
    them unique.
    """

    model_curi = "lara:base/Namespace"
    namespace_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    uri = models.URLField(
        unique=True,
        db_index=True,
        help_text="Universal Resource Identifier - A unique namespace for identifying data, https://de.unigreifswald/biochem/akb",
    )
    description = models.TextField(
        blank=True, help_text="description of this Namespace"
    )

    # search_vector = SearchVectorField(null=True)

    class Meta:
        """Model metadata."""

        ordering: ClassVar[list[str]] = ["uri"]
        db_table_comment = (
            "Generic Namespace model: Namespaces can be used to specify the scope or "
            "provenience of data, entities, etc. and helps to make them unique."
        )
        # indexes = (GinIndex(fields=["search_vector"]),)

    def __str__(self):
        """Return the namespace URI."""
        return self.uri or ""

    def __repr__(self):
        """Return the namespace representation."""
        return self.uri or ""

    def save(self, *args, **kwargs):
        """Generate default values for the id."""
        if self.namespace_id is None:
            if self.uri is not None:
                self.namespace_id = uuid.uuid5(uuid.NAMESPACE_URL, self.uri)
            else:
                self.namespace_id = uuid.uuid4()

        super().save(*args, **kwargs)

    def parse_uri(self):
        """
        Parse a URI into parts.

        To retrieve the parts of the URI, use the following attributes (example: https://de.unigreifswald/biochem/akb"):
                result = parse_URI()
                    result.scheme : "https"
                    result.netloc : "de.unigreifswald"
                    result.path : "/biochem/akb"
        """
        return urlparse(self.uri)


class Tag(models.Model):
    """
    Generic tags and keywords model.

    Tags and keywords can be used to group related information or make objects findable.
    """

    model_curi = "lara:base/Tag"
    tag_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    tag = models.TextField(
        unique=True,
        db_index=True,
        help_text="a tag, can be used for tagging items, like 'reviewed', 'interesting',"
        " but also for keywords, like 'python', 'evolution'",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    # search_vector = SearchVectorField(null=True)

    class Meta:
        """Model metadata."""

        ordering: ClassVar[list[str]] = ["tag"]
        db_table_comment = (
            "Generic tags and keywords model: Tags and keywords can be used"
            "to group related information or make objects findable."
        )
        # indexes = (GinIndex(fields=["search_vector"]),)

    def __str__(self):
        """Return the tag string."""
        return self.tag or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/Tag/"
                + self.tag.replace(" ", "_").lower().strip()
            )

        super().save(*args, **kwargs)

class ItemRole(models.Model):
    """
    Generic item role.

    Can be used for indicating the role of LARA items, like containers or processes: sample, reference,
    standard, control, ...
    """

    model_curi = "lara:base/ItemRole"
    itemrole_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    role = models.TextField(
        db_index=True,
        help_text="item role, like sample, reference, standard, control, ..",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        blank=True, help_text="A short description of the item role"
    )

    class Meta:
        """Model metadata."""

        verbose_name_plural = "ItemRoles"
        db_table_comment = (
            "Generic item role/state, can be used for indicating the role of LARA items,"
            "like a containers or processes: sample, reference, standard, control, ..."
        )

    def __str__(self):
        """Return the item role string."""
        return self.role or ""

class ItemStatus(models.Model):
    """
    Generic item status/state for LARA items.

    Indicates status for containers or processes (planned, started, evaluating) or order states, like
    to_order, pending, ordered, received ...
    Hint: please do not use tags for this - separating tag and status makes processing clearer.
    """

    model_curi = "lara:base/ItemStatus"
    itemstatus_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    status = models.TextField(
        db_index=True,
        help_text="item status, like planned, started, evaluating, .. finished",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    color = models.TextField(
        # default=ColorBlind16.full_color_scheme[randint(0, 16)].get_web(),
        blank=True,
        help_text="colour scheme indicating the status of the item",
    )
    value = models.FloatField(
        default=0.0,
        blank=True,
        help_text="relative value between 0 and 1, indicating the status of the item",
    )
    description = models.TextField(
        blank=True, help_text="A short description of the item status"
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        blank=True,
        help_text="tags for item status",
    )

    class Meta:
        """Model metadata."""

        verbose_name_plural = "ItemStatus"
        db_table_comment = (
            "Generic item status/state, can be used for indicating the status of LARA items,"
            "like a containers or processes: planned, started, evaluating or order states, like to_order,"
            "pending, ordered, received ..."
        )

    def __str__(self):
        """Return the item status string."""
        return self.status or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/ItemStatus/"
                + self.status.replace(" ", "_").lower().strip()
            )

        super().save(*args, **kwargs)


# TODO: add a subset for status

# class ItemRole(models.Model):
#     """generic item role, can be used for indicating the role of LARA items,
#     like a containers or processes: sample, reference, standard, control, ...
#     """

#     model_curi = "lara:base/ItemRole"
#     itemrole_id = models.UUIDField(
#         primary_key=True, default=uuid.uuid4, editable=False
#     )
#     role = models.TextField(
#         db_index=True,
#         help_text="item role, like sample, reference, standard, control, ..",
#     )
#     iri = models.URLField(
#         blank=True,
#         null=True,
#         unique=True,
#         max_length=512,
#         help_text="International Resource Identifier - IRI: is used for semantic representation ",
#     )
#     description = models.TextField(
#         blank=True, help_text="A short description of the item role"
#     )

#     class Meta:
#         verbose_name_plural = "ItemRoles"
#         db_table_comment = (
#             "Generic item role/state, can be used for indicating the role of LARA items,"
#             "like a containers or processes: sample, reference, standard, control, ..."
#         )

#     def __str__(self):
#         return self.role or ""

#     def save(self, *args, **kwargs):
#         """
#         Here we generate some default values for the IRI
#         """
#         if not self.iri:
#             self.iri = (
#                 settings.LARA_PREFIXES["lara"]
#                 + "base/ItemRole/"
#                 + self.role.replace(" ", "_").lower().strip()
#             )

#         super().save(*args, **kwargs)


class BlockchainHashesAbstr(models.Model):
    """Store the latest blockchain hash of a data block to build the chain."""

    blockchainhashes_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )

    block_id = models.TextField(
        blank=True,
        help_text="Identifier of the data block for which the blockchain hash was currently calculated,"
        "e.g., data of a measurement series.",
    )

    hash_sha256 = models.CharField(
        max_length=256,
        unique=True,
        blank=True,
        null=True,
        default=None,
        help_text="SHA256 hash of all data (JSON and XML)",
    )
    hash_sha512 = models.CharField(
        max_length=512,
        unique=True,
        blank=True,
        null=True,
        default=None,
        help_text="SHA512 hash of all data (JSON and XML)",
    )
    timestamp = models.DateTimeField(
        default=timezone.now,
        help_text="date and time when the blockchain hash was last modified",
    )

    class Meta:
        """Model metadata."""

        abstract = True
        verbose_name_plural = "BlockchainHashes"

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        self.timestamp = timezone.now()

        super().save(*args, **kwargs)


class ExtraDataAbstr(models.Model):
    """
    Extend data with extra information.

    e.g. more telephone numbers, customer numbers, ...
    """

    extradata_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    data_type = models.ForeignKey(
        DataType,
        related_name="%(app_label)s_%(class)s_name_related",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    namespace = models.ForeignKey(
        Namespace,
        related_name="%(app_label)s_%(class)s_namespaces_related",
        related_query_name="%(app_label)s_%(class)s_namespaces_related_query",
        on_delete=models.CASCADE,
        help_text="namespace of data",
    )
    name_display = models.TextField(
        blank=True,
        help_text="Human readable display name or title of extra data",
    )
    name = models.TextField(blank=True, help_text="name of extra data")
    name_full = models.TextField(
        unique=True, blank=True, help_text="full name of extra data"
    )

    version = models.TextField(blank=True, help_text="version of extra data")

    hash_sha256 = models.CharField(
        max_length=256,
        unique=True,
        blank=True,
        default=None,
        help_text="SHA256 hash of all data (JSON and XML)",
    )
    data_json = models.JSONField(blank=True, null=True, help_text="generic JSON field")
    media_type = models.ForeignKey(
        MediaType,
        related_name="%(app_label)s_%(class)s_file_types_related",
        related_query_name="%(app_label)s_%(class)s_file_types_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="IANA media type of extra data file",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    url = models.URLField(
        blank=True,
        help_text="Universal Resource Locator - this can be used to link the data to external locations",
    )
    datetime_created = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was created"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )
    description = models.TextField(blank=True, help_text="description of extra data")
    search_vector = SearchVectorField(null=True)

    class Meta:
        """Model metadata."""

        abstract = True
        verbose_name_plural = "ExtraData"
        constraints: ClassVar[list[models.UniqueConstraint]] = [
            models.UniqueConstraint(
                fields=["name", "namespace", "version"],
                name="%(app_label)s_%(class)s_name_namespace_version_unique",
            ),
        ]
        indexes: ClassVar[tuple[models.Index, ...]] = (GinIndex(fields=["search_vector"]),)

    def __str__(self):
        """Return the extra data name."""
        return f"{self.name_full}" or ""

    def __repr__(self):
        """Return the extra data representation."""
        return f"{self.name_full}" or ""


class ExtraData(ExtraDataAbstr):
    """
    Extend data with extra information.

    e.g., more telephone numbers, customer numbers, ...
    """

    model_curi = "lara:base/ExtraData"
    # extradata_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    file = models.FileField(
        upload_to="base", blank=True, null=True, help_text="rel. path/filename"
    )
    image = models.ImageField(
        upload_to="base/images/",
        blank=True,
        null=True,
        help_text="location room map rel. path/filename to image",
    )

    class Meta:
        """Model metadata."""

        indexes = (
            models.Index(fields=["name", "namespace"], name="name_namespace_idx"),
        )
        db_table_comment = (
            "This class can be used to extend data, by extra information,"
            "e.g. more telephone numbers, customer numbers, ..."
        )
        verbose_name_plural = "ExtraData"

    # def save(self, force_insert=None, using=None): #force_insert=force_insert, using=using
    def save(self, *args, **kwargs):
        """Generate default values for name and name_full."""
        if self.version is None or self.version == "":
            self.version = "0.0.1"

        if self.name is None or self.name == "":
            if self.name_display is None or self.name_display == "":
                self.name = "_".join(
                    (
                        timezone.now().strftime("%Y%m%d_%H%M%S"),
                        "data",
                        self.hash_sha256[:8],
                    )
                )
            else:
                self.name = self.name_display.replace(" ", "_").lower().strip()

        else:
            self.name = self.name.replace(" ", "_").lower().strip()
        if self.name_full is None or self.name_full == "":
            if self.namespace is not None and self.version is not None:
                self.name_full = f"{self.namespace.uri}/" + "_".join(
                    (self.name.replace(" ", "_"), self.version)
                )
            else:
                self.name_full = "_".join((self.name.replace(" ", "_"), self.version))

        if not self.iri:
            netloc = self.namespace.parse_uri().netloc
            path = self.namespace.parse_uri().path
            self.iri = f"{settings.LARA_PREFIXES['lara']}base/ExtraData/{netloc}{path}/{self.name.replace(' ', '_').lower().strip()}"  # noqa: E501

        if self.hash_sha256 is None:
            if self.data_json is not None:
                to_hash = json.dumps(self.data_json)
                self.hash_sha256 = hashlib.sha256(to_hash.encode("utf-8")).hexdigest()
            else:
                self.hash_sha256 = hashlib.sha256(
                    str(self.iri).encode("utf-8")
                ).hexdigest()

        self.datetime_created = (
            timezone.now() if self.datetime_created is None else self.datetime_created
        )

        self.datetime_last_modified = timezone.now()

        super().save(*args, **kwargs)


class ExternalResource(models.Model):
    """
    Generic external resource class.

    External resources can be used to link to external resources, like e.g. literature, pdf, a website,
    a file, a database, ...
    """

    model_curi = "lara:base/ExternalResource"
    externalresource_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    namespace = models.ForeignKey(
        Namespace,
        related_name="%(app_label)s_%(class)s_namespaces_related",
        related_query_name="%(app_label)s_%(class)s_namespaces_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="namespace of external resource",
    )
    name_display = models.TextField(
        blank=True,
        help_text="Human readable display name or title of external resource",
    )
    name = models.TextField(
        unique=True, blank=True, help_text="name of the external resource"
    )
    resource_type = models.ForeignKey(
        DataType,
        related_name="%(app_label)s_%(class)s_resource_type_related",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="type of external resource, like pdf, website, handle, DOI, ...",
    )
    url = models.URLField(
        blank=True,
        help_text="Universal Resource Locator - URL - this can be used to link the data to external locations",
    )
    iri = models.URLField(
        blank=True,
        unique=True,
        help_text="Universal Resource Name - URI - this can be used to uniquely identify the external resource",
    )
    prefix = models.TextField(
        blank=True,
        help_text="prefix for compact URIs (CURI), e.g. 'skos' for 'http://www.w3.org/2004/02/skos/core#'",
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        blank=True,
        help_text="tags for external resource",
    )
    description = models.TextField(
        blank=True, help_text="description of the external resource"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )
    search_vector = SearchVectorField(null=True)

    class Meta:
        """Model metadata."""

        indexes = (
            models.Index(fields=["name", "namespace"], name="ext_res_name_namespace_idx"),
            GinIndex(fields=["search_vector"]),
        )
        db_table_comment = (
            "Generic external resource class: External resources can be used "
            "to link to external resources, like e.g. literature, pdf, a website, a file, a database, ..."
        )

    def __str__(self):
        """Return the external resource name."""
        return self.name or ""

    def __repr__(self):
        """Return the external resource representation."""
        return self.name or ""

    def save(self, *args, **kwargs):
        """Generate default values for the name."""
        if self.name_display is not None or "":
            if self.name is None or self.name == "":
                self.name = self.name_display.replace(" ", "_").lower().strip()
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/ExternalResource/"
                + self.name.replace(" ", "_").lower().strip()
            )
        self.datetime_last_modified = timezone.now()

        super().save(*args, **kwargs)


class PhysicalUnit(models.Model):
    """Physical units, like meter, kilogram, ..., preferably SI units."""

    model_curi = "lara:base/PhysicalUnit"
    physicalunit_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(help_text="physical unit, like meter, kilogram, ...")
    symbol = models.TextField(
        blank=True, help_text="symbol of the physical unit, like m, kg, ..."
    )
    dimension = models.TextField(
        blank=True, help_text="physical dimension, like length, mass, ..."
    )
    transformation_si = models.JSONField(
        blank=True,
        null=True,
        help_text="transformation formula to SI unit, in machine readable form (e.g. python code), like: x * 1000.0",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        blank=True, help_text="A short description of the physical unit"
    )

    class Meta:
        """Model metadata."""

        db_table_comment = (
            "Physical units, like, meter, kilogram, ..., preferably SI units"
        )

    def __str__(self):
        """Return the physical unit name."""
        return self.name or ""

    def __repr__(self):
        """Return the physical unit representation."""
        return self.name or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/PhysicalUnit/"
                + self.name.replace(" ", "_").lower().strip()
            )

        super().save(*args, **kwargs)


class PhysicalStateMatter(models.Model):
    """Physical state of matter, like solid, liquid, gas, plasma, ... ."""

    model_curi = "lara:base/PhysicalStateMatter"
    physicalstatematter_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(
        help_text="physical state of matter, like solid, liquid, gas, plasma, ..."
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        blank=True,
        help_text="A short description of the physical state of matter",
    )

    class Meta:
        """Model metadata."""

        db_table_comment = (
            "Physical state of matter, like solid, liquid, gas, plasma, ..."
        )
        verbose_name_plural = "PhysicalStatesMatter"

    def __str__(self):
        """Return the physical state name."""
        return self.name or ""

    def __repr__(self):
        """Return the physical state representation."""
        return self.name or ""


class ScientificConstant(models.Model):
    """Mathematical and physical constants."""

    model_curi = "lara:base/ScientificConstant"
    scientificconstant_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(blank=True, help_text="generic text field")
    symbol = models.TextField(blank=True, help_text="symbol of the constant")
    value = models.FloatField(blank=True, null=True, help_text="Float value of the ")
    uncertainty = models.FloatField(
        blank=True, null=True, help_text="uncertainty of the value +/-"
    )
    unit = models.ForeignKey(
        PhysicalUnit,
        related_name="%(app_label)s_%(class)s_units_related",
        related_query_name="%(app_label)s_%(class)s_units_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="physical unit",
    )
    data_json = models.JSONField(blank=True, null=True, help_text="complex content")
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    # -> external resources
    url_source = models.URLField(
        blank=True,
        help_text="Universal Resource Locator - URL - source, e.g. in literature",
    )
    url_definition = models.URLField(
        blank=True, help_text="Universal Resource Locator - URL - definition"
    )
    description = models.TextField(blank=True, help_text="description of the constant")
    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )
    search_vector = SearchVectorField(null=True)

    class Meta:
        """Model metadata."""

        db_table_comment = "Mathematical and Physical constants"
        indexes = (GinIndex(fields=["search_vector"]),)

    def __str__(self):
        """Return the constant name."""
        return f"{self.name}" or ""

    def __repr__(self):
        """Return the constant representation."""
        return f"{self.name} ({self.description})" or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/ScientificConstant/"
                + self.name.replace(" ", "_").lower().strip()
            )
        self.datetime_last_modified = timezone.now()

        super().save(*args, **kwargs)


class Currency(models.Model):
    """
    Generic currency class.

    Hint: see also Money class from django-shop.
    """

    model_curi = "lara:base/Currency"
    currency_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.TextField(unique=True, help_text="currency name")
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    alpha3code = models.TextField(
        blank=True,
        unique=True,
        help_text="acronym, e.g. EUR, USD, ..",
    )
    numeric_code = models.IntegerField(
        blank=True,
        null=True,
        help_text="International 3 number code, like 978 (for EUR) ",
    )
    symbol = models.TextField(blank=True, help_text="currency symbol, e.g. €, $, ...")
    exchange_rate_eur = models.DecimalField(
        max_digits=20,
        decimal_places=9,
        blank=True,
        null=True,
        help_text="currency exchange rate to Euro ",
    )
    datetime_last_modified = models.DateTimeField(
        default=timezone.now, help_text="date and time when data was last modified"
    )
    description = models.TextField(blank=True, help_text="description of the currency")

    class Meta:
        """Model metadata."""

        verbose_name_plural = "Currencies"
        db_table_comment = "Generic currency class"

    def __str__(self):
        """Return the currency name."""
        return self.name or ""

    def save(self, *args, **kwargs):
        """Generate default values for the IRI."""
        if not self.iri:
            self.iri = (
                settings.LARA_PREFIXES["lara"]
                + "base/Currency/"
                + self.name.replace(" ", "_").lower().strip()
            )

        super().save(*args, **kwargs)

    def to_euro(self, amount: float = 1.0):
        """Convert the given amount to EUR using the stored exchange rate."""
        return amount * self.exchange_rate_eur



class LARAExtension(models.Model):
    """
    Store configuration for LARA web UI extensions.

    Extensions can add new apps, features, or modules as an IFrame or a new page. An (external) URL can be
    provided to point to an external resource or microservice, or a Django app can be added. Extensions can
    be added to the main menu or to a sub-menu.
    """

    model_curi = "lara:base/LARAExtension"
    laraextension_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(unique=True, help_text="name of the extension")
    url = models.URLField(blank=True, help_text="External URL of the extension")
    icon = models.TextField(blank=True, help_text="SVG icon of the extension")
    parent = models.ForeignKey(
        "self",
        related_name="%(app_label)s_%(class)s_parents_related",
        related_query_name="%(app_label)s_%(class)s_parents_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="parent extension for building a route / menu hierarchy",
    )
    new_tab = models.BooleanField(default=False, help_text="open in new tab")
    target_element = models.TextField(
        blank=True, help_text="target HTML element of the extension"
    )
    components = models.JSONField(
        blank=True, null=True, help_text="components / templates of the extension"
    )

    description = models.TextField(blank=True, help_text="description of the extension")

    class Meta:
        """Model metadata."""

        db_table_comment = "LARA UI extensions"

    def __str__(self):
        """Return the extension name."""
        return self.name or ""

    def __repr__(self):
        """Return the extension representation."""
        return self.name or ""


