import json
import logging

from lxml import etree

from django.http import Http404
from django.http import HttpResponse
from django.shortcuts import redirect

from fs.osfs import OSFS

from django.conf import settings
from mitxmako.shortcuts import render_to_string, render_to_response

from models import StudentModule
from multicourse import multicourse_settings
from util.views import accepts

import courseware.content_parser as content_parser
import xmodule

log = logging.getLogger("mitx.courseware")

class I4xSystem(object):
    '''
    This is an abstraction such that x_modules can function independent 
    of the courseware (e.g. import into other types of courseware, LMS, 
    or if we want to have a sandbox server for user-contributed content)

    I4xSystem objects are passed to x_modules to provide access to system
    functionality.

    Note that these functions can be closures over e.g. a django request
    and user, or other environment-specific info.
    '''
    def __init__(self, ajax_url, track_function, render_function,
                 module_from_xml, render_template, request=None,
                 filestore=None, course=None):
        '''
        Create a closure around the system environment.

        ajax_url - the url where ajax calls to the encapsulating module go.
        track_function - function of (event_type, event), intended for logging
                         or otherwise tracking the event.
                         TODO: Not used, and has inconsistent args in different
                         files.  Update or remove.
        module_from_xml - function that takes (module_xml) and returns a corresponding
                          module instance object.
        render_function - function that takes (module_xml) and renders it,
                          returning a dictionary with a context for rendering the
                          module to html.  Dictionary will contain keys 'content'
                          and 'type'.
        render_template - a function that takes (template_file, context), and returns
                          rendered html.
        request - the request in progress
        filestore - A filestore ojbect.  Defaults to an instance of OSFS based at
                    settings.DATA_DIR.
        '''
        self.course = course
        self.ajax_url = ajax_url
        self.track_function = track_function
        if not filestore: 
            self.filestore = OSFS(settings.DATA_DIR)
        else:
            self.filestore = filestore
            if settings.DEBUG:
                log.info("[courseware.module_render.I4xSystem] filestore path = %s",
                         filestore)
        self.module_from_xml = module_from_xml
        self.render_function = render_function
        self.render_template = render_template
        self.exception404 = Http404
        self.DEBUG = settings.DEBUG
        self.id = request.user.id if request is not None else 0

    def get(self, attr):
        '''	provide uniform access to attributes (like etree).'''
        return self.__dict__.get(attr)
    
    def set(self,attr,val):
        '''provide uniform access to attributes (like etree)'''
        self.__dict__[attr] = val

    def __repr__(self):
        return repr(self.__dict__)

    def __str__(self):
        return str(self.__dict__)

def smod_cache_lookup(cache, module_type, module_id):
    '''
    Look for a student module with the given type and id in the cache.

    cache -- list of student modules

    returns first found object, or None
    '''
    for o in cache: 
        if o.module_type == module_type and o.module_id == module_id:
            return o
    return None

def make_track_function(request):
    ''' 
    Make a tracking function that logs what happened.
    For use in I4xSystem.
    '''
    import track.views

    def f(event_type, event):
        return track.views.server_track(request, event_type, event, page='x_module')
    return f

def grade_histogram(module_id):
    ''' Print out a histogram of grades on a given problem. 
        Part of staff member debug info.
    '''
    from django.db import connection
    cursor = connection.cursor()

    q = """SELECT courseware_studentmodule.grade,
                  COUNT(courseware_studentmodule.student_id)
    FROM courseware_studentmodule
    WHERE courseware_studentmodule.module_id=%s
    GROUP BY courseware_studentmodule.grade"""
    # Passing module_id this way prevents sql-injection.
    cursor.execute(q, [module_id])

    grades = list(cursor.fetchall())
    grades.sort(key=lambda x: x[0])          # Add ORDER BY to sql query?
    if len(grades) == 1 and grades[0][0] is None:
        return []
    return grades


def make_module_from_xml_fn(user, request, student_module_cache, position):
    '''Create the make_from_xml() function'''
    def module_from_xml(xml):
        '''Modules need a way to convert xml to instance objects.
        Pass the rest of the context through.'''
        (instance, sm, module_type) = get_module(
            user, request, xml, student_module_cache, position)
        return instance
    return module_from_xml


def get_module(user, request, module_xml, student_module_cache, position=None):
    ''' Get an instance of the xmodule class corresponding to module_xml,
    setting the state based on an existing StudentModule, or creating one if none
    exists.

    Arguments:
      - user                  : current django User
      - request               : current django HTTPrequest
      - module_xml            : lxml etree of xml subtree for the requested module
      - student_module_cache  : list of StudentModule objects, one of which may
                                match this module type and id
      - position   	          : extra information from URL for user-specified
                                position within module

    Returns:
      - a tuple (xmodule instance, student module, module type).
    '''
    module_type = module_xml.tag
    module_class = xmodule.get_module_class(module_type)
    module_id = module_xml.get('id')

    # Grab xmodule state from StudentModule cache
    smod = smod_cache_lookup(student_module_cache, module_type, module_id)
    state = smod.state if smod else None

    # get coursename if present in request
    # coursename = multicourse_settings.get_coursename_from_request(request)

    # if coursename and settings.ENABLE_MULTICOURSE:
    #     # path to XML for the course
    #     xp = multicourse_settings.get_course_xmlpath(coursename)
    #     data_root = settings.DATA_DIR + xp
    # else:
    data_root = settings.DATA_DIR+"/"+os.path.basename(course.path)

    # Setup system context for module instance
    ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/'

    module_from_xml = make_module_from_xml_fn(
        user, request, student_module_cache, position)
    
    system = I4xSystem(track_function = make_track_function(request), 
                       render_function = lambda xml: render_x_module(
                           user, request, xml, student_module_cache, position),
                       render_template = render_to_string,
                       ajax_url = ajax_url,
                       request = request,
                       filestore = OSFS(data_root),
                       module_from_xml = module_from_xml,
                       )
    # pass position specified in URL to module through I4xSystem
    system.set('position', position) 
    instance = module_class(system, 
                            etree.tostring(module_xml), 
                            module_id, 
                            state=state)

    # If StudentModule for this instance wasn't already in the database,
    # and this isn't a guest user, create it.
    if not smod and user.is_authenticated():
        smod = StudentModule(student=user, module_type = module_type,
                           module_id=module_id, state=instance.get_state())
        smod.save()
        # Add to cache. The caller and the system context have references
        # to it, so the change persists past the return
        student_module_cache.append(smod)

    return (instance, smod, module_type)

