# coding: utf-8
"""Python bindings for 0MQ."""

# Copyright (C) PyZMQ Developers
# Distributed under the terms of the Modified BSD License.

import atexit
import weakref

from zmq.backend import Context as ContextBase
from . import constants
from .attrsettr import AttributeSetter
from .constants import ENOTSUP, ctx_opt_names
from .socket import Socket
from zmq.error import ZMQError

from zmq.utils.interop import cast_int_addr


class Context(ContextBase, AttributeSetter):
    """Create a zmq Context
    
    A zmq Context creates sockets via its ``ctx.socket`` method.
    """
    sockopts = None
    _instance = None
    _shadow = False
    _exiting = False
    
    def __init__(self, io_threads=1, **kwargs):
        super(Context, self).__init__(io_threads=io_threads, **kwargs)
        if kwargs.get('shadow', False):
            self._shadow = True
        else:
            self._shadow = False
        self.sockopts = {}
        
        self._exiting = False
        if not self._shadow:
            ctx_ref = weakref.ref(self)
            def _notify_atexit():
                ctx = ctx_ref()
                if ctx is not None:
                    ctx._exiting = True
            atexit.register(_notify_atexit)
    
    def __del__(self):
        """deleting a Context should terminate it, without trying non-threadsafe destroy"""
        if not self._shadow and not self._exiting:
            self.term()
    
    def __enter__(self):
        return self
    
    def __exit__(self, *args, **kwargs):
        self.term()
    
    @classmethod
    def shadow(cls, address):
        """Shadow an existing libzmq context
        
        address is the integer address of the libzmq context
        or an FFI pointer to it.
        
        .. versionadded:: 14.1
        """
        address = cast_int_addr(address)
        return cls(shadow=address)
    
    @classmethod
    def shadow_pyczmq(cls, ctx):
        """Shadow an existing pyczmq context
        
        ctx is the FFI `zctx_t *` pointer
        
        .. versionadded:: 14.1
        """
        from pyczmq import zctx
        
        underlying = zctx.underlying(ctx)
        address = cast_int_addr(underlying)
        return cls(shadow=address)

    # static method copied from tornado IOLoop.instance
    @classmethod
    def instance(cls, io_threads=1):
        """Returns a global Context instance.

        Most single-threaded applications have a single, global Context.
        Use this method instead of passing around Context instances
        throughout your code.

        A common pattern for classes that depend on Contexts is to use
        a default argument to enable programs with multiple Contexts
        but not require the argument for simpler applications:

            class MyClass(object):
                def __init__(self, context=None):
                    self.context = context or Context.instance()
        """
        if cls._instance is None or cls._instance.closed:
            cls._instance = cls(io_threads=io_threads)
        return cls._instance
    
    #-------------------------------------------------------------------------
    # Hooks for ctxopt completion
    #-------------------------------------------------------------------------
    
    def __dir__(self):
        keys = dir(self.__class__)

        for collection in (
            ctx_opt_names,
        ):
            keys.extend(collection)
        return keys

    #-------------------------------------------------------------------------
    # Creating Sockets
    #-------------------------------------------------------------------------

    @property
    def _socket_class(self):
        return Socket
    
    def socket(self, socket_type):
        """Create a Socket associated with this Context.

        Parameters
        ----------
        socket_type : int
            The socket type, which can be any of the 0MQ socket types:
            REQ, REP, PUB, SUB, PAIR, DEALER, ROUTER, PULL, PUSH, etc.
        """
        if self.closed:
            raise ZMQError(ENOTSUP)
        s = self._socket_class(self, socket_type)
        for opt, value in self.sockopts.items():
            try:
                s.setsockopt(opt, value)
            except ZMQError:
                # ignore ZMQErrors, which are likely for socket options
                # that do not apply to a particular socket type, e.g.
                # SUBSCRIBE for non-SUB sockets.
                pass
        return s
    
    def setsockopt(self, opt, value):
        """set default socket options for new sockets created by this Context
        
        .. versionadded:: 13.0
        """
        self.sockopts[opt] = value
    
    def getsockopt(self, opt):
        """get default socket options for new sockets created by this Context
        
        .. versionadded:: 13.0
        """
        return self.sockopts[opt]
    
    def _set_attr_opt(self, name, opt, value):
        """set default sockopts as attributes"""
        if name in constants.ctx_opt_names:
            return self.set(opt, value)
        else:
            self.sockopts[opt] = value
    
    def _get_attr_opt(self, name, opt):
        """get default sockopts as attributes"""
        if name in constants.ctx_opt_names:
            return self.get(opt)
        else:
            if opt not in self.sockopts:
                raise AttributeError(name)
            else:
                return self.sockopts[opt]
    
    def __delattr__(self, key):
        """delete default sockopts as attributes"""
        key = key.upper()
        try:
            opt = getattr(constants, key)
        except AttributeError:
            raise AttributeError("no such socket option: %s" % key)
        else:
            if opt not in self.sockopts:
                raise AttributeError(key)
            else:
                del self.sockopts[opt]

__all__ = ['Context']
