"""
Course settings module. The settings are based of django.conf. All settings in
courseware.global_course_settings are first applied, and then any settings
in the settings.DATA_DIR/course_settings.py are applied. A setting must be
in ALL_CAPS.
    
Settings are used by calling
    
from courseware import course_settings

Note that courseware.course_settings is not a module -- it's an object. So 
importing individual settings is not possible:

from courseware.course_settings import GRADER  # This won't work.

"""

from lxml import etree
import random
import imp
import logging
import sys
import types

from django.conf import settings

from courseware import global_course_settings
from courseware import graders
from courseware.graders import Score
from models import StudentModule
import courseware.content_parser as content_parser
import courseware.modules

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

class Settings(object):
    def __init__(self):
        # update this dict from global settings (but only for ALL_CAPS settings)
        for setting in dir(global_course_settings):
            if setting == setting.upper():
                setattr(self, setting, getattr(global_course_settings, setting))
        
        
        data_dir = settings.DATA_DIR
        
        fp = None
        try:
            fp, pathname, description = imp.find_module("course_settings", [data_dir])
            mod = imp.load_module("course_settings", fp, pathname, description)
        except Exception as e:
            _log.exception("Unable to import course settings file from " + data_dir + ". Error: " + str(e))
            mod = types.ModuleType('course_settings')
        finally:
            if fp:
                fp.close()
                
        for setting in dir(mod):
            if setting == setting.upper():
                setting_value = getattr(mod, setting)
                setattr(self, setting, setting_value)
                
        # Here is where we should parse any configurations, so that we can fail early
        self.GRADER = graders.grader_from_conf(self.GRADER)

course_settings = Settings()




def grade_sheet(student,coursename=None):
    """
    This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
    
    - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters,
    each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded
    problems, and is good for displaying a course summary with due dates, etc.
    
    - grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
    """
    dom=content_parser.course_file(student,coursename)
    course = dom.xpath('//course/@name')[0]
    xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)

    responses=StudentModule.objects.filter(student=student)
    response_by_id = {}
    for response in responses:
        response_by_id[response.module_id] = response
    
    
    totaled_scores = {}
    chapters=[]
    for c in xmlChapters:
        sections = []
        chname=c.get('name')
        
        
        for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section', 
                           course=course, chname=chname):
            problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem', 
                           course=course, chname=chname, section=s.get('name'))

            graded = True if s.get('graded') == "true" else False
            scores=[]
            if len(problems)>0:
                for p in problems:
                    (correct,total) = get_score(student, p, response_by_id, coursename=coursename)
                    
                    if settings.GENERATE_PROFILE_SCORES:
                        if total > 1:
                            correct = random.randrange( max(total-2, 1) , total + 1 )
                        else:
                            correct = total
                    
                    if not total > 0:
                        #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                        graded = False 
                    scores.append( Score(correct,total, graded, p.get("name")) )

                section_total, graded_total = aggregate_scores(scores, s.get("name"))
                #Add the graded total to totaled_scores
                format = s.get('format', "")
                subtitle = s.get('subtitle', format)
                if format and graded_total[1] > 0:
                    format_scores = totaled_scores.get(format, [])
                    format_scores.append( graded_total )
                    totaled_scores[ format ] = format_scores

                section_score={'section':s.get("name"),
                       'scores':scores,
                       'section_total' : section_total,
                       'format' : format,
                       'subtitle' : subtitle,
                       'due' : s.get("due") or "",
                       'graded' : graded,
                       }
                sections.append(section_score)

        chapters.append({'course':course,
                         'chapter' : c.get("name"),
                         'sections' : sections,})
    
        
    grader = course_settings.GRADER
    grade_summary = grader.grade(totaled_scores)
    
    return {'courseware_summary' : chapters,
            'grade_summary' : grade_summary}

def aggregate_scores(scores, section_name = "summary"):    
    total_correct_graded = sum(score.earned for score in scores if score.graded)
    total_possible_graded = sum(score.possible for score in scores if score.graded)
    
    total_correct = sum(score.earned for score in scores)
    total_possible = sum(score.possible for score in scores)
        
    #regardless of whether or not it is graded
    all_total = Score(total_correct, 
                          total_possible,
                          False,
                          section_name)
    #selecting only graded things
    graded_total = Score(total_correct_graded, 
                         total_possible_graded, 
                         True, 
                         section_name)

    return all_total, graded_total
    

def get_score(user, problem, cache, coursename=None):
    ## HACK: assumes max score is fixed per problem
    id = problem.get('id')
    correct = 0.0
    
    # If the ID is not in the cache, add the item
    if id not in cache:
        module = StudentModule(module_type = 'problem',  # TODO: Move into StudentModule.__init__?
                               module_id = id,
                               student = user, 
                               state = None, 
                               grade = 0,
                               max_grade = None,
                               done = 'i')
        cache[id] = module

    # Grab the # correct from cache
    if id in cache:
        response = cache[id]
        if response.grade!=None:
            correct=float(response.grade)
        
    # Grab max grade from cache, or if it doesn't exist, compute and save to DB
    if id in cache and response.max_grade != None:
        total = response.max_grade
    else:
        ## HACK 1: We shouldn't specifically reference capa_module
        ## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
        from module_render import I4xSystem
        system = I4xSystem(None, None, None, coursename=coursename)
        total=float(courseware.modules.capa_module.Module(system, etree.tostring(problem), "id").max_score())
        response.max_grade = total
        response.save()
        
    #Now we re-weight the problem, if specified
    weight = problem.get("weight", None)
    if weight:
        weight = float(weight) 
        correct = correct * weight / total
        total = weight        

    return (correct, total)
