import datetime
import random
 
from cachecow.decorators import cached_function
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models
from django.db import transaction
from django.db.models import Avg
from django.forms import ModelForm
from django.forms.util import ErrorList
from model_utils.managers import manager_from
 
from apps.manabi_redis.models import redis
from books.models import Textbook
from constants import DEFAULT_EASE_FACTOR
from constants import GRADE_NONE, GRADE_HARD, GRADE_GOOD, GRADE_EASY
from flashcards.cachenamespaces import deck_review_stats_namespace
from flashcards.models.intervals import initial_interval
from itertools import chain
import cards
import usertagging
 
 
class _DeckManager(object):
    def of_user(self, user):
        return self.filter(owner=user, active=True)
 
    def shared_decks(self):
        return self.filter(shared=True, active=True)
 
    def synchronized_decks(self, user):
        return self.filter(owner=user, synchronized_with__isnull=False)
 
DeckManager = lambda: manager_from(_DeckManager)
 
 
class Deck(models.Model):
    #manager
    objects = DeckManager()
 
    name = models.CharField(max_length=100)
    description = models.TextField(max_length=2000, blank=True)
    owner = models.ForeignKey(User, db_index=True)
 
    textbook_source = models.ForeignKey(Textbook, null=True, blank=True)
 
    picture = models.FileField(upload_to='/deck_media/', null=True, blank=True) 
    #TODO upload to user directory, using .storage
 
    priority = models.IntegerField(default=0, blank=True)
 
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    modified_at = models.DateTimeField(auto_now=True, editable=False)
 
    # whether this is a publicly shared deck
    shared = models.BooleanField(default=False, blank=True)
    shared_at = models.DateTimeField(null=True, blank=True)
    # or if not, whether it's synchronized with a shared deck
    synchronized_with = models.ForeignKey('self',
            null=True, blank=True, related_name='subscriber_decks')
 
    # "active" is just a soft deletion flag. "suspended" is temporarily 
    # disabled.
    suspended = models.BooleanField(default=False, db_index=True) 
    active = models.BooleanField(default=True, blank=True, db_index=True)
 
    def __unicode__(self):
        return u'{0} ({1})'.format(self.name, self.owner)
 
    class Meta:
        app_label = 'flashcards'
        ordering = ('name',)
        #TODO unique_together = (('owner', 'name'), )
 
    def get_absolute_url(self):
        return reverse('deck_detail', kwargs={'deck_id': self.id})
 
    def delete(self, *args, **kwargs):
        # You shouldn't delete a shared deck - just set active=False
        self.subscriber_decks.clear()
        super(Deck, self).delete(*args, **kwargs)
 
    def fact_tags(self):
        '''
        Returns tags for all facts inside this deck.
        Includes tags on facts made on subscribed facts.
        '''
        #return usertagging.models.Tag.objects.usage_for_queryset(
            #self.facts())
        from facts import Fact
        deck_facts = Fact.objects.with_upstream(
            self.owner, deck=self)
        return usertagging.models.Tag.objects.usage_for_queryset(
            deck_facts)
 
    #def facts(self):
    #    '''Returns all Facts for this deck,
    #    including subscribed ones, not including subfacts.
    #    '''
    #    from fields import FieldContent
    #    if self.synchronized_with:
    #        updated_fields = FieldContent.objects.filter(fact__deck=self, fact__active=True, fact__synchronized_with__isnull=False) #fact__in=self.subscriber_facts.all())
    #        # 'other' here means non-updated, subscribed
    #        other_facts = Fact.objects.filter(parent_fact__isnull=True, id__in=updated_fields.values_list('fact', flat=True))
    #        other_fields = FieldContent.objects.filter(fact__deck=self.synchronized_with).exclude(fact__active=True, fact__in=other_facts.values_list('synchronized_with', flat=True))
    #        #active_subscribers = active_subscribers | other_fields
    #        return updated_fields | other_fields
    #    else:
    #        return FieldContent.objects.filter(fact__deck=self, fact__active=True)
 
    def field_contents(self):
        '''
        Returns all FieldContents for facts in this deck,
        preferring updated subscriber fields to subscribed ones,
        when the deck is synchronized.
        '''
        from fields import FieldContent
        if self.synchronized_with:
            updated_fields = FieldContent.objects.filter(
                    fact__deck=self, fact__active=True,
                    fact__synchronized_with__isnull=False) 
            #fact__in=self.subscriber_facts.all())
            # 'other' here means non-updated, subscribed
            other_facts = Fact.objects.filter(
                    parent_fact__isnull=True,
                    id__in=updated_fields.values_list('fact', flat=True))
            other_fields = FieldContent.objects.filter(
                    fact__deck=self.synchronized_with
                    ).exclude(fact__active=True,
                              fact__in=other_facts.values_list(
                                    'synchronized_with', flat=True))
            return updated_fields | other_fields
        else:
            return FieldContent.objects.filter(
                    fact__deck=self, fact__active=True)
 
    @property
    def has_subscribers(self):
        '''
        Returns whether there are subscribers to this deck, because
        it is shared, or it had been shared before.
        '''
        return self.subscriber_decks.filter(active=True).exists()
 
    @transaction.commit_on_success    
    def share(self):
        '''
        Shares this deck publicly.
        '''
        if self.synchronized_with:
            raise TypeError('Cannot share synchronized decks (decks which are already synchronized with shared decks).')
        self.shared = True
        self.shared_at = datetime.datetime.utcnow()
        self.save()
 
    @transaction.commit_on_success
    def unshare(self):
        '''
        Unshares this deck.
        '''
        if not self.shared:
            raise TypeError('This is not a shared deck, so it cannot be unshared.')
        self.shared = False
        self.save()
 
    def get_subscriber_deck_for_user(self, user):
        '''
        Returns the subscriber deck for `user` of this deck.
        If it doesn't exist, returns None.
        If multiple exist, even though this shouldn't happen,
        we just return the first one.
        '''
        subscriber_decks = self.subscriber_decks.filter(owner=user, active=True)
        if subscriber_decks.exists():
            return subscriber_decks[0]
 
    @transaction.commit_on_success    
    def subscribe(self, user):
        '''
        Subscribes to this shared deck for the given user.
        They will study this deck as their own, but will 
        still receive updates to content.
 
        Returns the newly created deck.
 
        If the user was already subscribed to this deck, 
        returns the existing deck.
        '''
        from facts import Fact
 
        # check if the user is already subscribed to this deck
        subscriber_deck = self.get_subscriber_deck_for_user(user)
        if subscriber_deck:
            return subscriber_deck
 
        if not self.shared:
            raise TypeError('This is not a shared deck - cannot subscribe to it.')
        if self.synchronized_with:
            raise TypeError('Cannot share a deck that is already synchronized to a shared deck.')
 
        #TODO dont allow multiple subscriptions to same deck by same user
 
        # copy the deck
        deck = Deck(
            synchronized_with=self,
            name=self.name,
            description=self.description,
            #TODO implement textbook=shared_deck.textbook, #picture too...
            priority=self.priority,
            textbook_source=self.textbook_source,
            owner_id=user.id)
        deck.save()
 
        # copy the tags
        deck.tags = usertagging.utils.edit_string_for_tags(self.tags)
 
        # copy the facts - just the first few as a buffer
        shared_fact_to_fact = {}
        #TODO dont hardcode value here #chain(self.fact_set.all(), Fact.objects.filter(parent_fact__deck=self)):
        for shared_fact in self.fact_set.filter(active=True, parent_fact__isnull=True).order_by('new_fact_ordinal')[:10]: 
            #FIXME get the child facts for this fact too
            #if shared_fact.parent_fact:
            #    #child fact
            #    fact = Fact(
            #        fact_type=shared_fact.fact_type,
            #        active=shared_fact.active) #TODO should it be here?
            #    fact.parent_fact = shared_fact_to_fact[shared_fact.parent_fact]
            #    fact.save()
            #else:
            #   #regular fact
            fact = Fact(
                deck=deck,
                fact_type_id=shared_fact.fact_type_id,
                synchronized_with=shared_fact,
                active=True, #shared_fact.active, #TODO should it be here?
                priority=shared_fact.priority,
                new_fact_ordinal=shared_fact.new_fact_ordinal,
                notes=shared_fact.notes)
            fact.save()
            shared_fact_to_fact[shared_fact] = fact
 
            # don't copy the field contents for this fact - we'll get them from 
            # the shared fact later
 
            # copy the cards
            for shared_card in shared_fact.card_set.filter(active=True):
                card = shared_card.copy(fact)
                card.save()
        #done!
        return deck
 
    def card_count(self):
        return cards.Card.objects.of_deck(self, with_upstream=True).available().count()
 
    #TODO kill - unused?
    #@property
    #def new_card_count(self):
    #    return Card.objects.approx_new_count(deck=self)
    #    #FIXME do for sync'd decks
    #    return cards.Card.objects.cards_new_count(
    #            self.owner, deck=self, active=True, suspended=False)
 
    #TODO kill - unused?
    #@property
    #def due_card_count(self):
    #    return cards.Card.objects.cards_due_count(
    #            self.owner, deck=self, active=True, suspended=False)
 
    @cached_function(namespace=deck_review_stats_namespace)
    def average_ease_factor(self):
        '''
        Includes suspended cards in the calcuation. Doesn't include inactive cards.
        '''
        ease_factors = redis.zrange('ease_factor:deck:{0}'.format(self.id),
                                    0, -1, withscores=True)
        cardinality = len(ease_factors)
        if cardinality:
            return sum(score for val,score in ease_factors) / cardinality
        return DEFAULT_EASE_FACTOR
 
    @transaction.commit_on_success    
    def delete_cascading(self):
        #FIXME if this is a shared/synced deck
        for fact in self.fact_set.all():
            for card in fact.card_set.all():
                card.delete()
            fact.delete()
        self.delete()
 
    def export_to_csv(self):
        '''
        Returns an HttpRespone object containing the binary CSV data.
        Decoupling this from HttpResponse is more trouble than it's worth.
        '''
        from django.http import HttpResponse
        import csv, StringIO
 
        class UnicodeWriter(object):
            '''
            http://djangosnippets.org/snippets/993/
            Usage example:
                fp = open('my-file.csv', 'wb')
                writer = UnicodeWriter(fp)
                writer.writerows([
                    [u'Bob', 22, 7],
                    [u'Sue', 28, 6],
                    [u'Ben', 31, 8],
                    # \xc3\x80 is LATIN CAPITAL LETTER A WITH MACRON
                    ['\xc4\x80dam'.decode('utf8'), 11, 4],
                ])
                fp.close()
            '''
            def __init__(self, f, dialect=csv.excel_tab, encoding="utf-16", **kwds):
                # Redirect output to a queue
                self.queue = StringIO.StringIO()
                self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
                self.stream = f
 
                # Force BOM
                if encoding=="utf-16":
                    import codecs
                    f.write(codecs.BOM_UTF16)
 
                self.encoding = encoding
 
            def writerow(self, row):
                # Modified from original: now using unicode(s) to deal with e.g. ints
                self.writer.writerow([unicode(s).encode("utf-8") for s in row])
                # Fetch UTF-8 output from the queue ...
                data = self.queue.getvalue()
                data = data.decode("utf-8")
                # ... and reencode it into the target encoding
                data = data.encode(self.encoding)
 
                # strip BOM
                if self.encoding == "utf-16":
                    data = data[2:]
 
                # write to the target stream
                self.stream.write(data)
                # empty queue
                self.queue.truncate(0)
 
            def writerows(self, rows):
                for row in rows:
                    self.writerow(row)
 
 
        # make a valid filename for the deck based on its alphanumeric characters
        import string
        filename = filter(
                lambda c: c in (string.ascii_letters + '0123456789'), self.name)
        if not filename:
            filename = 'manabi_deck'
        filename += '.csv'
 
        # Create the HttpResponse object with the appropriate CSV header.
        response = HttpResponse(mimetype='text/csv')
        response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
 
        writer = UnicodeWriter(response)
 
        from flashcards.models import FactType, Fact, FieldType
        fact_type = FactType.objects.get(id=1)
        field_types = FieldType.objects.filter(fact_type=fact_type).order_by('id')
        facts = Fact.objects.with_upstream(self.owner, deck=self)
        card_templates = fact_type.cardtemplate_set.all().order_by('id')
 
        header = [field.display_name for field in field_types] + \
                [template.name for template in card_templates]
        writer.writerow(header)
 
        for fact in facts:
            fields = list(field_content.content
                        for field_content
                        in fact.field_contents.order_by('field_type__id'))
 
            templates = []
            activated_card_templates = [e.template for e in fact.card_set.filter(active=True)]
            for card_template in fact_type.cardtemplate_set.all():
                if card_template  in activated_card_templates:
                    templates.append('on')
                else:
                    templates.append('off')
 
            writer.writerow(fields + templates)
 
        response['Content-Length'] = len(response.content)
 
        return response
 
usertagging.register(Deck)