"""Type-related utilities.

This module provides facilities to conveniently build Python counterparts to
Rust's `struct` and `enum` types that can be (de)serialized as events, requests
or replies within a [`Simulation`][nexosim.Simulation].

## Struct-like classes

Rust distinguishes between regular `struct` types (with named fields), unit
`struct` types and tuple `struct` types. Because these types are serialized
differently, it is important to preserve this distinction on the Python side.

Regular Rust `struct` types can be directly represented in Python with
dataclasses, using e.g. the standard `@dataclasses.dataclass` decorator or the
`@define` decorator from the `attrs` package.

Unit `struct` types are in turn represented by empty classes deriving from the
[UnitType][nexosim.types.UnitType] class. Likewise, tuple `struct` types are
represented by deriving from one of the `TupleType*Arg` classes.

Here are example Rust `struct` types and their Python counterparts:

=== "Rust"
    ```rust
    struct MyRegularStruct {
        foo: f64,
        bar: (u32, String),
    }

    struct MyUnitStruct;

    struct MyTupleStruct(u32, String);
    ```
=== "Python"
    ```python
    from dataclasses import dataclass

    from nexosim.types import UnitType, TupleType2Arg

    @dataclass
    class MyRegularStruct:
        foo: int
        bar: (int, str)

    class MyUnitStruct(UnitType): ... # no implementation needed

    class MyTupleStruct(TupleType2Arg[int, str]): ... # no implementation needed
    ```

In the case of tuple `struct` types, if you don't use a type checker or if your
type checker supports dynamically-generated base classes (as of the day of this
writing, this is the case for *Pyright* but not *Mypy*), it is possible to save
a bit of typing by using the convenience
[`tuple_type`][nexosim.types.tuple_type] type constructor instead of the
`TupleType*Arg` classes. For instant, the above `MyTupleStruct` could be instead
defined with:

```py
from nexosim.types import tuple_type

class MyTupleStruct(tuple_type(int, str)): ...
```

Note that tuple `struct` types with arity greater than 16 must be defined
manually with the [`tupleclass`][nexosim.types.tupleclass] decorator. This is
for instance how [`TupleType2Arg`][nexosim.types.TupleType2Arg] is defined:

```py
from nexosim.types import variantclass

@tupleclass
class TupleType2Arg[Arg0, Arg1]:
    _0: Arg0
    _1: Arg1
```

## Enum-like classes

Rust `enum` types are represented by classes decorated with
[`@enumclass`][nexosim.types.enumclass], defining the variants as nested
classes.

Unit-like, tuple-like and struct-like variants are defined in exactly the
same way as [`struct`-like classes][nexosim.types--struct-like-classes].

!!! warning
    Classes decorated with [`@enumclass`][nexosim.types.enumclass] implicitly
    define an `enum` type but are not the `enum` type itself.

    Rather, the `enum` type is represented by a Python `Union` of the nested
    variant types, which for convenience is automatically generated by the
    `@enumclass` decorator and exposed as the `type` class variable.

Here's an example Rust `enum` and its Python counterpart:

=== "Rust"
    ```rust
    enum MyEnum {
        MyUnitVariant,
        MyTupleVariant(i32, String),
        MyStructVariant { foo: f64, bar: u64 },
    }
    ```

=== "Python"
    ```python
    from dataclasses import dataclass
    from nexosim.types import enumclass, UnitType, TupleType2Arg

    @enumclass
    class MyEnum:
        class MyUnitVariant(UnitType): ...

        class MyTupleVariant(TupleType2Arg[int, str]): ...

        @dataclass
        class MyStructVariant:
            foo: float
            bar: int

        # The following class variable is automatically generated:
        #
        # type = MyUnitVariant | MyTupleVariant | MyStructVariant
    ```

If the above `enum` were in turn used within a Rust `struct`, it would be represented like this:

=== "Rust"
    ```rust
    struct MyComposedStruct {
        s: String,
        e: MyEnum,
    }
    ```
=== "Python"
    ```python
    @dataclass
    class MyComposedStruct:
        s: str
        e: MyEnum.type # <- the actual union type is given by the `type` field
    ```

## Type checking enums

Because the `type` field is dynamically added to the `enum` class definition by
the [`@enumclass`][nexosim.types.enumclass] decorator, it is invisible to type
checkers. This would in particular cause type checkers to complain in the
earlier declaration of `MyComposedStruct`, even though its definition is valid
and usable for serialization and deserialization purposes.

To prevent such false positives, you may elect to explicitly set the `type`
field, for instance:

```py
@enumclass
class MyEnum:
    class MyUnitVariant(UnitType): ...

    class MyTupleVariant(TupleType2Arg[int, str]): ...

    @dataclass
    class MyStructVariant:
        foo: float
        bar: int

    type = MyUnitVariant | MyTupleVariant | MyStructVariant
```

In order to prevent mistakes, the [`@enumclass`][nexosim.types.enumclass]
decorator raises an exception if an explicitly-set `type` field is not
equivalent to the auto-generated one.


## Pattern matching

Tuple- and struct-like `enum` variants are dataclasses, either because they are
explicitly marked as such (struct-like variants) or because they derive from
dataclasses (tuple-like variants). This implies that `enum`s can be
structurally matched.

This is an example of pattern matching for the previously-defined `MyEnum`:

```py
def print_my_enum(my_enum):
    match my_enum:
        case MyEnum.MyUnitVariant():
            print("MyUnitVariant")
        case MyEnum.MyTupleVariant(number, string):
            print(f"MyTupleVariant({number}, {string})")
        case MyEnum.MyStructVariant(foo=x, bar=y):
            print(f"MyStructVariant(foo={x}, bar={y})")
        case _:
            raise RuntimeError("unexpected enum variant")
```

[Explicitly setting the `type` field][nexosim.types--type-checking-enums]
additionally enables exhaustiveness checking. For instance, if the `type` field
is explicitly set in `MyEnum`'s definition, the missing case in the following
code can be caught by the type checker:

```py
def my_enum_discriminant(my_enum: MyEnum.type) -> int:
    match my_enum: # ERROR: function does not return `int` on all code paths
        case MyEnum.MyUnitVariant():
            return 0
        case MyEnum.MyTupleVariant(_):
            return 1
```
"""