def render_x_module(user, request, module_xml, student_module_cache, position=None):
    ''' Generic module for extensions. This renders to HTML.

    modules include sequential, vertical, problem, video, html

    Note that modules can recurse.  problems, video, html, can be inside sequential or vertical.

    Arguments:

      - user                  : current django User
      - request               : current django HTTPrequest
      - module_xml            : lxml etree of xml subtree for the current module
      - student_module_cache : list of StudentModule objects, one of which may match this module type and id
      - position   	      : extra information from URL for user-specified position within module

    Returns:

      - dict which is context for HTML rendering of the specified module.  Will have
      key 'content', and will have 'type' key if passed a valid module.
    '''
    if module_xml is None :
        return {"content": ""}

    (instance, smod, module_type) = get_module(
        user, request, module_xml, student_module_cache, position)

    content = instance.get_html()

    # special extra information about each problem, only for users who are staff 
    if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
        module_id = module_xml.get('id')
        histogram = grade_histogram(module_id)
        render_histogram = len(histogram) > 0
        staff_context = {'xml': etree.tostring(module_xml), 
                         'module_id': module_id,
                         'histogram': json.dumps(histogram),
                         'render_histogram': render_histogram}
        content += render_to_string("staff_problem_info.html", staff_context)

    context = {'content': content, 'type': module_type}
    return context

def modx_dispatch(request, module=None, dispatch=None, id=None):
    ''' Generic view for extensions. This is where AJAX calls go.

    Arguments:

      - request -- the django request.
      - module -- the type of the module, as used in the course configuration xml.
                  e.g. 'problem', 'video', etc
      - dispatch -- the command string to pass through to the module's handle_ajax call
           (e.g. 'problem_reset').  If this string contains '?', only pass
           through the part before the first '?'.
      - id -- the module id.  Used to look up the student module.
            e.g. filenamexformularesponse
    '''
    # ''' (fix emacs broken parsing)
    if not request.user.is_authenticated():
        return redirect('/')

    # python concats adjacent strings
    error_msg = ("We're sorry, this module is temporarily unavailable. "
                 "Our staff is working to fix it as soon as possible")


    # Grab the student information for the module from the database
    s = StudentModule.objects.filter(student=request.user, 
                                     module_id=id)

    if s is None or len(s) == 0:
        log.debug("Couldn't find module '%s' for user '%s' and id '%s'",
                  module, request.user, id)
        raise Http404
    s = s[0]

    oldgrade = s.grade
    oldstate = s.state

    # If there are arguments, get rid of them
    dispatch, _, _ = dispatch.partition('?')

    ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL,
                                                  module=module, id=id)
    coursename = multicourse_settings.get_coursename_from_request(request)
    if coursename and settings.ENABLE_MULTICOURSE:
        xp = multicourse_settings.get_course_xmlpath(coursename)
        data_root = settings.DATA_DIR + xp
    else:
        data_root = settings.DATA_DIR

    # Grab the XML corresponding to the request from course.xml
    try:
        xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
    except:
        log.exception(
            "Unable to load module during ajax call. module=%s, dispatch=%s, id=%s",
            module, dispatch, id)
        if accepts(request, 'text/html'):
            return render_to_response("module-error.html", {})
        else:
            response = HttpResponse(json.dumps({'success': error_msg}))
        return response

    # TODO: This doesn't have a cache of child student modules.  Just
    # passing the current one.  If ajax calls end up needing children,
    # this won't work (but fixing it may cause performance issues...)
    # Figure out :)
    module_from_xml = make_module_from_xml_fn(
        request.user, request, [s], None)

    # Create the module
    system = I4xSystem(track_function = make_track_function(request), 
                       render_function = None,
                       module_from_xml = module_from_xml,
                       render_template = render_to_string,
                       ajax_url = ajax_url,
                       request = request,
                       filestore = OSFS(data_root),
                       )

    try:
        module_class = xmodule.get_module_class(module)
        instance = module_class(system, xml, id, state=oldstate)
    except:
        log.exception("Unable to load module instance during ajax call")
        if accepts(request, 'text/html'):
            return render_to_response("module-error.html", {})
        else:
            response = HttpResponse(json.dumps({'success': error_msg}))
        return response

    # Let the module handle the AJAX
    try:
        ajax_return = instance.handle_ajax(dispatch, request.POST)
    except:
        log.exception("error processing ajax call")
        raise

    # Save the state back to the database
    s.state = instance.get_state()
    if instance.get_score(): 
        s.grade = instance.get_score()['score']
    if s.grade != oldgrade or s.state != oldstate:
        s.save()
    # Return whatever the module wanted to return to the client/caller
    return HttpResponse(ajax_return)
