import random
import simplejson
import base64
import urllib
import re
from datetime import datetime, timedelta
import logging

from django.http import Http404
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.template import RequestContext
from django.views.decorators.csrf import csrf_exempt  #TODO - fix this
from django.conf import settings

from filetransfers.api import prepare_upload, serve_file
from search.core import search
from utils.utils import set_trace, add_cache_forever_headers

from google.appengine.api.images import BadImageError, LargeImageError, NotImageError, UnsupportedSizeError

from models import UserProfile, Photo, Region, Organization, WidthTooSmallError, HeightTooSmallError
from forms import PhotoUploadForm

# Caches
# REGION_LIST       - list of regions and their meta-data + index
# REGION_ID_DICT    - dictionary of region-id to region + index
# MAP_CACHE         - list of images in the map-cache
# CHRONO_CACHE      - list of images in the chrono-cache
# NEW_PHOTOS        - list of latest images
# HIGH_SCORE_PHOTOS - list of photos with highest score
# PHOTO_ID_%d       - cached photo object
# PHOTO_ID_META_%d  - cached meta data for photo
# USER_COUNT        - number of users

def handler404(request):
    return HttpResponseRedirect(reverse('content') + '#missing')


def home_handler(request):
    new_photos = cache.get('NEW_PHOTOS') or setup_new_photos()
    top_photos = cache.get('HIGH_SCORE_PHOTOS') or setup_high_score_photos()
    for p in top_photos:
        p['style_width'] = '%d%%' % int(p['score'] / 5 * 100)
    return render_to_response('home/home.html', {
        'new_photos': new_photos[:3], 
        'top_photos': top_photos[:3], },
        context_instance=RequestContext(request))


def content_template(request, template_id):
    template_name = 'photos/%s' % template_id
    flagged_photos_count = 0
    unconfirmed_photos_count = 0
    confirmed_photos_count = 0
    if template_id == 'listTmpl.html':
        if request.user:
            try:
                profile = request.user.get_profile()
                if profile and profile.is_moderator:
                    flagged_photos_count = Photo.objects.filter(moderation_status = Photo.INAPPROPRIATE_STATUS).count()
                    confirmed_photos_count = Photo.objects.filter(moderation_status = Photo.CONFIRMED_STATUS).count()
                    unconfirmed_photos_count = Photo.objects.filter(moderation_status = 0).count()
            except:
                pass
    
    return render_to_response(template_name, {
        'user_count': cache.get('USER_COUNT') or setup_user_count(),
        'flagged_photos_count': flagged_photos_count,
        'unconfirmed_photos_count': unconfirmed_photos_count,
        'confirmed_photos_count': confirmed_photos_count,},
        context_instance=RequestContext(request))


def content_handler(request):
    regions = cache.get('REGION_LIST') or setup_region_list()
    return render_to_response('photos/content_base.html', {
        'user': request.user,
        'years': range(datetime.today().year, 1920, -1),
        'regions': regions, },
        context_instance=RequestContext(request))


def image_download_handler(request, pk):
    photo = get_photo_or_404(pk)
    return serve_file(request, photo.photo)


def thumbnail_download_handler(request, pk):
    """
    Return thumbnail of requested aspect ratio
    supported ratios:
    1:1 (default)
    16:9
    """
    photo = get_photo_or_404(pk)
    if request.GET.get('aspect','1:1') == '1:1':
        thumbnail = photo.thumbnail 
    elif request.GET.get('aspect','1:1') == '16:9':
        thumbnail = photo.thumbnail_16_9
    else:
        raise Http404
    response = HttpResponse(thumbnail, content_type='image/jpeg')
    return add_cache_forever_headers(response, 'thumbnail/%s' % pk)


def preview_download_handler(request, pk):
    photo = get_photo_or_404(pk)
    preview = photo.preview
    response = HttpResponse(preview, content_type='image/jpeg')
    return add_cache_forever_headers(response, 'preview/%s' % pk)


def upload_success_handler(request, pk):
    photo = get_photo_or_404(pk)
    return render_to_response('photos/upload_success.html', {
        'upload_id': request.GET.get('upload_id', ''),
        'image_id': photo.pk, 
        'thumbnail_url': reverse('download_thumbnail', args=[photo.pk]), 
        'image_url': reverse('download_image', args=[photo.pk]), 
        'image_update_url': reverse('image_handler', args=[photo.pk]), }, 
        context_instance=RequestContext(request))


def upload_error_handler(request):
    return render_to_response('photos/upload_error.html', {
        'upload_id': request.GET.get('upload_id', ''), 
        'error_id': request.GET.get('error_id', ''), 
        'error_msg': request.GET.get('error_msg', ''), },
        context_instance=RequestContext(request))