import dataclasses
import inspect
import typing

import typing_extensions

from ._config import cbor2_converter as _cbor2_converter

_T = typing.TypeVar("_T")

_Arg0 = typing.TypeVar("_Arg0")
_Arg1 = typing.TypeVar("_Arg1")
_Arg2 = typing.TypeVar("_Arg2")
_Arg3 = typing.TypeVar("_Arg3")
_Arg4 = typing.TypeVar("_Arg4")
_Arg5 = typing.TypeVar("_Arg5")
_Arg6 = typing.TypeVar("_Arg6")
_Arg7 = typing.TypeVar("_Arg7")
_Arg8 = typing.TypeVar("_Arg8")
_Arg9 = typing.TypeVar("_Arg9")
_Arg10 = typing.TypeVar("_Arg10")
_Arg11 = typing.TypeVar("_Arg11")
_Arg12 = typing.TypeVar("_Arg12")
_Arg13 = typing.TypeVar("_Arg13")
_Arg14 = typing.TypeVar("_Arg14")
_Arg15 = typing.TypeVar("_Arg15")


def enumclass(cls: type[_T]) -> type[_T]:
    """Class decorator that marks a class as the definition of a Rust-like
    `enum`.

    Note that the decorated class is not the `enum` type itself: the Python
    `Union` of all variants that actually represents the `enum` is generated by
    this decorator and exposed as the `type` class variable.
    """
    variants = {name: ty for name, ty in cls.__dict__.items() if inspect.isclass(ty)}

    # Custom serialization hooks for enum variants.
    for name, ty in variants.items():
        if issubclass(ty, UnitType):
            hook = _variant_unstructure_hook(name, _unit_unstructure_hook)
        elif hasattr(ty, "_tuple_type"):
            hook = _variant_unstructure_hook(name, _tuple_unstructure_hook)
        else:
            hook = _variant_unstructure_hook(name, _struct_unstructure_hook)

        _cbor2_converter.register_unstructure_hook(ty, hook)

    # Infer the union type and assign it to `cls.type`.
    ty_iter = iter(variants.values())
    try:
        ty = next(ty_iter)
    except StopIteration as err:
        raise ValueError("enums must contain at least one variant") from err

    for t in ty_iter:
        ty = typing.Union[t, ty]  # type: ignore

    if hasattr(cls, "type"):
        if getattr(cls, "type") != ty:
            raise TypeError(
                "the explicitly provided 'type' field does not match the enum variants"
            )
    else:
        setattr(cls, "type", ty)

    # This avoids the variant's hook being overwritten by the enum hook
    # in single variant enums
    structure_func = _cbor2_converter.structure
    if typing.get_origin(ty) is not typing.Union:
        structure_func = _cbor2_converter.get_structure_hook(ty)

    # Custom deserialization hook.
    def structure_enum_hook(d: typing.Any, t: object):  # type: ignore
        if isinstance(d, dict):
            try:
                (ty_name,) = d  # type: ignore
                (ty_unstruct,) = d.values()  # type: ignore
            except ValueError as err:
                raise ValueError(
                    "enum structuring expects a string or a single-entry dictionary"
                ) from err
        else:
            if not isinstance(d, str):
                raise ValueError(
                    "enum structuring expects a string or a single-entry dictionary"
                )

            ty_name = d
            ty_unstruct = None

        try:
            ty = variants[ty_name]
        except KeyError as err:
            raise ValueError(
                "could not match unstructured data to a valid enum variant"
            ) from err

        return structure_func(ty_unstruct, ty)

    _cbor2_converter.register_structure_hook(ty, structure_enum_hook)

    return cls


