#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Weak references to things with intids. These serve the same purpose as general
persistent weak references from :mod:`persistent.wref`, but are specific
to things with intids, and do not keep the object alive or accessible once
the object is removed from the intid catalog
(whereas weak refs do until such time as the database is GC'd).

.. $Id$
"""

from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

import warnings
import functools

from zc.intid import IIntIds

from zope import component
from zope import interface

from nti.externalization.integer_strings import to_external_string

from nti.ntiids.ntiids import TYPE_MISSING
from nti.ntiids.ntiids import make_ntiid

from nti.wref.interfaces import ICachingWeakRef
from nti.wref.interfaces import IWeakRefToMissing

logger = __import__('logging').getLogger(__name__)


class _AbstractWeakRef(object):
    """
    A weak reference to a content object (generally, anything
    with an intid). Call this object to return either the
    object, or if the object has gone away, ``None``.

    Subclasses need to support three properties: ``_entity_id``,
    ``_entity_oid``, and ``_v_entity_cache``. The first is the intid.
    Because intids can be reused, we also include a backup, the
    object's OID (if it has one) in the second. This isn't foolproof,
    but should to pretty good. The third is used for caching.
    Subclasses will need to decide how to implement ``__getstate__``
    and ``__setstate__``. If subclasses need a ``__dict__``, they will have
    to declare that in ``__slots__.``
    """

    __slots__ = ('_entity_id', '_entity_oid', '_v_entity_cache')

    def __init__(self, content_object):
        self._entity_id = component.getUtility(IIntIds).getId(content_object)
        # _v_entity_cache is a volatile attribute. It's either None, meaning we have
        # no idea, the resolved object, or False
        self._v_entity_cache = content_object
        self._entity_oid = getattr(content_object, '_p_oid', None)

    def _cached(self, allow_cached):
        if allow_cached and self._v_entity_cache is not None:
            # Notice that we must explicitly check for _v_entity_cache being
            # not False, because we use False to represent a failed lookup. (But
            # we cannot use just a simple bool comparison because that fails for
            # something like an empty container)
            return self._v_entity_cache if self._v_entity_cache is not False else None

        try:
            result = component.getUtility(IIntIds).getObject(self._entity_id)
        except KeyError:
            result = None

        if self._entity_oid is not None:
            result_oid = getattr(result, '_p_oid', None)
            if result_oid is None or result_oid != self._entity_oid:
                result = None

        if allow_cached:  # only perturb the state if we are allowed to
            if result is not None:
                self._v_entity_cache = result
            else:
                self._v_entity_cache = False

        return result

    def __call__(self, allow_cached=True):
        return self._cached(allow_cached)

    def __eq__(self, other):
        if self is other:
            return True
        # pylint: disable=protected-access
        try:
            if self._entity_id == other._entity_id:
                return self._entity_oid == other._entity_oid \
                    or self._entity_oid is None
            return False
        except AttributeError:  # pragma: no cover
            return NotImplemented

    def __ne__(self, other):
        if self is other:
            return False
        eq = self.__eq__(other)
        if eq is NotImplemented:
            return eq
        return not eq

    def __hash__(self):
        return hash((self._entity_id, self._entity_oid))

    def __repr__(self):
        return "<%s.%s %s>" % (self.__class__.__module__,
                               self.__class__.__name__,
                               self._entity_id)

    def make_missing_ntiid(self):
        eid = self._entity_id
        # This intid is probably no longer used, but we have no guarantee
        # of that. We do some trivial manipulation on it to make it less
        # obvious what it is, and less likely to come into the system when
        # we don't want it to
        eid = to_external_string(eid)
        # base64 might be nice, but that doesn't play well with ntiids
        return make_ntiid(nttype=TYPE_MISSING, specific=eid)


@interface.implementer(IWeakRefToMissing, ICachingWeakRef)
class WeakRef(_AbstractWeakRef):
    """
    A weak reference to a content object (generally, anything
    with an intid). Call this object to return either the
    object, or if the object has gone away, ``None``.

    Note that this is not a persistent object. It is not mutable (and
    it's also very tiny), and it has value semantics, so there's very
    little need to be persistent. This means it's suitable for use in
    OOSet objects and OOBTree objects as keys (if subclassed to provide
    total ordering!)
    """

    __slots__ = ()

    def __getstate__(self):
        return self._entity_id, self._entity_oid

    def __setstate__(self, state):
        self._entity_id, self._entity_oid = state
        self._v_entity_cache = None

    # We are not orderable. Hopefully there is no data like this in the real
    # world, but we did have tests relying on it. Try to catch it here,
    # doing what Python 2 would have done itself (Python 3 would raise errors).

    _unorderable_message = (
        "WeakRef is not totally orderable. Storing this in "
        "BTrees is a bad idea. Seeing this warning come from anything other than a "
        "test module is an error."
    )

    def __lt__(self, other):
        warnings.warn(self._unorderable_message, stacklevel=2)
        return id(self) < id(other)

    def __gt__(self, other):
        warnings.warn(self._unorderable_message, stacklevel=2)
        return id(self) > id(other)


@functools.total_ordering
class ArbitraryOrderableWeakRef(WeakRef):
    """
    A subclass of :class:`WeakRef` that is orderable in a completely arbitrary
    way (based simply on intids).
    """

    __slots__ = ()

    def __lt__(self, other):
        # pylint: disable=protected-access
        try:
            return (self._entity_id, self._entity_oid) < (other._entity_id, other._entity_oid)
        except AttributeError:  # pragma: no cover
            return NotImplemented

    def __gt__(self, other):
        # pylint: disable=protected-access
        try:
            return (self._entity_id, self._entity_oid) > (other._entity_id, other._entity_oid)
        except AttributeError:  # pragma: no cover
            return NotImplemented


class NoCachingArbitraryOrderableWeakRef(ArbitraryOrderableWeakRef):
    """
    Does not allow caching.
    """
    __slots__ = ()
    def __call__(self, unused_allow_cached=False):
        return ArbitraryOrderableWeakRef.__call__(self, allow_cached=False)