@csrf_exempt #TODO - fix this
@login_required
def image_upload_handler(request):
    if request.method == 'POST':
        form = PhotoUploadForm(request.POST, request.FILES)
        if form.is_valid():
            try:
                new_photo = Photo()
                new_photo.owner = request.user if request.user.is_authenticated() else None
                new_photo.photo = form.cleaned_data['file']
                new_photo.save()
                redirect_url = '%s?upload_id=%s' % (reverse('upload_success_handler', args=[new_photo.pk]), form.cleaned_data['upload_id'])
            except BadImageError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1001, 'BadImageError')            
            except LargeImageError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1002, 'LargeImageError')            
            except WidthTooSmallError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1003, 'WidthTooSmallError')            
            except HeightTooSmallError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1004, 'HeightTooSmallError')
            except NotImageError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1005, 'NotImageError')
            except UnsupportedSizeError:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1006, 'UnsupportedSizeError')            
            except:
                redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                    form.cleaned_data['upload_id'], 1010, 'UnknownError')
                    
            return HttpResponseRedirect(redirect_url)
        else:
            redirect_url = '%s?upload_id=%s&error_id=%d&error_msg=%s' % (reverse('upload_error_handler'), 
                form.data['upload_id'], 1000, 'FormInvalidError')            

    upload_url, upload_data = prepare_upload(request, reverse('image_upload_handler'))
    if upload_url.lower().find('temporary-blobstore-error') >= 0:
        upload_url = 'bad'  #browser will check this

    form = PhotoUploadForm()
    return render_to_response('photos/upload.html', {
        'form': form, 
        'upload_url': upload_url, 
        'upload_data': upload_data}, context_instance=RequestContext(request))



@csrf_exempt #TODO - fix this
def image_handler(request, pk):
    """
    GET -       Returns the image meta-data
    DELETE -    Requires logged-in + owner, Deletes the image
    POST/PUT -  Requires logged-in + owner, Updates the image meta-data 

    title
    description
    seasonId / taken_season
    year / taken_year
    locationId / region
    coordinates
    organization
    unflag - allowed only for moderator
    """
    photo = get_photo_or_404(pk)
    if request.method == 'GET':
        if photo.moderation_status == Photo.INAPPROPRIATE_STATUS:
            if not request.user or not request.user.is_authenticated() \
            or not request.user.get_profile() or not request.user.get_profile().is_moderator:
                raise Http404
        return HttpResponse(simplejson.dumps(photo.get_metadata()))

    if not request.user.is_authenticated():
        raise Exception('Not authenticated')


    # properties allowed only by moderator
    if 'unflag' in request.REQUEST:
        user_profile = request.user.get_profile()
        if user_profile and user_profile.is_moderator:
            photo.moderation_status = Photo.CONFIRMED_STATUS
            photo.save()
            return HttpResponse('ok')

    if request.method == 'DELETE':
        user_profile = request.user.get_profile()
        if user_profile and user_profile.is_moderator:
            photo.delete()
            return HttpResponse('ok')

    # properties allowed only by owner
    if request.user != photo.owner:
        raise Exception('Not owner')
    
    if request.method in ['POST', 'PUT']:
        dirty = False
        if photo.moderation_status == 0: #pending 
            photo.moderation_status = Photo.CONFIRMED_STATUS 
            dirty = True            
        
        for attr in ['title', 'description', 'coordinates' ]:
            if attr in request.REQUEST:
                photo.__setattr__(attr, request.REQUEST[attr])
                dirty = True

        if 'seasonId' in request.REQUEST:
            photo.taken_season = int(request.REQUEST['seasonId'])
            dirty = True
        
        if 'taken_season' in request.REQUEST:
            photo.taken_season = int(request.REQUEST['taken_season'])
            dirty = True
            
        if 'year' in request.REQUEST:
            photo.taken_year = int(request.REQUEST['year'])
            dirty = True    
        
        if 'taken_year' in request.REQUEST:
            photo.taken_year = int(request.REQUEST['taken_year'])
            dirty = True    
            
        if 'locationId' in request.REQUEST:
            photo.region = Region.objects.get(id = int(request.REQUEST['locationId']))
            dirty = True

        if 'region' in request.REQUEST:
            photo.region = Region.objects.get(id = int(request.REQUEST['region']))
            dirty = True

        if 'organization' in request.REQUEST:
            photo.organization = Organization.objects.get(id = int(request.REQUEST['organization']))
            dirty = True

        if 'owner' in request.REQUEST:
            photo.owner = User.objects.get(id = int(request.REQUEST['owner']))
            dirty = True

        if dirty:
            photo.save()
            return HttpResponse('ok')
        else:
            raise Exception('Nothing to update')


