# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Minimal models for LinkML elements."""

from dataclasses import dataclass, field
from typing import Iterable


@dataclass(frozen=True)
class MinimalSlot:
    """A minimal representation of a LinkML slot without a name"""

    range: str
    multivalued: bool


@dataclass(frozen=True)
class MinimalNamedSlot(MinimalSlot):
    """A minimal representation of a LinkML slot with a name."""

    slot_name: str

    def __lt__(self, other):
        return (
            self.slot_name < other.slot_name
            and self.multivalued < other.multivalued
            and self.range < other.range
        )


class MinimalClass(frozenset[MinimalNamedSlot]):
    """A minimal representation of a LinkML class"""


class NamedClassConflict(RuntimeError):
    """Raised when an attempt is made to add two classes of the same name but
    conflicting definitions to the same model."""


@dataclass
class MinimalLinkMLModel:
    """A minimal representation of a LinkML model in form of a dictionary
    mapping anonymous classes to their names."""

    __AUTO_GENERATION_PREFIX = "AutoGeneratedClass"

    named_classes: dict[str, MinimalClass] = field(default_factory=dict)
    anonymous_classes: dict[MinimalClass, str] = field(default_factory=dict)

    def all_classes(self) -> Iterable[tuple[str, MinimalClass]]:
        """Iterates all class names and definitions currently in the model."""
        for cls_name, cls_def in self.named_classes.items():
            yield cls_name, cls_def
        for cls_def, cls_name in self.anonymous_classes.items():
            yield cls_name, cls_def

    def add_named_class(self, cls_def: MinimalClass, cls_name: str) -> None:
        """Adds a named class. Raises an error if the class name is already used
        for a different class definition."""
        if (
            cls_name in self.named_classes and cls_def != self.named_classes[cls_name]
        ) or cls_name.startswith(self.__AUTO_GENERATION_PREFIX):
            raise NamedClassConflict(cls_name)
        self.named_classes[cls_name] = cls_def

    def add_anonymous_class(self, cls_def: MinimalClass) -> str:
        """Adds an anonymous class and returns a generated class name. If the
        same class was previously defined, the class is re-used."""
        if cls_def in self.anonymous_classes:
            cls_name = self.anonymous_classes[cls_def]
        else:
            cls_name = f"{self.__AUTO_GENERATION_PREFIX}{len(self.anonymous_classes)}"
            self.anonymous_classes[cls_def] = cls_name
        return cls_name
