
from django.db import models
from django.db.models.signals import post_save, pre_save, class_prepared


class DenormFieldBase(object):
    pass


def DenormField(model, field, *args, **kwds):
    
    other_field = model._meta.get_field(field)
    base_class = other_field.__class__
    
    class DenormField(DenormFieldBase, base_class):
        
        """
        A field which will replicate the contents of a field on another model.
        """
        
        def __init__(self, model, field, foreign_key=None, *args, **kwargs):
            # Store what we're referring to
            self.foreign_key = foreign_key
            self.other_model, self.other_field = model, field
            # Connect save signal to nab save events and write ourselves
            post_save.connect(self.other_saved, sender=self.other_model)
            # Sub-init
            kwargs.update({
                "blank": self.other_field.blank,
                "null": self.other_field.null,
                "max_length": self.other_field.max_length,
            })
            base_class.__init__(self, *args, **kwargs)
        
        def contribute_to_class(self, cls, attname, *args, **kwds):
            # Save our parent class so we can query it to get instances to save to
            self.parent_model = cls
            class_prepared.connect(self.setup, sender=self.parent_model)
            # Subcall
            models.Field.contribute_to_class(self, cls, attname, *args, **kwds)
        
        def setup(self, **kwds):
            # Work out the ForeignKey on this object that we need
            if self.foreign_key:
                if isinstance(self.foreign_key, (str, unicode)):
                    self.foreign_key = foreign_key
                else:
                    self.foreign_key = foreign_key.attname
            else:
                # Find the fkey
                fkeys = filter(lambda x: isinstance(x, models.ForeignKey) and x.rel.to == self.other_model, self.parent_model._meta.fields)
                if not fkeys:
                    raise ValueError("%s has no ForeignKeys to %s; cannot auto-denormalise." % (self.parent_model, self.other_model))
                if len(fkeys) > 1:
                    raise ValueError("%s has more than one ForeignKey to %s; please specify foreign_key= on the DenormField." % (self.parent_model, self.other_model))
                self.foreign_key = fkeys[0].attname
            if self.foreign_key.endswith("_id"):
                self.foreign_key = self.foreign_key[:-3]
            # Listen out for creates on our own model
            pre_save.connect(self.self_saved, sender=self.parent_model)
        
        def other_saved(self, instance, created, **kwargs):
            # Work out what instance of our own model is related
            if not hasattr(instance, "_denorm_instance"):
                try:
                    instance._denorm_instance = self.parent_model.objects.get(**{self.foreign_key: instance.id})
                    instance._denorm_instance.saves = 0
                except instance.DoesNotExist:
                    instance._denorm_instance = None
            # If there is an instance, update it with the new value
            if instance._denorm_instance:
                setattr(instance._denorm_instance, self.attname, getattr(instance, self.other_field.attname))
                # Up the count of who's saved on the instance
                instance._denorm_instance.saves += 1
                # Save if we're the last one
                if instance._denorm_instance.saves == len([x for x in self.parent_model._meta.fields if isinstance(x, DenormFieldBase)]):
                    instance._denorm_instance.save()
        
        def self_saved(self, instance, **kwargs):
            if not instance.id: # Only on initial creation
                # Get the other instance
                other_instance = getattr(instance, self.foreign_key)
                # Save its value into us
                setattr(instance, self.attname, getattr(other_instance, self.other_field.attname))
    
    return DenormField(model, other_field, *args, **kwds)
