from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.utils.encoding import smart_unicode
from django.utils.functional import curry
from django.db.models.query import Q, EmptyQuerySet
from pycon.core import bind_prop_to_class
from itertools import chain, groupby
from datetime import date, datetime
from models import *
import difflib

ACTIONS = { ADDITION: u'new',
            CHANGE:   u'edit',
            DELETION: u'remove' }

@bind_prop_to_class(LogEntry)
def action(entry):
    return ACTIONS[entry.action_flag]

@bind_prop_to_class(Proposal)
def change_history(obj):
    ct = ContentType.objects.get_for_model(type(obj))
    return LogEntry.objects.filter(content_type=ct,
                                   object_id=smart_unicode(obj.id),
                                   action_flag=CHANGE).order_by(
                                        'action_time').extra(
                    select={'model': '"django_content_type"."model"'},
                    tables = ['django_content_type'],
                    where=['"django_content_type"."id" = '
                           '"django_admin_log"."content_type_id"'])

def vote_change_histories():
    ct_review     = ContentType.objects.get_for_model(Review)

    return LogEntry.objects.filter(content_type=ct_review
                ).order_by('action_time').extra(
                    select={'model': 'django_content_type.model'},
                    tables = ['django_content_type'],
                    where=['"django_content_type"."id" = '
                           '"django_admin_log"."content_type_id"'])

def all_change_histories(accending=False):
    ct_proposal   = ContentType.objects.get_for_model(Proposal)
    ct_attachment = ContentType.objects.get_for_model(AttachedFile)
    ct_comment    = ContentType.objects.get_for_model(Comment)
    ct_review     = ContentType.objects.get_for_model(Review)

    return LogEntry.objects.filter(content_type__in=[
        ct_proposal, ct_attachment, ct_comment, ct_review]
                ).order_by(
        'action_time' if accending else '-action_time').extra(
                    select={'model': 'django_content_type.model'},
                    tables = ['django_content_type'],
                    where=['"django_content_type"."id" = '
                           '"django_admin_log"."content_type_id"'])

@bind_prop_to_class(Proposal)
def full_change_history(proposal, accending=True):
    ct_proposal   = ContentType.objects.get_for_model(Proposal)
    ct_attachment = ContentType.objects.get_for_model(AttachedFile)
    ct_comment    = ContentType.objects.get_for_model(Comment)
    ct_review     = ContentType.objects.get_for_model(Review)
    comments      = [ smart_unicode(instance['id'])
                     for instance in proposal.comment_set.values('id') ]
    attachments   = [ smart_unicode(instance['id'])
                     for instance in proposal.attachedfile_set.values('id') ]
    reviews       = [ smart_unicode(instance['id'])
                     for instance in proposal.review_set.values('id') ]
    Qprop         = (Q(content_type=ct_proposal) &
                     Q(object_id=smart_unicode(proposal.id)))
    Qattachments  = Q(content_type=ct_attachment) & Q(object_id__in=attachments)
    Qcomments     = Q(content_type=ct_comment)    & Q(object_id__in=comments)
    Qreviews      = Q(content_type=ct_review)     & Q(object_id__in=reviews)
    Qhistory      = Qprop | Qattachments | Qcomments | Qreviews
    return LogEntry.objects.filter(Qhistory).order_by(
        'action_time' if accending else '-action_time').extra(
                    select={'model': 'django_content_type.model'},
                    tables = ['django_content_type'],
                    where=['"django_content_type"."id" = '
                           '"django_admin_log"."content_type_id"'])

@bind_prop_to_class(User)
def myproposals_full_change_history(user, accending=False):
    user_id = smart_unicode(user.id)
    ## Q objects and reverse relations are broken
    proposals = list(set(smart_unicode(instance['id'])
        for instance in
            chain(Proposal.objects.filter(comment__commenter=user_id
                                          ).distinct().values('id') ,
                  Proposal.objects.filter(review__reviewer=user_id
                                          ).distinct().values('id') ,
                  Proposal.objects.filter(attachedfile__submitter=user_id
                                          ).distinct().values('id') ,
                  Proposal.objects.filter(Q(submitter=user_id) |
                                          Q(coauthors=user_id)
                                          ).distinct().values('id'))))
    if not proposals: return EmptyQuerySet()
    ct_proposal   = ContentType.objects.get_for_model(Proposal)
    ct_attachment = ContentType.objects.get_for_model(AttachedFile)
    ct_comment    = ContentType.objects.get_for_model(Comment)
    ct_review     = ContentType.objects.get_for_model(Review)

    comments      = [ smart_unicode(instance['id'])
                      for instance in Comment.objects.filter(
                        proposal__in=proposals).values('id') ]
    attachments   = [ smart_unicode(instance['id'])
                     for instance in AttachedFile.objects.filter(
                        proposal__in=proposals).values('id') ]
    reviews       = [ smart_unicode(instance['id'])
                     for instance in Review.objects.filter(
                        proposal__in=proposals).values('id') ]

    Qhistory      = Q(content_type=ct_proposal)   & Q(object_id__in=proposals)
    if attachments:
        Qattach   = Q(content_type=ct_attachment) & Q(object_id__in=attachments)
        Qhistory  = Qhistory | Qattach
    if comments:
        Qcomment  = Q(content_type=ct_comment)    & Q(object_id__in=comments)
        Qhistory  = Qhistory | Qcomment
    if reviews:
        Qrev      = Q(content_type=ct_review)     & Q(object_id__in=reviews)
        Qhistory  = Qhistory | Qrev
    return LogEntry.objects.filter(Qhistory).order_by(
        'action_time' if accending else '-action_time').extra(
                    select={'model': 'django_content_type.model'},
                    tables = ['django_content_type'],
                    where=['"django_content_type"."id" = '
                           '"django_admin_log"."content_type_id"'])