@csrf_exempt #TODO - fix this
def image_public_update_handler(request, pk):
    """
    Updates the image properties. Does not require user to be logged in or owner

    score
    flag_inappropriate
    """
    if request.method != 'POST':
        raise Exception('Bad method')

    dirty = False
    photo = get_photo_or_404(pk)

    if 'score' in request.REQUEST:
        photo.update_ranking(int(request.REQUEST['score']))
        dirty = True

    if 'flag_inappropriate' in request.REQUEST:
        photo.moderation_status = Photo.INAPPROPRIATE_STATUS
        photo.moderation_comment = request.REQUEST['flag_inappropriate'] 
        dirty = True

    if dirty:
        photo.save()
        return HttpResponse('ok')
    else:
        raise Exception('Nothing to update')
    

    
def list_images(request):
    """
    Return a set of images based on query parameters, filters, etc.

    Parameters:
    limit - maxmimum number of items to return
    start - start index of first item
    locationId - index of selected region, , default(all)
    order - newest, ranking, taken, default(newest)
    search - search query
    owner - return only images owned by current user
    seasonId - return only images from specified season
    year - return only images from specified year
    ownerId - return only images from specified owner
    set - grid (default), map, chron
    """
    start = int(request.GET.get('start', 0))
    limit = int(request.GET.get('limit', Photo.objects.count()))
    
    if 'search' in request.GET:
        # http://stackoverflow.com/questions/300445/how-to-unquote-a-urlencoded-unicode-string-in-python
        def unquote(text):
            def unicode_unquoter(match):
                return unichr(int(match.group(1),16))
            return re.sub(r'%u([0-9a-fA-F]{4})',unicode_unquoter,text)
        z = unquote(request.GET.get('search'))
        photos = search(Photo, z)
        photos = [p.get_metadata() for p in photos if p.moderation_status >= Photo.MIN_OK_MODERATION_STATUS]
        return HttpResponse(simplejson.dumps({
            'total': len(photos),
            'start': start,
            'limit': limit,
            'photos': photos[start:limit],
        }), content_type='application/json')
                                        

    photos = Photo.objects.all()
    if 'locationId' in request.GET:
        region = int(request.GET.get('locationId'))
        photos = photos.filter(region = region)

    if 'seasonId' in request.GET:
        seasonId = int(request.GET.get('seasonId'))
        photos = photos.filter(taken_season = seasonId)

    if 'year' in request.GET:
        year = int(request.GET.get('year'))
        photos = photos.filter(taken_year = year)

    order_by = '-upload_date'
    if 'order' in request.GET:
        order = request.GET.get('order')
        if order == 'taken':
            order_by = '-taken_year'
        elif order == 'score':
            order_by = '-ranking_avg'

    if 'owner' in request.GET:
        if request.user.is_authenticated():
            photos = photos.filter(owner = request.user)
        else:
            photos = Photo.objects.none()
            
    if 'ownerId' in request.GET:
        try:
            ownerObj = User.objects.get(id = int(request.GET.get('ownerId')))
            photos = photos.filter(owner = ownerObj)
        except User.DoesNotExist:
            photos = Photo.objects.none()           
           
    try:
        if request.user and request.user.is_authenticated() and request.user.get_profile() and request.user.get_profile().is_moderator:
            if 'moderated' in request.GET:
                photos = photos.filter(moderation_status = Photo.INAPPROPRIATE_STATUS)
            elif 'unconfirmed' in request.GET:
                photos = photos.filter(moderation_status = 0)
            else:
                photos = Photo.filter_ok_moderation_statuses(photos)
        else:
            photos = Photo.filter_ok_moderation_statuses(photos)
    except UserProfile.DoesNotExist:
        photos = Photo.filter_ok_moderation_statuses(photos)

    return HttpResponse(simplejson.dumps({
        'total': photos.count(),
        'start': start,
        'limit': limit,
        'photos': [p.get_metadata() for p in photos.order_by('-moderation_status', order_by)[start:limit]]
    }), content_type='application/json')

      
def init_dataset(request):
    region_data = cache.get('REGION_ID_DICT') or setup_region_dict()
    new_photos_data = cache.get('NEW_PHOTOS') or setup_new_photos()
    return render_to_response('photos/dataset.js.php', {
        'images': simplejson.dumps(new_photos_data),
        'regions': simplejson.dumps(region_data), }, 
        context_instance=RequestContext(request))

        
def init_minimap_dataset(request):
    region_data = cache.get('REGION_ID_DICT') or setup_region_dict()
    map_photos = cache.get('MAP_CACHE') or setup_map_cache()
    return render_to_response('photos/dataset.js.php', {
        'images': simplejson.dumps(map_photos),
        'regions': simplejson.dumps(region_data), }, 
        context_instance=RequestContext(request))


