from collections import OrderedDict

from .relmanager import RelManager
from .fields import Field, ForeignKey, OneToOneField
from . import signals

# Architecture
#
# This is complicated enough to warrant some explanation.
#
# A Model class has a Meta instance.
#
# The Fields are instances in the Meta instance.
#
# For a given Model, all Field instances are shared since there is only
# one Meta instance for that Model class. This means that a field can't know
# it's exact Model instance, only it's parent Model class.


# from: http://stackoverflow.com/questions/12006267/how-do-django-models-work
# from: lib/python2.7/site-packages/django/db/models/base.py
#
# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class MetaModel(type):
    """
    *do not use this class directly*

    *code reviews of this class are very welcome*

    base class for :class:`alkali.model.Model`.

    this complicated metaclass is required to convert a stylized class
    into a useful concrete one. it converts :class:`alkali.fields.Field`
    variables into their base types as attributes on the instantiated
    class.

    **Meta**: adds a ``Meta`` class if not already defined in ``Model``
    derived class

    **objects**: :class:`alkali.manager.Manager`
    """

    # this called once per Model _definition_
    # __new__ is the method called before __init__
    # meta_class is _this_ class, aka: MetaModel
    # this makes a new MetaModel instance
    def __new__(meta_class, name, bases, attrs):
        #print "__new__ cls:",type(meta_class),meta_class,name
        super_new = super(MetaModel, meta_class).__new__

        # Also ensure initialization is only performed for subclasses of Model
        # (excluding Model class itself). This keeps all of Models attrs intact.
        if not any( map( lambda b: isinstance(b, MetaModel), bases ) ):
            new_class = super_new(meta_class, name, bases, attrs)
            return new_class

        # new_class is an instance of 'name' (aka Model) whose type is MetaModel
        # print "new_class", type(new_class), new_class
        # new_class <class 'alkali.metamodel.MetaModel'> <class 'redb.metamodel.MyModel'>
        new_class = super_new(meta_class, name, bases, {})
        new_class._add_meta( attrs )
        new_class._add_fields()
        new_class._add_manager()
        new_class._add_relmanagers()
        new_class._add_exceptions()

        # put the rest of the attributes (methods and properties)
        # defined in the Model derived class into the "new" Model
        for name, attr in attrs.items():
            setattr(new_class, name, attr)

        signals.model_creation.send(meta_class, model=new_class)

        return new_class

    def _add_manager( new_class ):
        from .manager import Manager
        setattr( new_class, 'objects', Manager(new_class) )

    def _add_relmanagers( new_class ):
        """
        if this class has foreign keys then we need to add the
        reverse lookup into the *other* model
        """
        for name, field in new_class.Meta.fields.items():
            if not isinstance(field, ForeignKey):
                continue

            # note the name=name in the lambda, this is vital to capture
            # the current value of name and not the last of the loop
            # more info: http://stackoverflow.com/questions/2295290
            rel_manager = property(
                    lambda fm_instance, name=name: RelManager(fm_instance, new_class, name)
                    )
            set_name = "{}_set".format(new_class.__name__).lower()
            setattr( field.foreign_model, set_name, rel_manager )

            signals.pre_delete.connect(
                    new_class.objects.cb_delete_foreign,
                    sender=field.foreign_model)

            if isinstance(field, OneToOneField):
                signals.post_save.connect(
                    new_class.objects.cb_create_foreign,
                    sender=field.foreign_model)

    def _add_exceptions( new_class ):
        from .model import ObjectDoesNotExist

        # dynamically create a new class types
        DoesNotExist            = type('DoesNotExist', (ObjectDoesNotExist,), {} )
        EmptyPrimaryKey         = type('EmptyPrimaryKey', (Exception,), {} )
        MultipleObjectsReturned = type('MultipleObjectsReturned', (Exception,), {} )

        setattr( new_class, 'ObjectDoesNotExist', ObjectDoesNotExist )
        setattr( new_class, 'DoesNotExist', DoesNotExist )
        setattr( new_class, 'EmptyPrimaryKey', EmptyPrimaryKey )
        setattr( new_class, 'MultipleObjectsReturned', MultipleObjectsReturned )

    def _add_meta( new_class, attrs ):

        def _get_fields( attrs ):
            return [(k, v) for k, v in attrs.items() if isinstance(v, Field)]

        def _get_field_order(attrs):
            """
            returns field names in the order they were defined in the class
            """
            fields = _get_fields(attrs)
            fields.sort(key=lambda e: e[1]._order)
            return [k for k, _ in fields]

        class Object():
            pass

        # Meta is an instance in Model class
        # all following properties on the Meta class, not instance
        meta = attrs.pop( 'Meta', Object )
        setattr( new_class, 'Meta', meta() )

        if not hasattr(meta, 'filename'):
            meta.filename = None

        if not hasattr(meta, 'storage'):
            meta.storage = None

        if not hasattr(meta, 'ordering'):
            meta.ordering = _get_field_order(attrs)

        meta.field_filter = lambda self, field_type: \
                [n for n, f in self.fields.items() if isinstance(f, field_type)]

        # don't let user miss a field if they've defined Meta.ordering
        assert len(meta.ordering) == len(_get_fields(attrs)), \
            "missing/extra fields defined in Meta.ordering"

        # put the fields into the meta class
        # meta.ordering contains field names, attrs contains Field types
        meta.fields = OrderedDict()
        for field in meta.ordering:
            meta.fields[field] = attrs.pop(field)
            delattr( meta.fields[field], '_order' )

        # make sure 'pk' isn't a field name, etc
        for d in ['pk']:
            assert d not in meta.fields

        # you can set a property on a class but it will only be called on an instance
        # I'd prefer this to be a read-only property but I guess that can't happen
        #
        # note: don't use a dict comprehension because interim dict will have keys
        # inserted in random order
        meta.pk_fields = OrderedDict(
                [(name, field) for name, field in meta.fields.items() if field.primary_key]
                )

        # monkey patch stupid fucking iterators
        meta.pk_fields._keys = meta.pk_fields.keys
        meta.pk_fields.keys = lambda: list(meta.pk_fields._keys())
        meta.pk_fields._values = meta.pk_fields.values
        meta.pk_fields.values = lambda: list(meta.pk_fields._values())

        meta.fields._keys = meta.fields.keys
        meta.fields.keys = lambda: list(meta.fields._keys())
        meta.fields._values = meta.fields.values
        meta.fields.values = lambda: list(meta.fields._values())

        if len(meta.fields):
            assert len(meta.pk_fields) > 0, "no primary_key defined in fields"

    def _add_fields( new_class ):
        """
        put the Field reference into new_class
        """
        meta = new_class.Meta

        # add properties to field
        for name, field in meta.fields.items():
            field._name = name
            fget = lambda self: getattr(self, '_name')
            setattr( field.__class__, 'name', property(fget=fget) )

            field._model = new_class
            fget = lambda self: getattr(self, '_model')
            setattr( field.__class__, 'model', property(fget=fget) )

            fget = lambda self: self.model.Meta
            setattr( field.__class__, 'meta', property(fget=fget) )

        # put fields in model
        for name, field in meta.fields.items():
            # make magic property model.fieldname_field that returns Field object
            fget = lambda self, name=name: self.Meta.fields[name]
            setattr( new_class, name + '__field', property(fget=fget) )

            # set the Field descriptor object on the model class
            # which makes it accessable on the model instance
            #
            # the field can't be a property to Meta.fields[name]
            # because then the descriptor-ness is lost and a normal
            # getattr is called on the model instance
            setattr( new_class, name, field )

    # creates a new instance of derived model, this is called each
    # time a Model instance is created
    def __call__(cls, *args, **kw):
        obj = cls.__new__(cls, *args)

        if 'pk' in kw:
            assert len(cls.Meta.pk_fields) == 1, "can't currently set compound primary key via kwargs"

            field_name = cls.Meta.pk_fields.keys()[0]
            assert field_name not in kw, "can't pass in 'pk' and actual pk field name"

            value = kw.pop('pk')
            kw[field_name] = value

        # put field values (int,str,etc) into model instance
        for name, field in cls.Meta.fields.items():
            # THINK: this somewhat duplicates Field.__set__ code
            value = kw.pop(name, field.default_value)
            value = field.cast(value)

            # store the actual value in the model's __dict__, used by Field.__get__
            obj.__dict__[name] = value

        obj._dirty = False
        obj.__init__(*args, **kw)

        return obj