def _variant_unstructure_hook(
    name: str, hook: typing.Callable[[typing.Any], typing.Any]
):
    def inner(e: typing.Any) -> typing.Any:
        e_unstruct = hook(e)

        return {name: e_unstruct}

    return inner


def _unit_unstructure_hook(e: typing.Any) -> typing.Any:  # type: ignore
    return None


def _tuple_unstructure_hook(e: typing.Any) -> typing.Any:  # type: ignore
    try:
        e_instruct = [
            _cbor2_converter.unstructure(getattr(e, field.name))
            for field in dataclasses.fields(e)
        ]
        if len(e_instruct) == 1:
            e_instruct = e_instruct[0]

        return e_instruct

    except TypeError as err:
        raise ValueError(
            "tuple-like types must expose a dataclass-like interface"
        ) from err


def _struct_unstructure_hook(e: typing.Any) -> typing.Any:  # type: ignore
    try:
        return {
            field.name: _cbor2_converter.unstructure(getattr(e, field.name))
            for field in dataclasses.fields(e)
        }
    except TypeError as err:
        raise ValueError("struct types must expose a dataclass-like interface") from err


@typing_extensions.dataclass_transform()
def tupleclass(cls: typing.Type[_T]) -> typing.Type[_T]:
    """Decorator that marks a class as a tuple-like type.

    This automatically makes the class a dataclass.
    """

    # Tuple struct pretty-printing.
    def repr(s):
        r = tuple(getattr(s, field.name) for field in dataclasses.fields(cls))  # type: ignore
        if len(r) == 1:
            return "{}({})".format(s.__class__.__qualname__, r[0])

        return "{}{}".format(s.__class__.__qualname__, r)

    setattr(cls, "__repr__", repr)

    # Make this class a dataclass and mark it as a tuple type.
    cls = dataclasses.dataclass(cls)
    setattr(cls, "_tuple_type", None)

    # Register the serialization hook.
    _cbor2_converter.register_unstructure_hook(cls, _tuple_unstructure_hook)

    # Register the deserialization hook.
    #
    # Note that zero-arity deserialization works by default since the class is a
    # dataclass.
    arity = len(dataclasses.fields(cls))  # type: ignore
    if arity == 1:

        def structure_hook(d, t):
            (ty,) = typing.get_args(t.__orig_bases__[0])  # type: ignore

            return t(_cbor2_converter.structure(d, ty))

        _cbor2_converter.register_structure_hook(cls, structure_hook)
    elif arity > 1:

        def structure_hook(d, t):
            ty_list = typing.get_args(t.__orig_bases__[0])
            args = [
                _cbor2_converter.structure(di, ty)
                for di, ty in zip(d, ty_list, strict=True)
            ]

            return t(*args)

        _cbor2_converter.register_structure_hook(cls, structure_hook)

    return cls


class UnitType:
    """Base class for unit-like types."""

    __unit_type__ = None

    def __repr__(self):
        return self.__class__.__qualname__


_cbor2_converter.register_structure_hook(UnitType, lambda d, t: t())
_cbor2_converter.register_unstructure_hook(UnitType, _unit_unstructure_hook)


@tupleclass
class TupleType0Arg:
    """Base class for nullary tuple-like types."""

    pass


@tupleclass
class TupleType1Arg(typing.Generic[_Arg0]):
    """Base class for unary tuple-like types."""

    _0: _Arg0


@tupleclass
class TupleType2Arg(typing.Generic[_Arg0, _Arg1]):
    """Base class for binary tuple-like types."""

    _0: _Arg0
    _1: _Arg1


