# coding: utf-8

"""
Copy from search/martylib/core/singleton.py
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import abc
import collections
import threading
import funcsigs


class Singleton(abc.ABCMeta):
    """
    Returns the same instance every time it's called.

    Example usage:
        >>> import six
        >>> class MyAwesomeClass(six.with_metaclass(Singleton)):
        >>>     pass

        >>> a = MyAwesomeClass()
        >>> b = MyAwesomeClass()
        >>> a is b
        <<< True
    """

    _instances = {}
    _locks = collections.defaultdict(threading.Lock)

    def _get_instance_id(cls, args, kwargs):
        return cls

    def _extra_actions(cls, instance, args, kwargs):
        pass

    def __call__(cls, *args, **kwargs):
        with cls._locks[cls]:
            instance_id = cls._get_instance_id(args, kwargs)
            if instance_id not in cls._instances:
                cls._instances[instance_id] = super(Singleton, cls).__call__(*args, **kwargs)
            cls._extra_actions(cls._instances[instance_id], args, kwargs)
            return cls._instances[instance_id]


class ThreadLocalSingleton(Singleton):
    """
    Returns the same instance for a given thread every time it's called.
    Different threads have different instances.

    Example usage:
        >>> import six
        >>> class MyAwesomeClass(six.with_metaclass(ThreadLocalSingleton)):
        >>>     pass

        >>> a = MyAwesomeClass()

        >>> def my_function():
        >>>     b = MyAwesomeClass()
        >>>     c = MyAwesomeClass()
        >>>     print(a is b, b is c)

        >>> import threading
        >>> threading.Thread(target=my_function).start()
        <<< (False, True)
    """

    def _get_instance_id(cls, args, kwargs):
        return cls, threading.current_thread()


class InitSingleton(Singleton):
    """
    Returns the same instance for a given class, args and kwargs.
    WARNING: any unhashable type in args or kwargs will cause TypeError.

    Example usage:
        >>> import six
        >>> class Foo(six.with_metaclass(InitSingleton)):
        >>>     def __init__(self, x, y=0):
        >>>         self.coordinates = (x, y)

        >>> Foo(1) is Foo(1, y=0)     # True
        >>> Foo(1, 2) is Foo(1, y=2)  # True
        >>> Foo(1, 2) is Foo(1, y=3)  # False
        >>> Foo({})                   # TypeError: unhashable type: 'dict'
    """

    @staticmethod
    def _get_bound_init_parameters(cls, *args, **kwargs):
        signature = funcsigs.signature(cls.__init__)
        bound_params = signature.bind(cls, *args, **kwargs)

        result = collections.OrderedDict()
        for name, param in signature.parameters.items():
            if name in bound_params.arguments:
                value = bound_params.arguments[name]
            else:
                # Missing parameter, default used.
                value = param.default

            if hasattr(value, 'SerializeToString') and callable(value.SerializeToString):
                value = value.SerializeToString()

            result[name] = value

        return tuple(result.items())

    def _get_instance_id(cls, args, kwargs):
        return InitSingleton._get_bound_init_parameters(cls, *args, **kwargs)


class DumbInitSingleton(InitSingleton):
    """
    InitSingleton for classes, which `__init__` is not supported by `funcsigs`, e.g. Cython classes.
    """

    def _get_instance_id(cls, args, kwargs):
        return args, tuple(sorted(kwargs.items()))
