from util.json_request import expect_json
import json
import os
import logging
import sys
import mimetypes
import StringIO
import exceptions
from collections import defaultdict
from uuid import uuid4

# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image

from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.conf import settings
from django import forms

from xmodule.modulestore import Location
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from github_sync import export_to_github
from static_replace import replace_urls

from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError
from functools import partial
from itertools import groupby
from operator import attrgetter

from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent

from cache_toolbox.core import set_cached_content, get_cached_content, del_cached_content
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import ADMIN_ROLE_NAME, EDITOR_ROLE_NAME
from .utils import get_course_location_for_item

log = logging.getLogger(__name__)


COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']


# ==== Public views ==================================================

@ensure_csrf_cookie
def signup(request):
    """
    Display the signup form.
    """
    csrf_token = csrf(request)['csrf_token']
    return render_to_response('signup.html', {'csrf': csrf_token})


@ensure_csrf_cookie
def login_page(request):
    """
    Display the login form.
    """
    csrf_token = csrf(request)['csrf_token']
    return render_to_response('login.html', {'csrf': csrf_token})


# ==== Views for any logged-in user ==================================

@login_required
@ensure_csrf_cookie
def index(request):
    """
    List all courses available to the logged in user
    """
    courses = modulestore().get_items(['i4x', None, None, 'course', None])

    # filter out courses that we don't have access to
    courses = filter(lambda course: has_access(request.user, course.location), courses)

    return render_to_response('index.html', {
        'courses': [(course.metadata.get('display_name'),
                    reverse('course_index', args=[
                        course.location.org,
                        course.location.course,
                        course.location.name]))
                    for course in courses]
    })


# ==== Views with per-item permissions================================

def has_access(user, location, role=EDITOR_ROLE_NAME):
    '''Return True if user allowed to access this piece of data'''
    '''Note that the CMS permissions model is with respect to courses'''
    '''There is a super-admin permissions if user.is_staff is set'''
    return is_user_in_course_group_role(user, get_course_location_for_item(location), role)


@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = ['i4x', org, course, 'course', name]
    
    # check that logged in user has permissions to this item
    if not has_access(request.user, location):
        raise PermissionDenied()

    upload_asset_callback_url = reverse('upload_asset', kwargs = {
            'org' : org,
            'course' : course,
            'coursename' : name
            })

    course = modulestore().get_item(location)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'sections': sections,
        'upload_asset_callback_url': upload_asset_callback_url
    })


@login_required
def edit_unit(request, location):
    """
    Display an editing page for the specified module.

    Expects a GET request with the parameter 'id'.

    id: A Location URL
    """
    # check that we have permissions to edit this item
    if not has_access(request.user, location):
        raise PermissionDenied()

    item = modulestore().get_item(location)

    if settings.LMS_BASE is not None:
        lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
            lms_base=settings.LMS_BASE,
            # TODO: These will need to be changed to point to the particular instance of this problem in the particular course
            course_id= modulestore().get_containing_courses(item.location)[0].id,
            location=item.location,
        )
    else:
        lms_link = None

    component_templates = defaultdict(list)

    templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
    for template in templates:
        if template.location.category in COMPONENT_TYPES:
            component_templates[template.location.category].append((
                template.display_name,
                template.location.url(),
            ))

    components = [
        component.location.url()
        for component
        in item.get_children()
    ]

    return render_to_response('unit.html', {
        'unit': item,
        'components': components,
        'component_templates': component_templates,
    })


@login_required
def preview_component(request, location):
    # TODO (vshnayder): change name from id to location in coffee+html as well.
    if not has_access(request.user, location):
        raise Http404  # TODO (vshnayder): better error

    component = modulestore().get_item(location)

    return render_to_response('component.html', {
        'preview': get_module_previews(request, component)[0],
        'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
    })


@login_required
def delete_unit(request, location):
    pass


def user_author_string(user):
    '''Get an author string for commits by this user.  Format:
    first last <email@email.com>.

    If the first and last names are blank, uses the username instead.
    Assumes that the email is not blank.
    '''
    f = user.first_name
    l = user.last_name
    if f == '' and l == '':
        f = user.username
    return '{first} {last} <{email}>'.format(first=f,
                                             last=l,
                                             email=user.email)


@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
    """
    Dispatch an AJAX action to a preview XModule

    Expects a POST request, and passes the arguments to the module

    preview_id (str): An identifier specifying which preview this module is used for
    location: The Location of the module to dispatch to
    dispatch: The action to execute
    """

    instance_state, shared_state = load_preview_state(request, preview_id, location)
    descriptor = modulestore().get_item(location)
    instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
    # Let the module handle the AJAX
    try:
        ajax_return = instance.handle_ajax(dispatch, request.POST)
    except NotFoundError:
        log.exception("Module indicating to user that request doesn't exist")
        raise Http404
    except:
        log.exception("error processing ajax call")
        raise

    save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
    return HttpResponse(ajax_return)