@tupleclass
class TupleType3Arg(typing.Generic[_Arg0, _Arg1, _Arg2]):
    """Base class for ternary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2


@tupleclass
class TupleType4Arg(typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3]):
    """Base class for 4-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3


@tupleclass
class TupleType5Arg(typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4]):
    """Base class for 5-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4


@tupleclass
class TupleType6Arg(typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5]):
    """Base class for 6-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5


@tupleclass
class TupleType7Arg(typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6]):
    """Base class for 7-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6


@tupleclass
class TupleType8Arg(
    typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7]
):
    """Base class for 8-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7


@tupleclass
class TupleType9Arg(
    typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8]
):
    """Base class for 9-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8


@tupleclass
class TupleType10Arg(
    typing.Generic[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8, _Arg9]
):
    """Base class for 10-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9


@tupleclass
class TupleType11Arg(
    typing.Generic[
        _Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8, _Arg9, _Arg10
    ]
):
    """Base class for 11-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10


@tupleclass
class TupleType12Arg(
    typing.Generic[
        _Arg0,
        _Arg1,
        _Arg2,
        _Arg3,
        _Arg4,
        _Arg5,
        _Arg6,
        _Arg7,
        _Arg8,
        _Arg9,
        _Arg10,
        _Arg11,
    ]
):
    """Base class for 12-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10
    _11: _Arg11


@tupleclass
class TupleType13Arg(
    typing.Generic[
        _Arg0,
        _Arg1,
        _Arg2,
        _Arg3,
        _Arg4,
        _Arg5,
        _Arg6,
        _Arg7,
        _Arg8,
        _Arg9,
        _Arg10,
        _Arg11,
        _Arg12,
    ]
):
    """Base class for 13-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10
    _11: _Arg11
    _12: _Arg12


@tupleclass
class TupleType14Arg(
    typing.Generic[
        _Arg0,
        _Arg1,
        _Arg2,
        _Arg3,
        _Arg4,
        _Arg5,
        _Arg6,
        _Arg7,
        _Arg8,
        _Arg9,
        _Arg10,
        _Arg11,
        _Arg12,
        _Arg13,
    ]
):
    """Base class for 14-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10
    _11: _Arg11
    _12: _Arg12
    _13: _Arg13


