"""Django/PostgreSQL implementation of the Meteor DDP service."""
from __future__ import unicode_literals
import os.path
import sys
from gevent.local import local
from dddp import alea

__version__ = '0.17.2'

default_app_config = 'dddp.apps.DjangoDDPConfig'

ADDED = 'added'
CHANGED = 'changed'
REMOVED = 'removed'

_GREEN = {}


def greenify():
    """Patch threading and psycopg2 modules for green threads."""
    # don't greenify twice.
    if _GREEN:
        return
    _GREEN[True] = True

    from gevent.monkey import patch_all, saved
    if ('threading' in sys.modules) and ('threading' not in saved):
        raise Exception('threading module loaded before patching!')
    patch_all()

    from psycogreen.gevent import patch_psycopg
    patch_psycopg()

    try:
        # Use psycopg2 by default
        import psycopg2
        del psycopg2
    except ImportError:
        # Fallback to psycopg2cffi if required (eg: pypy)
        from psycopg2cffi import compat
        compat.register()


class AlreadyRegistered(Exception):

    """Raised when registering over the top of an existing registration."""

    pass


class ThreadLocal(local):

    """Thread local storage for greenlet state."""

    _init_done = False

    def __init__(self):
        """Create new thread storage instance."""
        if self._init_done:
            raise SystemError('__init__ called too many times')
        self._init_done = True

    def __getattr__(self, name):
        """Create missing attributes using default factories."""
        try:
            factory = THREAD_LOCAL_FACTORIES[name]
        except KeyError:
            raise AttributeError(name)
        return self.get(name, factory)

    def get(self, name, factory, *factory_args, **factory_kwargs):
        """Get attribute, creating if required using specified factory."""
        update_thread_local = getattr(factory, 'update_thread_local', True)
        if (not update_thread_local) or (name not in self.__dict__):
            obj = factory(*factory_args, **factory_kwargs)
            if update_thread_local:
                setattr(self, name, obj)
            return obj
        return getattr(self, name)

    @staticmethod
    def get_factory(name):
        """Get factory for given name."""
        return THREAD_LOCAL_FACTORIES[name]


class RandomStreams(object):

    """Namespaced PRNG generator."""

    def __init__(self):
        """Initialize with random PRNG state."""
        self._streams = {}
        self._seed = THREAD_LOCAL.alea_random.hex_string(20)

    def get_seed(self):
        """Retrieve current PRNG seed."""
        return self._seed

    def set_seed(self, val):
        """Set current PRNG seed."""
        self._streams = {}
        self._seed = val
    random_seed = property(get_seed, set_seed)

    def __getitem__(self, key):
        """Get namespaced PRNG stream."""
        if key not in self._streams:
            return self._streams.setdefault(key, alea.Alea(self._seed, key))
        return self._streams[key]


def serializer_factory():
    """Make a new DDP serializer."""
    from django.core.serializers import get_serializer
    return get_serializer('python')()


THREAD_LOCAL_FACTORIES = {
    'alea_random': alea.Alea,
    'random_streams': RandomStreams,
    'serializer': serializer_factory,
}
THREAD_LOCAL = ThreadLocal()
METEOR_ID_CHARS = u'23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz'


def meteor_random_id(name=None, length=17):
    """Generate a new ID, optionally using namespace of given `name`."""
    if name is None:
        stream = THREAD_LOCAL.alea_random
    else:
        stream = THREAD_LOCAL.random_streams[name]
    return stream.random_string(length, METEOR_ID_CHARS)


def autodiscover():
    """Import all `ddp` submodules from `settings.INSTALLED_APPS`."""
    from django.utils.module_loading import autodiscover_modules
    from dddp.api import API
    autodiscover_modules('ddp', register_to=API)
    return API