def log_action(user, obj, action, message='', repr=None):
    ctype = ContentType.objects.get_for_model(type(obj))
    LogEntry.objects.log_action(user.id, ctype.id, obj.id,
                            repr if repr is not None else smart_unicode(obj),
                            action, message)

CHANGED_FROM_TO = (u'* Changed %(field)s:\n\n    **from:** %(old)s\n'
                   u'\n    **to:** %(new)s\n\n')
CHANGED_FROM_TO_SIMPLE = u'* Changed %(field)s: %(old)s => %(new)s\n'

def _(fmt, **kwdargs):
    return fmt % kwdargs

def change_diff(field, old, new):
    if not old.endswith('\n'): old += '\n'
    if not new.endswith('\n'): new += '\n'
    diff = '    '.join(difflib.ndiff(old.splitlines(True),
                                     new.splitlines(True)))
    return '* Changed ' + field + '::\n\n    ' + diff + '\n\n'

def log_proposal(user, prop, old=None):
    if old is None:
        message = u'* Title: ' + unicode(prop) + u'\n'
        message += u'* Summary::\n\n    '
        message += u'\n    '.join(prop.summary.split('\n')) + u'\n'
        log_action(user, prop, ADDITION, message)
        return
    message = u''
    if old.title != prop['title']:
        message += _(CHANGED_FROM_TO, field=u'title', old=old.title,
                     new=prop['title'])
    if old.duration != prop['duration']:
        message += _(CHANGED_FROM_TO_SIMPLE, field=u'duration',
                     old=unicode(old.duration), new=unicode(prop['duration']))
    if old.level != prop['level']:
        message += _(CHANGED_FROM_TO_SIMPLE, field =u'level',
                     old=PROPOSAL_LEVEL_CHOICES[old.level],
                     new=PROPOSAL_LEVEL_CHOICES[prop['level']])
    if old.categories != prop['categories']:
        message += _(CHANGED_FROM_TO, field = u'categories',
                     old=old.categories, new=prop['categories'])
    if old.summary != prop['summary']:
        message += change_diff('summary', old.summary, prop['summary'])
    if old.description != prop['description']:
        message += change_diff('description', old.description,
                               prop['description'])
    return curry(log_action, user, old, CHANGE,
                 message if message else '* Unknown Change\n')

def log_coauthor_edit(user, proposal, added=None, removed=None):
    message = ''
    if added: message += '* Added authors: ' + ', '.join(added) + '\n'
    if removed: message += '* Removed authors: ' + ', '.join(removed) + '\n'
    if not message: message = '* No apparent change\n'
    return log_action(user, proposal, CHANGE, message)

def log_attach_file(user, proposal, attachment):
    return log_action(user, attachment, ADDITION,
               '* Added attachment (%s) to proposal #%d\n' % (
                attachment.get_file_short_name(), proposal.id))

def log_comment(user, proposal, comment):
    message = u'* Added comment to proposal #%d::\n\n    ' % proposal.id
    message += u'\n    '.join(comment.comment.split('\n')) + u'\n'
    return log_action(user, comment, ADDITION, message)

def log_review(user, proposal, vote, old_vote=None, assign_voter=False):
    if old_vote is None:
        message = ''
        if assign_voter:
            message += (
                u'* Added self as reviewer to proposal #%d\n' % proposal.id)
        message += u'* Added review to proposal #%d\n' % proposal.id
        message += u'* Vote: ' + vote.score  + u'\n'
        message += u'* Comment::\n\n    '
        message += u'\n    '.join(vote.comment.split('\n')) + u'\n'
        log_action(user, vote, ADDITION, message)
        return
    message = ''
    if old_vote.score != vote['score']:
        message += _(CHANGED_FROM_TO_SIMPLE, field= u'score',
                     old=old_vote.score, new=vote['score'])
    if old_vote.comment != vote['comment']:
        message += change_diff('comment', old_vote.comment, vote['comment'])
    return curry(log_action, user, old_vote, CHANGE,
                 message if message else '* Unknown Change\n')

GENCM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'
def gc_extended(num, min=0, max=4095, floor=0, ceil=4095, missing=None):
    if num == missing: return '__'
    num = floor if num<min else (ceil if num>max else num)
    scaled = ((num-min)*4095)/(max-min)
    return GENCM[scaled/64]+GENCM[scaled%64]