def load_preview_state(request, preview_id, location):
    """
    Load the state of a preview module from the request

    preview_id (str): An identifier specifying which preview this module is used for
    location: The Location of the module to dispatch to
    """
    if 'preview_states' not in request.session:
        request.session['preview_states'] = defaultdict(dict)

    instance_state = request.session['preview_states'][preview_id, location].get('instance')
    shared_state = request.session['preview_states'][preview_id, location].get('shared')

    return instance_state, shared_state


def save_preview_state(request, preview_id, location, instance_state, shared_state):
    """
    Save the state of a preview module to the request

    preview_id (str): An identifier specifying which preview this module is used for
    location: The Location of the module to dispatch to
    instance_state: The instance state to save
    shared_state: The shared state to save
    """
    if 'preview_states' not in request.session:
        request.session['preview_states'] = defaultdict(dict)

    request.session['preview_states'][preview_id, location]['instance'] = instance_state
    request.session['preview_states'][preview_id, location]['shared'] = shared_state


def render_from_lms(template_name, dictionary, context=None, namespace='main'):
    """
    Render a template using the LMS MAKO_TEMPLATES
    """
    return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)


def preview_module_system(request, preview_id, descriptor):
    """
    Returns a ModuleSystem for the specified descriptor that is specialized for
    rendering module previews.

    request: The active django request
    preview_id (str): An identifier specifying which preview this module is used for
    descriptor: An XModuleDescriptor
    """

    return ModuleSystem(
        ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
        # TODO (cpennington): Do we want to track how instructors are using the preview problems?
        track_function=lambda type, event: None,
        filestore=descriptor.system.resources_fs,
        get_module=partial(get_preview_module, request, preview_id),
        render_template=render_from_lms,
        debug=True,
        replace_urls=replace_urls,
        user=request.user,
    )


def get_preview_module(request, preview_id, location):
    """
    Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
    from the set of preview data for the descriptor specified by Location

    request: The active django request
    preview_id (str): An identifier specifying which preview this module is used for
    location: A Location
    """
    descriptor = modulestore().get_item(location)
    instance_state, shared_state = descriptor.get_sample_state()[0]
    return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)


def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
    """
    Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state

    request: The active django request
    preview_id (str): An identifier specifying which preview this module is used for
    descriptor: An XModuleDescriptor
    instance_state: An instance state string
    shared_state: A shared state string
    """
    system = preview_module_system(request, preview_id, descriptor)
    try:
        module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
    except:
        module = ErrorDescriptor.from_descriptor(
            descriptor,
            error_msg=exc_info_to_str(sys.exc_info())
        ).xmodule_constructor(system)(None, None)

    module.get_html = wrap_xmodule(
        module.get_html,
        module,
        "xmodule_display.html",
    )
    module.get_html = replace_static_urls(
        module.get_html,
        module.metadata.get('data_dir', module.location.course)
    )
    save_preview_state(request, preview_id, descriptor.location.url(),
        module.get_instance_state(), module.get_shared_state())

    return module


def get_module_previews(request, descriptor):
    """
    Returns a list of preview XModule html contents. One preview is returned for each
    pair of states returned by get_sample_state() for the supplied descriptor.

    descriptor: An XModuleDescriptor
    """
    preview_html = []
    for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
        module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
        preview_html.append(module.get_html())
    return preview_html


@login_required
@expect_json
def save_item(request):
    item_location = request.POST['id']

    # check permissions for this user within this course
    if not has_access(request.user, item_location):
        raise PermissionDenied()

    logging.debug(request.POST['data'])
    if request.POST['data']:
        data = request.POST['data']
        modulestore().update_item(item_location, data)
        
    if request.POST['children']:
        children = request.POST['children']
        modulestore().update_children(item_location, children)

    # cdodge: also commit any metadata which might have been passed along in the
    # POST from the client, if it is there
    # note, that the postback is not the complete metadata, as there's system metadata which is
    # not presented to the end-user for editing. So let's fetch the original and
    # 'apply' the submitted metadata, so we don't end up deleting system metadata
    if request.POST['metadata']:
        posted_metadata = request.POST['metadata']
        # fetch original
        existing_item = modulestore().get_item(item_location)
        # update existing metadata with submitted metadata (which can be partial)
        existing_item.metadata.update(posted_metadata)
        modulestore().update_metadata(item_location, existing_item.metadata)

    descriptor = modulestore().get_item(item_location)
    preview_html = get_module_previews(request, descriptor)[0]

    return HttpResponse(json.dumps({
        'preview': preview_html
    }))


@login_required
@expect_json
def clone_item(request):
    parent_location = Location(request.POST['parent_location'])
    template = Location(request.POST['template'])

    if not has_access(request.user, parent_location):
        raise PermissionDenied()

    parent = modulestore().get_item(parent_location)
    dest_location = parent_location._replace(category=template.category, name=uuid4().hex)

    new_item = modulestore().clone_item(template, dest_location)

    # TODO: This needs to be deleted when we have proper storage for static content
    new_item.metadata['data_dir'] = parent.metadata['data_dir']

    modulestore().update_metadata(new_item.location.url(), new_item.own_metadata)
    modulestore().update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])

    return HttpResponse(json.dumps({'id': dest_location.url()}))

