# SPDX-FileCopyrightText: Copyright DB InfraGO AG
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

__all__ = ["UnsupportedPluginError", "UnsupportedPluginVersionError"]

import collections.abc as cabc
import dataclasses
import re
import typing as t


class UnsupportedPluginError(ValueError):
    """Raised when a plugin is unknown."""


class UnsupportedPluginVersionError(ValueError):
    """Raised when the plugin's version is unsupported."""

    def __str__(self) -> str:
        plugin, ns_plugin = self.args
        assert isinstance(plugin, Plugin) and isinstance(ns_plugin, Plugin)
        return (
            f"Plugin '{plugin.name}' with unsupported version encountered: "
            f"{plugin.version!r} does not match {ns_plugin.version!r}."
        )


@dataclasses.dataclass(frozen=True)
class Plugin:
    """Capella xml-element/plugin with info about name and version."""

    name: str
    version: str | tuple[str, str] | None = None
    viewpoint: str | None = None

    def __post_init__(self) -> None:
        if self.version is not None and self.viewpoint is None:
            raise TypeError("Versioned plugins require a viewpoint")

    @property
    def min_version(self) -> str | None:
        if isinstance(self.version, str):
            return self.version
        if isinstance(self.version, tuple):
            return self.version[0]
        return None

    @property
    def max_version(self) -> str | None:
        if isinstance(self.version, str):
            return self.version
        if isinstance(self.version, tuple):
            return self.version[1]
        return None

    def __le__(self, other: Plugin) -> bool:
        if self.version is None:
            raise AttributeError(f"Plugin '{self.name}' has no version")

        if isinstance(other.version, tuple):
            assert all(isinstance(o, str) for o in other.version)
            vmin, vmax = tuple(map(_tofloat, other.version))
            if isinstance(self.version, tuple):
                vsmin, vsmax = tuple(map(_tofloat, self.version))
                return vsmin <= vmin <= vmax <= vsmax
            else:
                return vmin <= _tofloat(self.version) <= vmax

        assert other.version is not None
        oversion = _tofloat(other.version)
        if isinstance(self.version, tuple):
            vsmin, vsmax = tuple(map(_tofloat, self.version))
            return vsmin <= oversion <= vsmax

        return oversion <= _tofloat(self.version)

    def __str__(self) -> str:
        if suffix := self.min_version:
            return self.name + suffix
        return self.name

    def matches_version(self, value: str | None) -> bool:
        """Check whether a version number is supported by this plugin."""
        if value is None:
            return self.version is None
        if self.version is None:
            return True
        if isinstance(self.version, str):
            return self.version == value

        for mymin, mymax, their in zip(
            self.version[0].split("."),
            self.version[1].split("."),
            value.split("."),
        ):
            if not int(mymin) <= int(their) <= int(mymax):
                return False
        return True


def _tofloat(other: str) -> float:
    """Change 1.x.y...str into 1.x float."""
    version = other.split(".")
    if len(version) > 1:
        version = version[:2]

    return float(".".join(version))


def get_namespace_prefix(url: str) -> str:
    """Map a namespace URL to its symbolic name (the "prefix").

    Parameters
    ----------
    url
        A namespace URL.

    Raises
    ------
    UnsupportedPluginError
        If the given URL does not match any known plugin.
    UnsupportedPluginVersionError
        If the URL matches a known plugin, but the version indicated by
        the URL is outside of the supported range.

    Returns
    -------
    str
        The symbolic name of the namespace matching the URI.
    """
    match = re.match(r"(.*/)((?:\d+\.)*\d+)$", url)
    if match:
        plugin_name, version = match.groups()
    else:
        plugin_name, version = url, None

    matched_plugins = [
        (nskey, nsplugin)
        for nskey, nsplugin in NAMESPACES_PLUGINS.items()
        if nsplugin.name in (plugin_name, url)
    ]
    if not matched_plugins:
        raise UnsupportedPluginError(url)
    if len(matched_plugins) != 1:
        raise RuntimeError(f"Ambiguous namespace {url!r}: {matched_plugins}")
    prefix, plugin = matched_plugins[0]

    if not plugin.matches_version(version):
        raise UnsupportedPluginVersionError(plugin_name, version)

    return prefix


