import datetime

from elixir import *
from sqlalchemy import func

### Utility function(s)

def indent_comments(comments):
    """Given a list of comment objects, returns the same comments (and changes
    them in-place) with an `indent` property set on each comment.  This
    indicates how deeply nested each comment is, relative to the first comment.
    The first comment's indent level is 0.

    The comments must be a complete subtree, ordered by their `left` property.

    This function will also cache the `parent` property for each comment.
    """

    last_comment = None
    indent = 0 
    right_ancestry = []
    for comment in comments:
        # If this comment is a child of the last, bump the nesting level
        if last_comment and comment.left < last_comment.right:
            indent = indent + 1 
            # Remember current ancestory relevant to the root
            right_ancestry.append(last_comment)

        # On the other hand, for every nesting level this comment may have just
        # broken out of, back out a level
        for i in xrange(len(right_ancestry) - 1, -1, -1):
            if comment.left > right_ancestry[i].right:
                indent = indent - 1 
                right_ancestry.pop(i)

        # Cache parent comment
        if len(right_ancestry):
            comment._parent = right_ancestry[-1]

        comment.indent = indent

        last_comment = comment

    return comments


### Usual database classes

class Discussion(Entity):
    """Represents a collection of comments attached to some other object."""
    count = Field(Integer)
    comments = OneToMany('Comment', order_by='left')

class Comment(Entity):
    time = Field(DateTime, default=datetime.datetime.now)
    text = Field(Unicode(65536))

    # Comments are a tree, and are stored as a nested set, because:
    # - It's easy to get a subtree with a single query.
    # - It's easy to get a comment's depth.
    # - It's easy to sort comments without recursion.
    # The only real disadvantage is that adding a comment requires a quick
    # update of all the following comments (in post-order), but that's rare
    # enough that it shouldn't be a problem.
    left = Field(Integer, index=True)
    right = Field(Integer)

    discussion = ManyToOne('Discussion')
    user = ManyToOne('User')

    def __init__(self, parent=None, **kwargs):
        """Constructor override to set left/right correctly on a new comment.
        """
        super(Comment, self).__init__(**kwargs)

        # Keep the comment count updated
        self.discussion.count += 1

        if parent:
            # Parent comment given.
            parent_right = parent.right
            # Shift every left/right ahead by two to make room for the new
            # comment's left/right
            Comment.query.filter(Comment.discussion == self.discussion) \
                         .filter(Comment.left >= parent_right) \
                         .update({ 'left': Comment.left + 2 })
            Comment.query.filter(Comment.discussion == self.discussion) \
                         .filter(Comment.right >= parent_right) \
                         .update({ 'right': Comment.right + 2 })

            # Then stick the new comment in the right place
            self.left = parent_right
            self.right = parent_right + 1

        else:
            query = session.query(func.max(Comment.right)) \
                           .filter(Comment.discussion == self.discussion)
            (max_right,) = query.one()

            if not max_right:
                # No comments yet.  Use 1 and 2
                self.left = 1
                self.right = 2
            else:
                self.left = max_right + 1
                self.right = max_right + 2

    @property
    def parent(self):
        """Returns this comment's parent.  This is cached, hence its being a
        property and not a method.
        """
        if not hasattr(self, '_parent'):
            self._parent = Comment.query \
                .filter(Comment.discussion_id == self.discussion_id) \
                .filter(Comment.left < self.left) \
                .filter(Comment.right > self.right) \
                .order_by(Comment.left.desc()) \
                .first()
        return self._parent