'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
#@login_required
#@ensure_csrf_cookie
def upload_asset(request, org, course, coursename):

    if request.method != 'POST':
        # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
        return HttpResponseBadRequest()

    # construct a location from the passed in path
    location = ['i4x', org, course, 'course', coursename]
    if not has_access(request.user, location):
        return HttpResponseForbidden()
    
    # Does the course actually exist?!?
    
    try:
        item = modulestore().get_item(location)
    except:
        # no return it as a Bad Request response
        logging.error('Could not find course' + location)
        return HttpResponseBadRequest()

    # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
    # nomenclature since we're using a FileSystem paradigm here. We're just imposing
    # the Location string formatting expectations to keep things a bit more consistent

    name = request.FILES['file'].name
    mime_type = request.FILES['file'].content_type
    filedata = request.FILES['file'].read()

    file_location = StaticContent.compute_location_filename(org, course, name)

    content = StaticContent(file_location, name, mime_type, filedata)

    # first commit to the DB
    contentstore().save(content)

    # then remove the cache so we're not serving up stale content
    # NOTE: we're not re-populating the cache here as the DB owns the last-modified timestamp
    # which is used when serving up static content. This integrity is needed for
    # browser-side caching support. We *could* re-fetch the saved content so that we have the
    # timestamp populated, but we might as well wait for the first real request to come in
    # to re-populate the cache.
    del_cached_content(file_location)

    # if we're uploading an image, then let's generate a thumbnail so that we can
    # serve it up when needed without having to rescale on the fly
    if mime_type.split('/')[0] == 'image':
        try:
            # not sure if this is necessary, but let's rewind the stream just in case
            request.FILES['file'].seek(0)

            # use PIL to do the thumbnail generation (http://www.pythonware.com/products/pil/)
            # My understanding is that PIL will maintain aspect ratios while restricting
            # the max-height/width to be whatever you pass in as 'size'
            # @todo: move the thumbnail size to a configuration setting?!?
            im = Image.open(request.FILES['file'])

            # I've seen some exceptions from the PIL library when trying to save palletted 
            # PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
            im = im.convert('RGB')
            size = 128, 128
            im.thumbnail(size, Image.ANTIALIAS)
            thumbnail_file = StringIO.StringIO()
            im.save(thumbnail_file, 'JPEG')
            thumbnail_file.seek(0)
        
            # use a naming convention to associate originals with the thumbnail
            #   <name_without_extention>.thumbnail.jpg
            thumbnail_name = os.path.splitext(name)[0] + '.thumbnail.jpg'
            # then just store this thumbnail as any other piece of content
            thumbnail_file_location = StaticContent.compute_location_filename(org, course, 
                                                                              thumbnail_name)
            thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name, 
                                              'image/jpeg', thumbnail_file)
            contentstore().save(thumbnail_content)

            # remove any cached content at this location, as thumbnails are treated just like any
            # other bit of static content
            del_cached_content(thumbnail_file_location)
        except:
            # catch, log, and continue as thumbnails are not a hard requirement
            logging.error('Failed to generate thumbnail for {0}. Continuing...'.format(name))

    return HttpResponse('Upload completed')

'''
This view will return all CMS users who are editors for the specified course
'''
@login_required
@ensure_csrf_cookie
def manage_users(request, org, course, name):
    location = ['i4x', org, course, 'course', name]
    
    # check that logged in user has permissions to this item
    if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
        raise PermissionDenied()

    return render_to_response('manage_users.html', {
        'editors': get_users_in_course_group_by_role(location, EDITOR_ROLE_NAME)
    })
    

def create_json_response(errmsg = None):
    if errmsg is not None:
        resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg}))
    else:
        resp = HttpResponse(json.dumps({'Status': 'OK'}))

    return resp

'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
@login_required
@ensure_csrf_cookie
def add_user(request, org, course, name):
    email = request.POST["email"]

    if email=='':
        return create_json_response('Please specify an email address.')

    location = ['i4x', org, course, 'course', name]
    
    # check that logged in user has admin permissions to this course
    if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
        raise PermissionDenied()
    
    user = get_user_by_email(email)
    
    # user doesn't exist?!? Return error.
    if user is None:
        return create_json_response('Could not find user by email address \'{0}\'.'.format(email))

    # user exists, but hasn't activated account?!?
    if not user.is_active:
        return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))

    # ok, we're cool to add to the course group
    add_user_to_course_group(request.user, user, location, EDITOR_ROLE_NAME)

    return create_json_response()

'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
@login_required
@ensure_csrf_cookie
def remove_user(request, org, course, name):
    email = request.POST["email"]

    location = ['i4x', org, course, 'course', name]
    
    # check that logged in user has admin permissions on this course
    if not has_access(request.user, location, role=ADMIN_ROLE_NAME):
        raise PermissionDenied()

    user = get_user_by_email(email)
    if user is None:
        return create_json_response('Could not find user by email address \'{0}\'.'.format(email))

    remove_user_from_course_group(request.user, user, location, EDITOR_ROLE_NAME)

    return create_json_response()

@login_required
@ensure_csrf_cookie
def asset_index(request, location):
    return render_to_response('asset_index.html',{})