NAMESPACES_PLUGINS: t.Final[cabc.Mapping[str, Plugin]] = {
    "CapellaRequirements": Plugin(
        "http://www.polarsys.org/capella/requirements",
        None,
        "org.polarsys.capella.vp.requirements",
    ),
    "Requirements": Plugin(
        "http://www.polarsys.org/kitalpha/requirements",
        None,
        "org.polarsys.kitalpha.vp.requirements",
    ),
    "concern": Plugin(
        "http://www.eclipse.org/sirius/diagram/description/concern/1.1.0",
    ),
    "description": Plugin("http://www.eclipse.org/sirius/description/1.1.0"),
    "description_1": Plugin(
        "http://www.eclipse.org/sirius/diagram/description/1.1.0"
    ),
    "description_2": Plugin(
        "http://www.eclipse.org/sirius/table/description/1.1.0"
    ),
    "description_3": Plugin(
        "http://www.eclipse.org/sirius/diagram/sequence/description/2.0.0",
    ),
    "diagram": Plugin("http://www.eclipse.org/sirius/diagram/1.1.0"),
    "diagramstyler": Plugin("http://thalesgroup.com/mde/melody/ordering"),
    "ecore": Plugin("http://www.eclipse.org/emf/2002/Ecore"),
    "filter": Plugin(
        "http://www.eclipse.org/sirius/diagram/description/filter/1.1.0",
    ),
    "libraries": Plugin(
        "http://www.polarsys.org/capella/common/libraries/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "metadata": Plugin("http://www.polarsys.org/kitalpha/ad/metadata/1.0.0"),
    "notation": Plugin("http://www.eclipse.org/gmf/runtime/1.0.2/notation"),
    "org.polarsys.capella.core.data.capellacommon": Plugin(
        "http://www.polarsys.org/capella/core/common/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.capellacore": Plugin(
        "http://www.polarsys.org/capella/core/core/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.capellamodeller": Plugin(
        "http://www.polarsys.org/capella/core/modeller/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.cs": Plugin(
        "http://www.polarsys.org/capella/core/cs/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.ctx": Plugin(
        "http://www.polarsys.org/capella/core/ctx/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.epbs": Plugin(
        "http://www.polarsys.org/capella/core/epbs/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.fa": Plugin(
        "http://www.polarsys.org/capella/core/fa/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.information": Plugin(
        "http://www.polarsys.org/capella/core/information/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.information.datatype": Plugin(
        "http://www.polarsys.org/capella/core/information/datatype/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.information.datavalue": Plugin(
        "http://www.polarsys.org/capella/core/information/datavalue/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.interaction": Plugin(
        "http://www.polarsys.org/capella/core/interaction/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.la": Plugin(
        "http://www.polarsys.org/capella/core/la/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.oa": Plugin(
        "http://www.polarsys.org/capella/core/oa/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "org.polarsys.capella.core.data.pa": Plugin(
        "http://www.polarsys.org/capella/core/pa/",
        ("5.0.0", "6.0.0"),
        "org.polarsys.capella.core.viewpoint",
    ),
    "re": Plugin("http://www.polarsys.org/capella/common/re/1.3.0"),
    "sequence": Plugin("http://www.eclipse.org/sirius/diagram/sequence/2.0.0"),
    "style": Plugin(
        "http://www.eclipse.org/sirius/diagram/description/style/1.1.0"
    ),
    "table": Plugin("http://www.eclipse.org/sirius/table/1.1.0"),
    "viewpoint": Plugin("http://www.eclipse.org/sirius/1.1.0"),
    "xmi": Plugin("http://www.omg.org/XMI"),
    "xsi": Plugin("http://www.w3.org/2001/XMLSchema-instance"),
}
NAMESPACES: t.Final[cabc.Mapping[str, str]] = {
    nskey: str(plugin) for nskey, plugin in NAMESPACES_PLUGINS.items()
}
