"""mapper.py - defines mappers for domain objects, mapping operations"""

import zblog.database.tables as tables
import zblog.domain.user as user
from zblog.domain.blog import *
from sqlalchemy import *
import sqlalchemy.util as util

# User mapper.  Here, we redefine the names of some of the columns
# to different property names.  normally the table columns are all
# sucked in automatically.
user.User.mapper = mapper(user.User, tables.users, properties={
    'id':tables.users.c.user_id,
    'name':tables.users.c.user_name,
    'group':tables.users.c.groupname,
    'crypt_password':tables.users.c.password,
})

# blog mapper.  this contains a reference to the user mapper,
# and also installs a "backreference" on that relationship to handle it
# in both ways. this will also attach a 'blogs' property to the user mapper.
Blog.mapper = mapper(Blog, tables.blogs, properties={
    'id':tables.blogs.c.blog_id,
    'owner':relation(user.User, lazy=False, backref='blogs'),
}, is_primary=True)

# override the 'blogs' property on the user mapper to be a "private" relation,
# which means the blogs only exist as children of that user.  remove the blog
# from the user's list, it gets deleted; delete the user, the blogs get deleted.
user.User.mapper.add_property('blogs', relation(Blog.mapper, private=True, lazy=True, backref='owner')) 

# topic mapper.  map all topic columns to the Topic class.
Topic.mapper = mapper(Topic, tables.topics)
        
# TopicAssocation mapper.  This is an "association" object, which is similar to
# a many-to-many relationship except extra data is associated with each pair 
# of related data.  because the topic_xref table doesnt have a primary key,
# the "primary key" columns of a TopicAssociation are defined manually here.
TopicAssociation.mapper = mapper(TopicAssociation,tables.topic_xref, 
                primary_key=[tables.topic_xref.c.post_id, tables.topic_xref.c.topic_id], 
                properties={
                    'topic':relation(Topic.mapper, lazy=False),
                })

# Post mapper, these are posts within a blog.  
# since we want the count of comments for each post, create a select that will get the posts
# and count the comments in one query.
posts_with_ccount = select(
    [c for c in tables.posts.c if c.key != 'body'] + [
        func.count(tables.comments.c.comment_id).label('comment_count')
    ],
    from_obj = [
        outerjoin(tables.posts, tables.comments)
    ],
    group_by=[
        c for c in tables.posts.c
    ]
    ) .alias('postswcount')

# then create a Post mapper on that query.  
# we have the body as "deferred" so that it loads only when needed,
# the user as a Lazy load, since the lazy load will run only once per user and
# its usually only one user's posts is needed per page,
# the owning blog is a lazy load since its also probably loaded into the identity map
# already, and topics is an eager load since that query has to be done per post in any
# case.
Post.mapper = mapper(Post, posts_with_ccount, properties={
    'id':posts_with_ccount.c.post_id,
    'body':deferred(tables.posts.c.body),
    'user':relation(user.User, lazy=True, backref='posts'),
    'blog':relation(Blog, lazy=True, backref='posts'),
    'topics':relation(TopicAssociation, lazy=False, private=True, association=Topic)
}, is_primary=True, order_by=[desc(posts_with_ccount.c.datetime)])

# override 'posts' property on Blog to be private, so that posts get deleted when the blog does.
Blog.mapper.add_property('posts', relation(Post.mapper, private=True, lazy=True, backref='blog'))

# override 'posts' property on User to be private, so all user posts in all blogs get
# removed when the user does.
user.User.mapper.add_property('posts', relation(Post.mapper, private=True, lazy=True, backref='user'))

# comment mapper.  This mapper is handling a hierarchical relationship on itself, and contains
# a lazy reference both to its parent comment and its list of child comments.
Comment.mapper = mapper(Comment, tables.comments, properties={
    'id':tables.comments.c.comment_id,
    'post':relation(Post.mapper, lazy=True, backref='comments'),
    'user':relation(user.User.mapper, lazy=False, backref='comments'),
    'parent':relation(Comment, primaryjoin=tables.comments.c.parent_comment_id==tables.comments.c.comment_id, foreignkey=tables.comments.c.comment_id, lazy=True, uselist=False),
    'replies':relation(Comment,primaryjoin=tables.comments.c.parent_comment_id==tables.comments.c.comment_id, lazy=True, uselist=True, private=True),
}, is_primary=True)

# override the "post" and "user" backreference-generated properties to be lazy properties
Post.mapper.add_property('comments', relation(Comment.mapper, private=True, lazy=True, backref='post'))
user.User.mapper.add_property('comments', relation(Comment.mapper, private=True, lazy=True, backref='user'))

# we define one special find-by for the comments of a post, which is going to make its own "noload"
# mapper and organize the comments into their correct hierarchy in one pass. hierarchical
# data normally needs to be loaded by separate queries for each set of children, unless you
# use a proprietary extension like CONNECT BY.
def find_by_post(post):
    """returns a hierarchical collection of comments based on a given criterion.  
    uses a mapper that does not lazy load replies or parents, and instead
    organizes comments into a hierarchical tree when the result is produced.
    """
    mapper = Comment.mapper.options(noload('replies'), noload('parent'))
    comments = mapper.select_by(post_id=post.id)
    result = []
    d = {}
    for c in comments:
        d[c.id] = c
        if c.parent_comment_id is None:
            result.append(c)
            c.parent=None
        else:
            parent = d[c.parent_comment_id]
            parent.replies.append(c)
            c.parent = parent
    return result

Comment.find_by_post = staticmethod(find_by_post)

# define a bunch of convenience methods on the objectstore.

def start_session():
    """clears the objectstore, so that when a new user request is handled, all data will be
    loaded from the database completely, and anything left over from the previous session
    is removed.  Clearing the objectstore is a thread-local operation."""
    objectstore.clear()
   
# keep track of transaction token in a thread local.
# this is a compatibility hack since SQLAlchemy recently
# changed its begin/commit style to return this tranasctional token
# and the code is not keeping track of it, so we track it here
# within our own begin/commit
trans = util.ThreadLocal()

def begin():
    """begins a transaction with the objectstore."""
    trans.t = objectstore.begin()

def commit():
    """commits a transaction with the objectstore.  everything modified since the last
    begin() is updated in the database."""
    print "\n\n------------------------------\n\n"
    trans.t.commit()
    
def delete(*obj):
    """marks an object (or objects) to be deleted upon the next commit()."""
    objectstore.delete(*obj) 
