""":mod:`wand.resource` --- Global resource management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

There is the global resource to manage in MagickWand API. This module
implements automatic global resource management through reference counting.

"""
import logging
import contextlib
import ctypes
from . import exceptions
from .api import library


__all__ = ('genesis', 'terminus', 'increment_refcount', 'decrement_refcount',
           'Resource', 'DestroyedResourceError')


def genesis():
    """Instantiates the MagickWand API.

    .. warning::
    
       Don't call this function directly. Use :func:`increment_refcount()` and
       :func:`decrement_refcount()` functions instead.

    """
    library.MagickWandGenesis()


def terminus():
    """Cleans up the MagickWand API.

    .. warning::
    
       Don't call this function directly. Use :func:`increment_refcount()` and
       :func:`decrement_refcount()` functions instead.

    """
    library.MagickWandTerminus()


#: (:class:`numbers.Integral`) The internal integer value that maintains
#: the number of referenced objects.
#:
#: .. warning::
#:
#:    Don't touch this global variable. Use :func:`increment_refcount()` and
#:    :func:`decrement_refcount()` functions instead.
#:
reference_count = 0


def increment_refcount():
    """Increments the :data:`reference_count` and instantiates the MagickWand
    API if it is the first use.

    """
    global reference_count
    if reference_count:
        reference_count += 1
    else:
        genesis()
        reference_count = 1
    logger = logging.getLogger(__name__ + '.increment_refcount')
    logger.debug(str(reference_count))


def decrement_refcount():
    """Decrements the :data:`reference_count` and cleans up the MagickWand
    API if it will be no more used.

    """
    global reference_count
    if not reference_count:
        raise RuntimeError('wand.resource.reference_count is already zero')
    reference_count -= 1
    logger = logging.getLogger(__name__ + '.decrement_refcount')
    logger.debug(str(reference_count))
    if not reference_count:
        terminus()


class Resource(object):
    """Abstract base class for MagickWand object that requires resource
    management. Its all subclasses manage the resource semiautomatically
    and support :keyword:`with` statement as well::

        with Resource() as resource:
            # use the resource...
            pass

    It doesn't implement constructor by itself, so subclasses should
    implement it. Every constructor should assign the pointer of its
    resource data into :attr:`resource` attribute **inside of :keyword`with`
    :meth:`allocate()` context.** For example::

        class Pizza(Resource):
            '''My pizza yummy.'''

            def __init__(self):
                with self.allocate():
                    self.resource = library.NewPizza()

    """

    #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` predicate function
    #: that returns whether the given pointer (that contains a resource data
    #: usuaully) is a valid resource.
    #:
    #: .. note::
    #:
    #:    It is an abstract attribute that has to be implemented
    #:    in the subclass.
    c_is_resource = NotImplemented

    #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that destroys
    #: the :attr:`resource`.
    #:
    #: .. note::
    #:
    #:    It is an abstract attribute that has to be implemented
    #:    in the subclass.
    c_destroy_resource = NotImplemented

    #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that gets
    #: an exception from the :attr:`resource`.
    #:
    #: .. note::
    #:
    #:    It is an abstract attribute that has to be implemented
    #:    in the subclass.
    c_get_exception = NotImplemented

    #: (:class:`ctypes.CFUNCTYPE`) The :mod:`ctypes` function that clears
    #: an exception of the :attr:`resource`.
    #:
    #: .. note::
    #:
    #:    It is an abstract attribute that has to be implemented
    #:    in the subclass.
    c_clear_exception = NotImplemented

    @property
    def resource(self):
        """Internal pointer to the resource instance. It may raise
        :exc:`DestroyedResourceError` when the resource has destroyed already.

        """
        if getattr(self, 'c_resource', None) is None:
            raise DestroyedResourceError(repr(self) + ' is destroyed already')
        return self.c_resource

    @resource.setter
    def resource(self, resource):
        if self.c_is_resource(resource):
            self.c_resource = resource
        else:
            raise TypeError(repr(wand) + ' is an invalid resource')
        increment_refcount()

    @resource.deleter
    def resource(self):
        self.c_destroy_resource(self.resource)
        self.c_resource = None

    @contextlib.contextmanager
    def allocate(self):
        """Allocates the memory for the resource explicitly. Its subclasses
        should assign the created resource into :attr:`resource` attribute
        inside of this context. For example::

            with resource.allocate():
                resource.resource = library.NewResource()

        """
        increment_refcount()
        try:
            yield self
        except:
            decrement_refcount()
            raise

    def destroy(self):
        """Cleans up the resource explicitly. If you use the resource in
        :keyword:`with` statement, it was called implicitly so have not to
        call it.

        """
        del self.resource
        decrement_refcount()

    def get_exception(self):
        """Gets a current exception instance.

        :returns: a current exception. it can be ``None`` as well if any
                  errors aren't occurred
        :rtype: :class:`wand.exceptions.WandException`

        """
        severity = ctypes.c_int()
        desc = self.c_get_exception(self.resource, ctypes.byref(severity))
        if severity.value == 0:
            return
        self.c_clear_exception(self.wand)
        exc_cls = exceptions.TYPE_MAP[severity.value]
        return exc_cls(ctypes.string_at(desc))

    def raise_exception(self, stacklevel=1):
        """Raises an exception or warning if it has occurred."""
        e = self.get_exception()
        if isinstance(e, Warning):
            warnings.warn(e, stacklevel=stacklevel + 1)
        elif isinstance(e, Exception):
            raise e

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.destroy()

    def __del__(self):
        try:
            self.destroy()
        except DestroyedResourceError:
            pass


class DestroyedResourceError(ReferenceError, AttributeError):
    """An error that rises when some code tries access to an already
    destroyed resource.

    """

