import abc
import functools
import threading
import typing as t


class SingletonMeta(type):
    """Suitable for constants

    Requires idempotent instance creation.
    """

    __INSTANCES = {}

    def __call__(cls, *args, **kwargs) -> t.Self:
        inst = cls.__INSTANCES.get(cls)
        if inst is None:
            inst = super().__call__(*args, **kwargs)
            cls.__INSTANCES[cls] = inst
        return inst


class AbcSingletonMeta(SingletonMeta, abc.ABCMeta):
    pass


class MultitonMeta(type):
    __INSTANCES = {}

    def __call__(cls, *args, **kwargs):
        try:  # key must be hashable
            tail = None
            if kwargs:
                tail = frozenset(kwargs.items())
            key = (cls, args, tail)
        except TypeError:
            return super().__call__(*args, **kwargs)

        inst = cls.__INSTANCES.get(key)
        if inst is None:
            inst = super().__call__(*args, **kwargs)
            cls.__INSTANCES[key] = inst
        return inst


class AbcMultitonMeta(MultitonMeta, abc.ABCMeta):
    pass


def threadsafe(metacls: type) -> type:
    tlock = threading.Lock()
    new_metacls = type(f"Threadsafe{metacls.__name__}", (metacls,), {})

    @functools.wraps(metacls.__call__)
    def guarded_call(self, *args, **kwargs):
        with tlock:
            return super(new_metacls, self).__call__(*args, **kwargs)

    new_metacls.__call__ = guarded_call
    return new_metacls


ThreadsafeMultitonMeta = threadsafe(MultitonMeta)
ThreadsafeAbcMultitonMeta = threadsafe(AbcMultitonMeta)

ThreadsafeSingletonMeta = threadsafe(SingletonMeta)
ThreadsafeAbcSingletonMeta = threadsafe(AbcSingletonMeta)
