from __future__ import unicode_literals

import six
from django.apps import apps
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save

from ..auth import channel_session, channel_session_user
from ..channel import Group

CREATE = 'create'
UPDATE = 'update'
DELETE = 'delete'


class BindingMetaclass(type):
    """
    Metaclass that tracks instantiations of its type.
    """

    register_immediately = False
    binding_classes = []

    def __new__(cls, name, bases, body):
        klass = type.__new__(cls, name, bases, body)
        if bases != (object, ):
            cls.binding_classes.append(klass)
            if cls.register_immediately:
                klass.register()
        return klass

    @classmethod
    def register_all(cls):
        for binding_class in cls.binding_classes:
            binding_class.register()
        cls.register_immediately = True


@six.add_metaclass(BindingMetaclass)
class Binding(object):
    """
    Represents a two-way data binding from channels/groups to a Django model.
    Outgoing binding sends model events to zero or more groups.
    Incoming binding takes messages and maybe applies the action based on perms.

    To implement outbound, implement:
     - group_names, which returns a list of group names to send to
     - serialize, which returns message contents from an instance + action

    To implement inbound, implement:
     - deserialize, which returns pk, data and action from message contents
     - has_permission, which says if the user can do the action on an instance
     - create, which takes the data and makes a model instance
     - update, which takes data and a model instance and applies one to the other

    Outbound will work once you implement the functions; inbound requires you
    to place one or more bindings inside a protocol-specific Demultiplexer
    and tie that in as a consumer.
    """

    # Model to serialize

    model = None

    # Only model fields that are listed in fields should be send by default
    # if you want to really send all fields, use fields = ['__all__']

    fields = None
    exclude = None

    # Decorators
    channel_session_user = True
    channel_session = False

    # the kwargs the triggering signal (e.g. post_save) was emitted with
    signal_kwargs = None

    @classmethod
    def register(cls):
        """
        Resolves models.
        """
        # Connect signals
        for model in cls.get_registered_models():
            pre_save.connect(cls.pre_save_receiver, sender=model)
            post_save.connect(cls.post_save_receiver, sender=model)
            pre_delete.connect(cls.pre_delete_receiver, sender=model)
            post_delete.connect(cls.post_delete_receiver, sender=model)

    @classmethod
    def get_registered_models(cls):
        """
        Resolves the class model attribute if it's a string and returns it.
        """
        # If model is None directly on the class, assume it's abstract.
        if cls.model is None:
            if "model" in cls.__dict__:
                return []
            else:
                raise ValueError("You must set the model attribute on Binding %r!" % cls)
        # If neither fields nor exclude are not defined, raise an error
        if cls.fields is None and cls.exclude is None:
            raise ValueError("You must set the fields or exclude attribute on Binding %r!" % cls)
        # Optionally resolve model strings
        if isinstance(cls.model, six.string_types):
            cls.model = apps.get_model(cls.model)
        cls.model_label = "%s.%s" % (
            cls.model._meta.app_label.lower(),
            cls.model._meta.object_name.lower(),
        )
        return [cls.model]

    # Outbound binding

    @classmethod
    def encode(cls, stream, payload):
        """
        Encodes stream + payload for outbound sending.
        """
        raise NotImplementedError()

    @classmethod
    def pre_save_receiver(cls, instance, **kwargs):
        creating = instance._state.adding
        cls.pre_change_receiver(instance, CREATE if creating else UPDATE)

    @classmethod
    def post_save_receiver(cls, instance, created, **kwargs):
        cls.post_change_receiver(instance, CREATE if created else UPDATE, **kwargs)

    @classmethod
    def pre_delete_receiver(cls, instance, **kwargs):
        cls.pre_change_receiver(instance, DELETE)

    @classmethod
    def post_delete_receiver(cls, instance, **kwargs):
        cls.post_change_receiver(instance, DELETE, **kwargs)

    @classmethod
    def pre_change_receiver(cls, instance, action):
        """
        Entry point for triggering the binding from save signals.
        """
        if action == CREATE:
            group_names = set()
        else:
            group_names = set(cls.group_names(instance))

        if not hasattr(instance, '_binding_group_names'):
            instance._binding_group_names = {}
        instance._binding_group_names[cls] = group_names

    @classmethod
    def post_change_receiver(cls, instance, action, **kwargs):
        """
        Triggers the binding to possibly send to its group.
        """
        old_group_names = instance._binding_group_names[cls]
        if action == DELETE:
            new_group_names = set()
        else:
            new_group_names = set(cls.group_names(instance))

        # if post delete, new_group_names should be []
        self = cls()
        self.instance = instance

        # Django DDP had used the ordering of DELETE, UPDATE then CREATE for good reasons.
        self.send_messages(instance, old_group_names - new_group_names, DELETE, **kwargs)
        self.send_messages(instance, old_group_names & new_group_names, UPDATE, **kwargs)
        self.send_messages(instance, new_group_names - old_group_names, CREATE, **kwargs)

    def send_messages(self, instance, group_names, action, **kwargs):
        """
        Serializes the instance and sends it to all provided group names.
        """
        if not group_names:
            return  # no need to serialize, bail.
        self.signal_kwargs = kwargs
        payload = self.serialize(instance, action)
        if payload == {}:
            return  # nothing to send, bail.

        assert self.stream is not None
        message = self.encode(self.stream, payload)
        for group_name in group_names:
            group = Group(group_name)
            group.send(message)

    @classmethod
    def group_names(cls, instance):
        """
        Returns the iterable of group names to send the object to based on the
        instance and action performed on it.
        """
        raise NotImplementedError()

    def serialize(self, instance, action):
        """
        Should return a serialized version of the instance to send over the
        wire (e.g. {"pk": 12, "value": 42, "string": "some string"})
        Kwargs are passed from the models save and delete methods.
        """
        raise NotImplementedError()

    # Inbound binding

    @classmethod
    def trigger_inbound(cls, message, **kwargs):
        """
        Triggers the binding to see if it will do something.
        Also acts as a consumer.
        """
        # Late import as it touches models
        from django.contrib.auth.models import AnonymousUser
        self = cls()
        self.message = message
        self.kwargs = kwargs
        # Deserialize message
        self.action, self.pk, self.data = self.deserialize(self.message)
        self.user = getattr(self.message, "user", AnonymousUser())
        # Run incoming action
        self.run_action(self.action, self.pk, self.data)

    @classmethod
    def get_handler(cls):
        """
        Adds decorators to trigger_inbound.
        """
        handler = cls.trigger_inbound
        if cls.channel_session_user:
            return channel_session_user(handler)
        elif cls.channel_session:
            return channel_session(handler)
        else:
            return handler

    @classmethod
    def consumer(cls, message, **kwargs):
        handler = cls.get_handler()
        handler(message, **kwargs)

    def deserialize(self, message):
        """
        Returns action, pk, data decoded from the message. pk should be None
        if action is create; data should be None if action is delete.
        """
        raise NotImplementedError()

    def has_permission(self, user, action, pk):
        """
        Return True if the user can do action to the pk, False if not.
        User may be AnonymousUser if no auth hooked up/they're not logged in.
        Action is one of "create", "delete", "update".
        """
        raise NotImplementedError()

    def run_action(self, action, pk, data):
        """
        Performs the requested action. This version dispatches to named
        functions by default for update/create, and handles delete itself.
        """
        # Check to see if we're allowed
        if self.has_permission(self.user, action, pk):
            if action == "create":
                self.create(data)
            elif action == "update":
                self.update(pk, data)
            elif action == "delete":
                self.delete(pk)
            else:
                raise ValueError("Bad action %r" % action)

    def create(self, data):
        """
        Creates a new instance of the model with the data.
        """
        raise NotImplementedError()

    def update(self, pk, data):
        """
        Updates the model with the data.
        """
        raise NotImplementedError()

    def delete(self, pk):
        """
        Deletes the model instance.
        """
        self.model.objects.filter(pk=pk).delete()
