#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.db import models

class SortingManager(models.Manager):
    """An extended manager that extends the default manager
    by `get_sorted()` which returns the objects in the database
    sorted by their position"""

    def get_sorted(self):
        """Returns a sorted list"""
        return self.model.objects.all().order_by('position')

class InjectingModelBase(models.base.ModelBase):
    """This helper metaclass is used by PositionalSortMixIn.
    This metaclass injects two attributes:
     - objects: this replaces the default manager
     - position: this field holds the information about the position
                 of the item in the list"""

    def __new__(cls, name, bases, attrs):
        """Metaclass constructor calling Django and then modifying
        the resulting class"""
        # get the class which was alreeady built by Django
        child = models.base.ModelBase.__new__(cls, name, bases, attrs)

        # try to add some more or less neccessary fields
        try:
            # add the sorting manager
            child.add_to_class('objects', SortingManager())
            # add the IntegerField
            child.add_to_class('position', models.IntegerField(editable=False))

        except AttributeError:
            # add_to_class was not yet added to the class.
            # No problem, this is called twice by Django, add_to-class
            # will appear later
            pass
        # we're done - output the class, it's ready for use
        return child

class PositionalSortMixIn(object):
    """This mix-in class implements a user defined order in the database.
    To apply this mix-in you need to inherit from it before you inherit
    from `models.Model`. It adds a custom manager and a IntegerField
    called `position` to your model. be careful, it overwrites any existing
    fiels that you might have defined."""
    # get a metaclass which injects the neccessary fields
    __metaclass__ = InjectingModelBase

    def get_object_at_offset(self, offset):
        """Get the object whose position is `offset` positions away
        from it's own."""
        # get the class in which this was mixed in
        model_class = self.__class__
        try:
            return model_class.objects.get(position=self.position+offset)
        except model_class.DoesNotExist:
            # no such model? no deal, just return None
            return None

    # some shortcuts, convenience methods
    get_next = lambda self: self.get_object_at_offset(1)
    get_previous = lambda self: self.get_object_at_offset(-1)

    def move_down(self):
        """Moves element one position down"""
        # get the element after this one
        one_after = self.get_next()

        if not one_after:
            # already the last element
            return

        # flip the positions
        one_after.position, self.position = self.position, one_after.position
        for obj in (one_after, self):
            obj.save()

    def move_up(self):
        """Moves element one position up"""
        # get the element before this one
        one_before = self.get_previous()

        if not one_before:
            # already the first
            return

        # flip the positions
        one_before.position, self.position = self.position, one_before.position
        for obj in (one_before, self):
            obj.save()

    def save(self):
        """Saves the model to the database.
        It populates the `position` field of the model automatically if there
        is no such field set. In this case, the element will be appended at
        the end of the list."""
        model_class = self.__class__
        # is there a position saved?
        if not self.position:
            # no, it was ampty. Find one
            try:
                # get the last object
                last = model_class.objects.all().order_by('-position')[0]
                # the position of the last element
                self.position = last.position +1
            except IndexError:
                # IndexError happened: the quary did not return any objects
                # so this has to be the first
                self.position = 0

        # save the now properly set-up model
        return models.Model.save(self)

    def delete(self):
        """Deletes the item from the list."""
        model_class = self.__class__
        # get all objects with a position greater than this objects position
        objects_after = model_class.objects.filter(position__gt=self.position)
        # iterate through all objects which were found
        for element in objects_after:
            # decrease the position in the list (means: move forward)
            element.position -= 1
            element.save()
        # now we can safely remove this model instance
        return models.Model.delete(self)