def google_chart_url(after=None):
    ct_proposal   = ContentType.objects.get_for_model(Proposal)
    ct_attachment = ContentType.objects.get_for_model(AttachedFile)
    ct_comment    = ContentType.objects.get_for_model(Comment)
    ct_review     = ContentType.objects.get_for_model(Review)

    ## we will treat the attachment add as a proposal edit
    ## order from bottom to top
    order =['New+Proposal',
            'Edit+Proposal',
            'New+Review',
            'Edit+Review',
            'Comment']
    revorder = list(reversed(order))

    mapping = { ADDITION: { ct_proposal.id:   'New+Proposal',
                            ct_attachment.id: 'Edit+Proposal',
                            ct_comment.id:    'Comment',
                            ct_review.id:     'New+Review' },
                CHANGE:   { ct_proposal.id:   'Edit+Proposal',
                            ct_attachment.id: 'Edit+Proposal',
                            ct_comment.id:    'Edit+Proposal',
                            ct_review.id:     'Edit+Review' },
    }

    ## we will have 5 types of data
    hist = LogEntry.objects.filter(
        content_type__in=[ct_proposal.id, ct_attachment.id,
                          ct_comment.id, ct_review.id],
        action_flag__in=[ADDITION, CHANGE]).order_by('action_time')
    max = hist.count()
    before = None
    if after:
        hist = hist.filter(action_time__gt=after)

    num = hist.count()
    first, last = hist[0].action_time.date(), hist[num-1].action_time.date()
    delta = hist[0].action_time.date()
    ndays = (last - first).days + 1

    line_counts = { 'New+Proposal': [0]*ndays,
                    'Edit+Proposal': [0]*ndays,
                    'New+Review': [0]*ndays,
                    'Edit+Review': [0]*ndays,
                    'Comment': [0]*ndays, }
    if after is None:
        current = { 'New+Proposal': 0,
                    'Edit+Proposal': 0,
                    'New+Review': 0,
                    'Edit+Review': 0,
                    'Comment': 0, }
    else:

        current = { 'New+Proposal':
            LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=ADDITION,
                                    content_type=ct_proposal.id).count(),
                    'Edit+Proposal':
            (LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=ADDITION,
                                    content_type=ct_attachment.id).count() +
             LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=CHANGE,
                                    content_type__in=[ct_proposal.id,
                                                      ct_attachment.id,
                                                      ct_comment.id
                                                      ]).count()),
                    'New+Review':
            LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=ADDITION,
                                    content_type=ct_review.id).count(),
                    'Edit+Review':
            LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=CHANGE,
                                    content_type=ct_review.id).count(),
                    'Comment':
            LogEntry.objects.filter(action_time__lte=after,
                                    action_flag=ADDITION,
                                    content_type=ct_comment.id).count(), }
    total = { 'New+Proposal': 0,
              'Edit+Proposal': 0,
              'New+Review': 0,
              'Edit+Review': 0,
              'Comment': 0, }

    sentinal = []
    byday = [sentinal]*ndays
    for day, hists in groupby(hist,
                              lambda x: ((x.action_time.date()) - delta).days):
        assert(byday[day] is sentinal)
        byday[day] = list(hists)

    for day, hists in enumerate(byday):
        for hist in hists:
            current[mapping[hist.action_flag][hist.content_type_id]] += 1
        local_tot = 0
        for key in order:
            local_tot += current[key]
            total[key] += local_tot
            assert(line_counts[key][day] == 0)
            line_counts[key][day] = total[key]
            current[key] = 0

    colors = { 'New+Proposal': '9F0251',
               'Edit+Proposal': 'EC799A',
               'New+Review': '495E88',
               'Edit+Review': 'A0AEC1',
               'Comment': 'EDBD3E', }

    scale = ((max/100)+1)*25
    max = scale*4
    yaxisl = '|'.join(str(i) for i in xrange(0, scale*5, scale))
    data = 'chd=e:' + ",".join( "".join(google_encode(count, max)
                                        for count in line_counts[key])
                               for key in revorder) + ','
    data += 'AA' * ndays

    title  = 'chtt=PyCon+2008+Talk+Proposal+Change+History' # title
    chart  = 'cht=lc' # basic line chart, one set of data points per day.
    size   = 'chs=600x300' # dimensions
    legend = 'chdl=' + '|'.join(revorder) # legend
    bcolor = 'chm=' + '|'.join("b,%s,%d,%d,0" % (colors[key], ind, ind+1)
                               for ind, key in enumerate(revorder))
    lcolor = 'chco=' + ','.join(colors[key] for key in revorder)
    axis   = 'chxt=x,y'
    axisl  = 'chxl=0:|' + first.isoformat() + '|' + last.isoformat()
    axisl += '|1:|' + yaxisl

    urld = '&'.join([title, chart, size, legend, lcolor, bcolor,
                     axis, axisl, data])

    return 'http://chart.apis.google.com/chart?' + urld
