import sys
import logging
from datetime import datetime
from decimal import Decimal
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from djangotoolbox.fields import BlobField
from django.core.cache import cache
from google.appengine.api.images import Image, JPEG
from utils.utils import set_trace
from templatetags.gmp_tags import pretty_name

# Create your models here.

class GMPException(Exception):
    pass
class WidthTooSmallError(GMPException):
    pass
class HeightTooSmallError(GMPException):
    pass
    
class Organization(models.Model):
    """
    The Organization that donated the pictures
    """
    name = models.CharField(max_length=140, help_text='Name of organization')

    def __unicode__(self):
        return self.name


class UserProfile(models.Model):
    """
    Extra info about the user
    """
    is_moderator = models.BooleanField(default=False, help_text='True if user has moderation permissions')
    user = models.ForeignKey(User, help_text='matching user')



class Region(models.Model):
    """
    The Region in the Carmel from which the picture was taken.
    """
    name = models.CharField(max_length=140, help_text='Name region')
    marker = models.CharField(max_length=140, blank=True, help_text='coordinate stirng for marker')
    kml = models.TextField(blank=True, help_text='kml describing region')
    sort_order = models.IntegerField(default=0, help_text='sort order, default 0 will sort lexi')

    def __unicode__(self):
        return self.name

    def dump(self):
        return {
            'id': self.id,
            'name': self.name,
            'marker': self.marker if self.marker else None
        }
        
    def save(self, *args, **kwargs):
        cache.delete('REGION_LIST')
        cache.delete('REGION_ID_DICT')
        super(Region, self).save(*args, **kwargs)