@tupleclass
class TupleType15Arg(
    typing.Generic[
        _Arg0,
        _Arg1,
        _Arg2,
        _Arg3,
        _Arg4,
        _Arg5,
        _Arg6,
        _Arg7,
        _Arg8,
        _Arg9,
        _Arg10,
        _Arg11,
        _Arg12,
        _Arg13,
        _Arg14,
    ]
):
    """Base class for 15-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10
    _11: _Arg11
    _12: _Arg12
    _13: _Arg13
    _14: _Arg14


@tupleclass
class TupleType16Arg(
    typing.Generic[
        _Arg0,
        _Arg1,
        _Arg2,
        _Arg3,
        _Arg4,
        _Arg5,
        _Arg6,
        _Arg7,
        _Arg8,
        _Arg9,
        _Arg10,
        _Arg11,
        _Arg12,
        _Arg13,
        _Arg14,
        _Arg15,
    ]
):
    """Base class for 16-ary tuple-like types."""

    _0: _Arg0
    _1: _Arg1
    _2: _Arg2
    _3: _Arg3
    _4: _Arg4
    _5: _Arg5
    _6: _Arg6
    _7: _Arg7
    _8: _Arg8
    _9: _Arg9
    _10: _Arg10
    _11: _Arg11
    _12: _Arg12
    _13: _Arg13
    _14: _Arg14
    _15: _Arg15


@typing.overload
def tuple_type() -> type[TupleType0Arg]: ...


@typing.overload
def tuple_type(_0: type[_Arg0]) -> type[TupleType1Arg[_Arg0]]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0], _1: type[_Arg1]
) -> type[TupleType2Arg[_Arg0, _Arg1]]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0], _1: type[_Arg1], _2: type[_Arg2]
) -> TupleType3Arg[_Arg0, _Arg1, _Arg2]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0], _1: type[_Arg1], _2: type[_Arg2], _3: type[_Arg3]
) -> TupleType4Arg[_Arg0, _Arg1, _Arg2, _Arg3]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0], _1: type[_Arg1], _2: type[_Arg2], _3: type[_Arg3], _4: type[_Arg4]
) -> TupleType5Arg[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
) -> TupleType6Arg[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
) -> TupleType7Arg[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
) -> TupleType8Arg[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
) -> TupleType9Arg[_Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
) -> TupleType10Arg[
    _Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8, _Arg9
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
) -> TupleType11Arg[
    _Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8, _Arg9, _Arg10
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
    _11: type[_Arg11],
) -> TupleType12Arg[
    _Arg0, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5, _Arg6, _Arg7, _Arg8, _Arg9, _Arg10, _Arg11
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
    _11: type[_Arg11],
    _12: type[_Arg12],
) -> TupleType13Arg[
    _Arg0,
    _Arg1,
    _Arg2,
    _Arg3,
    _Arg4,
    _Arg5,
    _Arg6,
    _Arg7,
    _Arg8,
    _Arg9,
    _Arg10,
    _Arg11,
    _Arg12,
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
    _11: type[_Arg11],
    _12: type[_Arg12],
    _13: type[_Arg13],
) -> TupleType14Arg[
    _Arg0,
    _Arg1,
    _Arg2,
    _Arg3,
    _Arg4,
    _Arg5,
    _Arg6,
    _Arg7,
    _Arg8,
    _Arg9,
    _Arg10,
    _Arg11,
    _Arg12,
    _Arg13,
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
    _11: type[_Arg11],
    _12: type[_Arg12],
    _13: type[_Arg13],
    _14: type[_Arg14],
) -> TupleType15Arg[
    _Arg0,
    _Arg1,
    _Arg2,
    _Arg3,
    _Arg4,
    _Arg5,
    _Arg6,
    _Arg7,
    _Arg8,
    _Arg9,
    _Arg10,
    _Arg11,
    _Arg12,
    _Arg13,
    _Arg14,
]: ...


@typing.overload
def tuple_type(
    _0: type[_Arg0],
    _1: type[_Arg1],
    _2: type[_Arg2],
    _3: type[_Arg3],
    _4: type[_Arg4],
    _5: type[_Arg5],
    _6: type[_Arg6],
    _7: type[_Arg7],
    _8: type[_Arg8],
    _9: type[_Arg9],
    _10: type[_Arg10],
    _11: type[_Arg11],
    _12: type[_Arg12],
    _13: type[_Arg13],
    _14: type[_Arg14],
    _15: type[_Arg15],
) -> TupleType16Arg[
    _Arg0,
    _Arg1,
    _Arg2,
    _Arg3,
    _Arg4,
    _Arg5,
    _Arg6,
    _Arg7,
    _Arg8,
    _Arg9,
    _Arg10,
    _Arg11,
    _Arg12,
    _Arg13,
    _Arg14,
    _Arg15,
]: ...


def tuple_type(*args):  # type: ignore
    """A convenient type constructor for tuple-like types.

    This function can saves some typing and reduce import boilerplate compared
    to manually specifying the `TupleType*Arg` base classes. It takes the types
    of the components as arguments and is overloaded by arity to return the
    appropriate `TupleType*Arg` class.

    Warning:
        Note that not all type checkers support dynamically generated base
        classes and it may be necessary to manally specify the appropriate
        `TupleType*Arg` base class in order to satisfy the type checker.
    """
    match len(args):  # type: ignore
        case 0:
            return TupleType0Arg
        case 1:
            return TupleType1Arg[*args]  # type: ignore
        case 2:
            return TupleType2Arg[*args]  # type: ignore
        case 3:
            return TupleType3Arg[*args]  # type: ignore
        case 4:
            return TupleType4Arg[*args]  # type: ignore
        case 5:
            return TupleType5Arg[*args]  # type: ignore
        case 6:
            return TupleType6Arg[*args]  # type: ignore
        case 7:
            return TupleType7Arg[*args]  # type: ignore
        case 8:
            return TupleType8Arg[*args]  # type: ignore
        case 9:
            return TupleType9Arg[*args]  # type: ignore
        case 10:
            return TupleType10Arg[*args]  # type: ignore
        case 11:
            return TupleType11Arg[*args]  # type: ignore
        case 12:
            return TupleType12Arg[*args]  # type: ignore
        case 13:
            return TupleType13Arg[*args]  # type: ignore
        case 14:
            return TupleType14Arg[*args]  # type: ignore
        case 15:
            return TupleType15Arg[*args]  # type: ignore
        case 16:
            return TupleType16Arg[*args]  # type: ignore
        case _:
            raise NotImplementedError(
                "only variant arity up to 16 is implemented; use the 'variantclass' decorator instead"
            )
