import ddtrace

from ddtrace.vendor import debtcollector

from .internal.logger import get_logger
from .vendor import wrapt


log = get_logger(__name__)


# To set attributes on wrapt proxy objects use this prefix:
# http://wrapt.readthedocs.io/en/latest/wrappers.html
_DD_PIN_NAME = '_datadog_pin'
_DD_PIN_PROXY_NAME = '_self_' + _DD_PIN_NAME


class Pin(object):
    """Pin (a.k.a Patch INfo) is a small class which is used to
    set tracing metadata on a particular traced connection.
    This is useful if you wanted to, say, trace two different
    database clusters.

        >>> conn = sqlite.connect('/tmp/user.db')
        >>> # Override a pin for a specific connection
        >>> pin = Pin.override(conn, service='user-db')
        >>> conn = sqlite.connect('/tmp/image.db')
    """
    __slots__ = ['app', 'tags', 'tracer', '_target', '_config', '_initialized']

    @debtcollector.removals.removed_kwarg("app_type")
    def __init__(self, service, app=None, app_type=None, tags=None, tracer=None, _config=None):
        tracer = tracer or ddtrace.tracer
        self.app = app
        self.tags = tags
        self.tracer = tracer
        self._target = None
        # keep the configuration attribute internal because the
        # public API to access it is not the Pin class
        self._config = _config or {}
        # [Backward compatibility]: service argument updates the `Pin` config
        self._config['service_name'] = service
        self._initialized = True

    @property
    def service(self):
        """Backward compatibility: accessing to `pin.service` returns the underlying
        configuration value.
        """
        return self._config['service_name']

    def __setattr__(self, name, value):
        if getattr(self, '_initialized', False) and name != '_target':
            raise AttributeError("can't mutate a pin, use override() or clone() instead")
        super(Pin, self).__setattr__(name, value)

    def __repr__(self):
        return 'Pin(service=%s, app=%s, tags=%s, tracer=%s)' % (
            self.service, self.app, self.tags, self.tracer)

    @staticmethod
    def _find(*objs):
        """
        Return the first :class:`ddtrace.pin.Pin` found on any of the provided objects or `None` if none were found


            >>> pin = Pin._find(wrapper, instance, conn, app)

        :param objs: The objects to search for a :class:`ddtrace.pin.Pin` on
        :type objs: List of objects
        :rtype: :class:`ddtrace.pin.Pin`, None
        :returns: The first found :class:`ddtrace.pin.Pin` or `None` is none was found
        """
        for obj in objs:
            pin = Pin.get_from(obj)
            if pin:
                return pin
        return None

    @staticmethod
    def get_from(obj):
        """Return the pin associated with the given object. If a pin is attached to
        `obj` but the instance is not the owner of the pin, a new pin is cloned and
        attached. This ensures that a pin inherited from a class is a copy for the new
        instance, avoiding that a specific instance overrides other pins values.

            >>> pin = Pin.get_from(conn)

        :param obj: The object to look for a :class:`ddtrace.pin.Pin` on
        :type obj: object
        :rtype: :class:`ddtrace.pin.Pin`, None
        :returns: :class:`ddtrace.pin.Pin` associated with the object, or None if none was found
        """
        if hasattr(obj, '__getddpin__'):
            return obj.__getddpin__()

        pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME
        pin = getattr(obj, pin_name, None)
        # detect if the PIN has been inherited from a class
        if pin is not None and pin._target != id(obj):
            pin = pin.clone()
            pin.onto(obj)
        return pin

    @classmethod
    @debtcollector.removals.removed_kwarg("app_type")
    def override(cls, obj, service=None, app=None, app_type=None, tags=None, tracer=None):
        """Override an object with the given attributes.

        That's the recommended way to customize an already instrumented client, without
        losing existing attributes.

            >>> conn = sqlite.connect('/tmp/user.db')
            >>> # Override a pin for a specific connection
            >>> Pin.override(conn, service='user-db')
        """
        if not obj:
            return

        pin = cls.get_from(obj)
        if not pin:
            pin = Pin(service)

        pin.clone(
            service=service,
            app=app,
            tags=tags,
            tracer=tracer,
        ).onto(obj)

    def enabled(self):
        """Return true if this pin's tracer is enabled. """
        return bool(self.tracer) and self.tracer.enabled

    def onto(self, obj, send=True):
        """Patch this pin onto the given object. If send is true, it will also
        queue the metadata to be sent to the server.
        """
        # Actually patch it on the object.
        try:
            if hasattr(obj, '__setddpin__'):
                return obj.__setddpin__(self)

            pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME

            # set the target reference; any get_from, clones and retarget the new PIN
            self._target = id(obj)
            return setattr(obj, pin_name, self)
        except AttributeError:
            log.debug("can't pin onto object. skipping", exc_info=True)

    def remove_from(self, obj):
        # Remove pin from the object.
        try:
            pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME

            pin = Pin.get_from(obj)
            if pin is not None:
                delattr(obj, pin_name)
        except AttributeError:
            log.debug("can't remove pin from object. skipping", exc_info=True)

    @debtcollector.removals.removed_kwarg("app_type")
    def clone(self, service=None, app=None, app_type=None, tags=None, tracer=None):
        """Return a clone of the pin with the given attributes replaced."""
        # do a shallow copy of Pin dicts
        if not tags and self.tags:
            tags = self.tags.copy()

        # we use a copy instead of a deepcopy because we expect configurations
        # to have only a root level dictionary without nested objects. Using
        # deepcopy introduces a big overhead:
        #
        # copy: 0.00654911994934082
        # deepcopy: 0.2787208557128906
        config = self._config.copy()

        return Pin(
            service=service or self.service,
            app=app or self.app,
            tags=tags,
            tracer=tracer or self.tracer,  # do not clone the Tracer
            _config=config,
        )