def init_map_dataset(request):
    map_photos = cache.get('MAP_CACHE') or setup_map_cache()
    return HttpResponse(simplejson.dumps({
        'total': len(map_photos),
        'photos': map_photos,
        }), content_type='application/json')


def init_chrono_dataset(request):
    photos = cache.get('CHRONO_CACHE') or setup_chrono_cache()
    return HttpResponse(simplejson.dumps({
        'total': len(photos),
        'photos': photos,
        }), content_type='application/json')


def list_regions(request):
    region_data = cache.get('REGION_LIST') or setup_region_list()
    return HttpResponse(simplejson.dumps(region_data), 
                        content_type='application/json')


def region_handler(request, pk):
    region = get_object_or_404(Region, pk=pk)
    return HttpResponse(simplejson.dumps(region.dump()), content_type='application/json')


def current_user(request):
    return HttpResponse(simplejson.dumps({
        'id': request.user.id,
        'username': request.user.username,
        'is_authenticated': request.user.is_authenticated(),
    }), content_type='application/json')


def redirect_handler(request, token):
    dest = base64.urlsafe_b64decode(str(token))
    return HttpResponseRedirect(dest)


def api_version(request):
    return HttpResponse(settings.API_VERSION)


def get_user_count(request):
    return HttpResponse(simplejson.dumps({'user_count': User.objects.count()}))


def facebook_share_handler(request, image_id):
    return render_to_response('photos/share_img.html', {
        'image_id': image_id, 
        'share_url': request.build_absolute_uri(reverse('download_preview', args=[image_id])),
        'random': str(random.randint(10**9,10**10)), },
        context_instance=RequestContext(request))


###############################################################################

def setup_region_dict():
    regions = Region.objects.all().order_by('sort_order', 'name')
    region_data = {}
    for i, r in enumerate(regions):
        data = r.dump()
        data.update({'index': i+1})
        region_data[r.id] = data
    cache.set('REGION_ID_DICT', region_data)
    return region_data


def setup_region_list():
    regions = Region.objects.all().order_by('sort_order', 'name')
    def helper (r):
        data = r.dump()
        data.update({'index': helper.count})
        helper.count += 1
        return data
    helper.count = 1
    region_list = map(helper, regions)
    cache.set('REGION_LIST', region_list)
    return region_list


def setup_new_photos(limit = 30):
    photo_data = Photo.objects.all()
    photo_data = Photo.filter_ok_moderation_statuses(photo_data)
    new_photos_data = [p.get_metadata() for p in photo_data[0:limit]]
    cache.set('NEW_PHOTOS', new_photos_data)        
    return new_photos_data


def setup_high_score_photos(limit = 30):
    top_photos = Photo.objects.order_by('-moderation_status', '-ranking_avg')
    top_photos = Photo.filter_ok_moderation_statuses(top_photos)
    top_photos_data = [p.get_metadata() for p in top_photos[0:limit]]
    cache.set('HIGH_SCORE_PHOTOS', top_photos_data)        
    return top_photos_data


def setup_map_cache():
    map_cache_images = Photo.objects.filter(in_map_cache = True)
    region_counter = {}
    map_cache_data = []
    for p in map_cache_images:
        z = region_counter[p.region] = region_counter.get(p.region,0) + 1
        if z <= Photo.NUM_IN_MAP_CACHE:
            map_cache_data.append(p.get_metadata())
        else:
            logging.info('skipping image %d since we already have too many in map cache' % (p.pk or -1))

    cache.set('MAP_CACHE', map_cache_data)
    return map_cache_data


def setup_chrono_cache():
    chrono_cache_images = Photo.objects.filter(in_chrono_cache = True)
    year_and_season_counter = {}
    chrono_cache_data = []
    for p in chrono_cache_images:
        z = year_and_season_counter[(p.taken_year, p.taken_season)] = year_and_season_counter.get((p.taken_year, p.taken_season),0) + 1
        if z <= Photo.NUM_IN_CHRONO_CACHE:
            chrono_cache_data.append(p.get_metadata())
        else:
            logging.info('skipping image %d since we already have too many in chrono cache' % (p.pk or -1))
    cache.set('CHRONO_CACHE', chrono_cache_data)
    return chrono_cache_data


def setup_user_count():
    user_count = User.objects.count()
    cache.set('USER_COUNT', user_count)
    return user_count


def get_photo_or_404(pk):
    key = Photo.get_cache_key(pk)
    photo = cache.get(key)

    invalidate = True
    try:
        if photo and photo.preview and photo.thumbnail and photo.thumbnail_16_9:
            invalidate = False
    except AttributeError:
        pass #this means we need to invalidate, since we have an old version without the thumbnails or preview
    finally:
        if invalidate:
            photo = get_object_or_404(Photo, pk=pk)
            cache.set(key, photo)
    return photo