class Photo(models.Model):
    """
    The photo meta-data
    """
    # By default all images have no moderation status (not-set)
    # Pending moderation - new images, will not be displyed except in moderation view
    # flagged moderation - images flagged by users, will not be displyed except in moderation view
    # moderated appropriate - images have been moderated. cannot be flagged by users.
    # moderated inappropriate - images have been moderated and marked as inappropriate. image will be removed from dataset (possibly by cron task)
    class Meta:
        ordering = ('-moderation_status', '-upload_date',)

    NUM_IN_MAP_CACHE = 3
    NUM_IN_CHRONO_CACHE = 6
    MIN_OK_MODERATION_STATUS = 10 
    INAPPROPRIATE_STATUS = 1
    CONFIRMED_STATUS = 10 

    MODERATION_STATUS = (
        (0, 'Not set'),
        (1, 'flagged inappropriate by user'),
        (2, 'moderated inappropriate by admin'),
        (3, 'pending moderation by operator'),
        (10, 'confirmed by user'),        
        (11, 'moderated appropriate by admin'),
    )

    SEASON_CHOICES = (
        (0, 'Unknown'),
        (1, 'Winter'),
        (2, 'Spring'),
        (3, 'Summer'),
        (4, 'Fall'),
    )

    owner = models.ForeignKey(User, null=True, blank=True, help_text='Image owner, null if unknown')
    owner_username = models.CharField(max_length=30, blank=True, help_text='denormalized username')
    title = models.CharField(max_length=48, blank=True, help_text='Photo title')
    description = models.CharField(max_length=140, blank=True, help_text='Photo description')
    photo = models.FileField(upload_to='uploads/%Y/%m/%d/%H/%M/%S/')
    photo_width = models.PositiveIntegerField(null=True, blank=True, help_text='Photo width') 
    photo_height = models.PositiveIntegerField(null=True, blank=True, help_text='Photo height') 
    thumbnail = BlobField(help_text='1:1 thumbnail')
    thumbnail_16_9 = BlobField(help_text='16:9 thumbnail')
    preview = BlobField()
    upload_date = models.DateTimeField(auto_now_add=True, help_text='Date when photo was uploaded to server')
    ranking_count = models.PositiveIntegerField(default=0, help_text='Number of people who submitted ranking')
    ranking_avg = models.DecimalField(default = 0, max_digits = 4, decimal_places = 3, help_text='Current ranking from 1 to 5')
    taken_date = models.DateTimeField(null=True, blank=True, help_text='Date when photo was taken')
    taken_season = models.PositiveIntegerField(default=0, choices=SEASON_CHOICES, help_text='Date when photo was taken')
    taken_year = models.PositiveIntegerField(null=True, blank=True, help_text='Year when photo was taken')
    organization = models.ForeignKey(Organization, null=True, blank=True, help_text='Organization which provided image. null if none')
    moderation_status = models.PositiveIntegerField(default=0, choices=MODERATION_STATUS, help_text='flag indicating current moderation status (if any)')
    moderation_comment = models.CharField(max_length=255, blank=True, help_text="reason why flagged / moderated")
    region = models.ForeignKey(Region, null=True, blank=True, help_text='Region image was taken. Null if unknown')
    coordinates = models.CharField(max_length=100, blank=True, help_text='Coordinates of photo. format TBD')
    in_map_cache = models.BooleanField(default=False, help_text='True if image is in cache set for map view')
    in_chrono_cache = models.BooleanField(default=False, help_text='True if image is in cache set for chrono view')
    is_qa = models.BooleanField(default=False, help_text='image is qa image')


    @property
    def photo_filename(self):
        return self.photo.name.rsplit('/', 1)[-1] if self.photo else None


    @property
    def photo_size(self):
        return self.photo.size if self.photo else None


    @property
    def photo_blobkey(self):
        return str(self.photo.file.blobstore_info.key()) if self.photo else None


    @property 
    def thumbnail_src(self):
        return reverse('download_thumbnail', args=[self.pk])


    def __unicode__(self):
        return '%s | %s | %s |%s' % (
            self.title if self.title else 'Untitled', 
            self.owner_username if self.owner_username else 'Anonymous', 
            self.photo_filename, self.photo_size)


    def save(self, *args, **kwargs):
        try:
            if not self.photo_width:
                self.photo_width = self.get_blob_width(self.photo_blobkey)
            if not self.photo_height:
                self.photo_height = self.get_blob_height(self.photo_blobkey)
            self.validate_dimensions()

        except SystemError:
            ex = sys.exc_info()[1]
            if ex.message == 'tile cannot extend outside image':
                raise WidthTooSmallError()
            else:
                raise

        if not self.thumbnail or not self.thumbnail_16_9 or 'redo_thumbnail' in kwargs:
            self.pop_param(kwargs, 'redo_thumbnail')
            self.thumbnail = self.generate_thumbnail(settings.THUMBNAIL_GEOMETRY_1_1)
            self.thumbnail_16_9 = self.generate_thumbnail(settings.THUMBNAIL_GEOMETRY_16_9)

        if not self.preview  or 'redo_preview' in kwargs:
            self.pop_param(kwargs, 'redo_preview')
            img = Image(blob_key = self.photo_blobkey)
            img.resize(width = settings.PREVIEW_GEOMETRY[0], height = settings.PREVIEW_GEOMETRY[1])
            self.preview = img.execute_transforms(output_encoding=JPEG)
             
        if self.owner and not self.owner_username:
            self.owner_username = pretty_name(self.owner)
            
        map_cache_modified = False
        chrono_cache_modified = False
        if 'no_cache_handling' not in kwargs or not kwargs['no_cache_handling']:
            map_cache_modified = self.update_map_cache()
            chrono_cache_modified = self.update_chrono_cache()
        if 'no_cache_handling' in kwargs:
            kwargs.pop('no_cache_handling')

        super(Photo, self).save(*args, **kwargs)
        self.clear_mem_caches(clear_map_cache = map_cache_modified,
                              clear_chrono_cache = chrono_cache_modified)
        

    def delete(self, *args, **kwargs):
        super(Photo, self).delete(*args, **kwargs)
        self.clear_mem_caches()


    def update_map_cache(self):
        """
        map cache maintains a set of 1 image per region
        the images in the set are the newest (upload_date) images per region, which are not flagged (aka moderated_ok)
        set is updated every time an image is saved as follows:
        
        - if current image is already in cache, and is flagged inappropriate --> remove from cache and find a new image        
        - if current image is not in cache, has a valid region, and is newer than image from same region in cache --> replace the image in the cache     
        - otherwise, do nothing
        """
        #TODO: Currently we have a bug if the region is changed or removed. A way to solve it to have in_map_cache be a Region (or None) instead of a
        #  boolean. A more interesting question would be how to handle the chrono cache (the type of in_chrono_cache should be a tuple? two fields?)
        # Anyway, when we handle this properly, the first if should not have the last "and".
        cache_modified = False
        if self.in_map_cache and self.moderation_status < self.MIN_OK_MODERATION_STATUS and self.region:
            self.in_map_cache = False  # Remove me, but don't save here (we're in the save flow)
            cache_modified = True
            cache_candidates = Photo.objects.filter(in_map_cache=False).filter(region=self.region)
            cache_candidates = self.filter_ok_moderation_statuses(cache_candidates)
            if cache_candidates.count() > 0:
                cache_candidates[0].in_map_cache = True
                cache_candidates[0].save(no_cache_handling=True)
            
        if not self.in_map_cache and self.moderation_status >= self.MIN_OK_MODERATION_STATUS and self.region:
            cached_images = Photo.objects.filter(in_map_cache=True).filter(region=self.region)
            cached_images = list(cached_images) #make the actual query, and fixate the count size
            if len(cached_images) >= self.NUM_IN_MAP_CACHE:
                logging.info('map cache is full, checking if we should add photo %d' % (self.pk or -1))
                if not self.upload_date or self.upload_date > cached_images[self.NUM_IN_MAP_CACHE - 1].upload_date:  # I'm newer
                    logging.info('adding photo %d to map cache because it is newer: %s' % (self.pk or -1, str(self.upload_date)))
                    self.in_map_cache = True
                    cache_modified = True
                    i = self.NUM_IN_MAP_CACHE - 1
                    while i < len(cached_images):
                        p = cached_images[i]
                        logging.info('removing photo %d from map cache because it is old: %s' % (p.pk, str(p.upload_date)))
                        p.in_map_cache = False
                        p.save(no_cache_handling=True)
                        i += 1
            else:
                cache_modified = True
                self.in_map_cache = True  # Add me, but don't save here
        return cache_modified
            
        
    def update_chrono_cache(self):
        """
        chrono cache maintains a set of 6 images per year, per season
        
        - if current image is already in cache, and is flagged inappropriate --> remove from cache and find a new image
        - if current image is not in cache, has a valid year and season, and is newer than the oldest image from same year and season in cache --> 
              replace the image in the cache
        - otherwise, do nothing        
        """
        #TODO: Same bug as above.
        cache_modified = False
        logging.info('checking if photo %d should go in cache' % (self.pk or -1))
        if self.in_chrono_cache and self.moderation_status < self.MIN_OK_MODERATION_STATUS and self.taken_year and self.taken_season:
            self.in_chrono_cache = False  # Remove me, but don't save here (we're in the save flow)            
            logging.info('removed photo %d from cache' % (self.pk or -1))
            cache_modified = True
            cache_candidates = Photo.objects.filter(in_chrono_cache=False).filter(taken_year=self.taken_year).filter(taken_season=self.taken_season)
            cache_candidates = self.filter_ok_moderation_statuses(cache_candidates)
            if cache_candidates.count() > 0:
                logging.info('added photo %d to cache instead' % cache_candidates[0].pk)
                cache_candidates[0].in_chrono_cache = True
                cache_candidates[0].save(no_cache_handling=True)
            
        if not self.in_chrono_cache and self.moderation_status >= self.MIN_OK_MODERATION_STATUS and self.taken_year and self.taken_season:
            logging.info('photo %d not in cache' % (self.pk or -1))
            cached_images = Photo.objects.filter(in_chrono_cache=True).filter(taken_year=self.taken_year).filter(taken_season=self.taken_season)
            cached_images = list(cached_images) #make the actual query, and fixate the count size
            if len(cached_images) >= self.NUM_IN_CHRONO_CACHE:
                logging.info('cache is full, checking if we should add photo %d' % (self.pk or -1))
                if not self.upload_date or self.upload_date > cached_images[self.NUM_IN_CHRONO_CACHE - 1].upload_date:  # I'm newer
                    logging.info('adding photo %d because it is newer: %s' % (self.pk or -1, str(self.upload_date)))
                    self.in_chrono_cache = True
                    cache_modified = True
                    i = self.NUM_IN_CHRONO_CACHE - 1
                    while i < len(cached_images):
                        p = cached_images[i]
                        logging.info('removing photo %d because it is old: %s' % (p.pk, str(p.upload_date)))
                        p.in_chrono_cache = False
                        p.save(no_cache_handling=True)
                        i += 1
            else:
                logging.info('cache is too small, adding photo %d' % (self.pk or -1))
                cache_modified = True
                self.in_chrono_cache = True  # Add me, but don't save here
        return cache_modified
            
        
    @classmethod    
    def filter_ok_moderation_statuses(cls, query_set):
        query_set = query_set.filter(moderation_status__gte = cls.MIN_OK_MODERATION_STATUS) 
        return query_set    


    @classmethod
    def get_cache_key(cls, pk):
        return 'PHOTO_ID_%s' % pk

    def pop_param(self, kwargs, param):
        try:
            kwargs.pop(param)
        except:
            pass


    def is_landscape(self):
        return self.photo_width >= self.photo_height


    def get_blob_width(self, blob_key, strip_size=0.01):
        im = Image(blob_key=blob_key) 
        im.crop(0.0,0.0,1.0,strip_size) 
        im_strip = Image(image_data=im.execute_transforms()) 
        return im_strip.width 


    def get_blob_height(self, blob_key, strip_size=0.01):
        im = Image(blob_key=blob_key) 
        im.crop(0.0,0.0,strip_size,1.0) 
        im_strip = Image(image_data=im.execute_transforms()) 
        return im_strip.height


    def validate_dimensions(self):
        if self.is_landscape():
            if self.photo_width < settings.MIN_GEOMETRY[0]:
                raise WidthTooSmallError()
            if self.photo_height < settings.MIN_GEOMETRY[1]:
                raise HeightTooSmallError()
        else:
            if self.photo_width < settings.MIN_GEOMETRY[1]:
                raise WidthTooSmallError()
            if self.photo_height < settings.MIN_GEOMETRY[0]:
                raise HeightTooSmallError()
        return True


    def generate_thumbnail(self, geometry):
        """
        Generate thumnail by scaling and croping
        Geometry: (width, height)
        """
        src_width = self.photo_width
        src_height = self.photo_height
        dst_width = geometry[0]
        dst_height = geometry[1]

        # see which dimension should be scaled, and which should be
        # scaled and cropped
        img = Image(blob_key = self.photo_blobkey)
        if 1.0 * dst_width / src_width > 1.0 * dst_height / src_height:
            img.resize(width = dst_width)
            scaled_img = Image(img.execute_transforms(output_encoding=JPEG))
            frac = 1.0 * (scaled_img.height - dst_height) / 2 / scaled_img.height
            scaled_img.crop(0.0, frac, 1.0, 1-frac)
        else:
            img.resize(height = dst_height)
            scaled_img = Image(img.execute_transforms(output_encoding=JPEG))
            frac = 1.0 * (scaled_img.width - dst_width) / 2 / scaled_img.width
            scaled_img.crop(frac, 0.0, 1.0-frac, 1.0)
        return scaled_img.execute_transforms(output_encoding=JPEG)


    def get_metadata(self):
        res = {
            'id': self.pk,
            'src': reverse('download_image', args=[self.pk]), 
            'thumbnail_src': reverse('download_thumbnail', args=[self.pk]), 
            'preview_src': reverse('download_preview', args=[self.pk]),
            'title': self.title,
            'description': self.description,
            'score': float(self.ranking_avg),
            'score_count': self.ranking_count,
            'locationId': self.region.id if self.region else None,
            'dayCreated': 1, #todo
            'upload_date':  self.upload_date.strftime('%d/%m/%y') if self.upload_date else '',
            'monthCreated': 1, #todo
            'yearCreated': self.taken_year if self.taken_year else datetime.now().year,
            'seasonId': self.taken_season,
            'owner_username': self.owner_username,
            'owner_id': self.owner.id if self.owner else None,
            'coordinates': self.coordinates,
        }
        if self.moderation_status != Photo.CONFIRMED_STATUS:
            res.update({
                'moderation_status': self.moderation_status,
                'moderation_comment': self.moderation_comment,
            })
        return res


    def update_ranking(self, new_rank):
        self.ranking_avg = ((self.ranking_avg * self.ranking_count) + Decimal(str(new_rank))) / (self.ranking_count + 1)
        self.ranking_avg = self.ranking_avg.quantize(Decimal('1.00')) #fix precision
        self.ranking_count += 1


    def clear_mem_caches(self, clear_map_cache = True, clear_chrono_cache = True):
        if self.pk:
            cache.delete(self.get_cache_key(self.pk))
        if clear_map_cache:
            cache.delete('MAP_CACHE')
        if clear_chrono_cache:
            cache.delete('CHRONO_CACHE')
        cache.delete('NEW_PHOTOS')
        cache.delete('HIGH_SCORE_PHOTOS')



