import StringIO
import datetime
import dateutil
import dateutil.parser
import json
import logging
import math
import numpy
import os
import random
import scipy
import struct
import sys
import traceback

from lxml import etree

## TODO: Abstract out from Django
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from django.http import Http404

from x_module import XModule
from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
import courseware.content_parser as content_parser

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

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, complex):
            return "{real:.7g}{imag:+.7g}*j".format(real = obj.real,imag = obj.imag)
        return json.JSONEncoder.default(self, obj)

class Module(XModule):
    ''' Interface between capa_problem and x_module. Originally a hack
    meant to be refactored out, but it seems to be serving a useful
    prupose now. We can e.g .destroy and create the capa_problem on a
    reset. 
    '''

    id_attribute = "filename"

    @classmethod
    def get_xml_tags(c):
        return ["problem"]

    def get_state(self):
        state = self.lcp.get_state()
        state['attempts'] = self.attempts
        return json.dumps(state)

    def get_score(self):
        return self.lcp.get_score()

    def max_score(self):
        return self.lcp.get_max_score()

    def get_html(self):
        return render_to_string('problem_ajax.html', 
                              {'id':self.item_id, 
                               'ajax_url':self.ajax_url,
                               })

    def get_init_js(self):
        return render_to_string('problem.js', 
                              {'id':self.item_id, 
                               'ajax_url':self.ajax_url,
                               })

    def get_problem_html(self, encapsulate=True):
        html = self.lcp.get_html()
        content={'name':self.name, 
                 'html':html, 
                 'weight': self.weight,
                 }
        
        # We using strings as truthy values, because the terminology of the check button
        # is context-specific.
        check_button = "Grade" if self.max_attempts else "Check"
        reset_button = True
        save_button = True

        # If we're after deadline, or user has exhuasted attempts, 
        # question is read-only. 
        if self.closed():
            check_button = False
            reset_button = False
            save_button = False
            

        # User submitted a problem, and hasn't reset. We don't want
        # more submissions. 
        if self.lcp.done and self.rerandomize == "always":
            #print "!"
            check_button = False
            save_button = False
        
        # User hasn't submitted an answer yet -- we don't want resets
        if not self.lcp.done:
            reset_button = False

        # We don't need a "save" button if infinite number of attempts and non-randomized
        if self.max_attempts == None and self.rerandomize != "always":
            save_button = False

        # Check if explanation is available, and if so, give a link
        explain=""
        if self.lcp.done and self.explain_available=='attempted':
            explain=self.explanation
        if self.closed() and self.explain_available=='closed':
            explain=self.explanation
        
        if len(explain) == 0:
            explain = False

        html=render_to_string('problem.html', 
                              {'problem' : content, 
                               'id' : self.item_id, 
                               'check_button' : check_button,
                               'reset_button' : reset_button,
                               'save_button' : save_button,
                               'answer_available' : self.answer_available(),
                               'ajax_url' : self.ajax_url,
                               'attempts_used': self.attempts, 
                               'attempts_allowed': self.max_attempts, 
                               'explain': explain
                               })
        if encapsulate:
            html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
            
        return html

    def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None):
        XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function)

        self.attempts = 0
        self.max_attempts = None
        
        dom2 = etree.fromstring(xml)
        
        self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed")
        self.explain_available=content_parser.item(dom2.xpath('/problem/@explain_available'))

        display_due_date_string=content_parser.item(dom2.xpath('/problem/@due'))
        if len(display_due_date_string)>0:
            self.display_due_date=dateutil.parser.parse(display_due_date_string)
            #log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date))
        else:
            self.display_due_date=None
        
        
        grace_period_string = content_parser.item(dom2.xpath('/problem/@graceperiod'))
        if len(grace_period_string)>0 and self.display_due_date:
            self.grace_period = content_parser.parse_timedelta(grace_period_string)
            self.close_date = self.display_due_date + self.grace_period
            #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date))
        else:
            self.grace_period = None
            self.close_date = self.display_due_date
            
        self.max_attempts=content_parser.item(dom2.xpath('/problem/@attempts'))
        if len(self.max_attempts)>0:
            self.max_attempts=int(self.max_attempts)
        else:
            self.max_attempts=None

        self.show_answer=content_parser.item(dom2.xpath('/problem/@showanswer'))

        if self.show_answer=="":
            self.show_answer="closed"

        self.rerandomize=content_parser.item(dom2.xpath('/problem/@rerandomize'))
        if self.rerandomize=="" or self.rerandomize=="always" or self.rerandomize=="true":
            self.rerandomize="always"
        elif self.rerandomize=="false" or self.rerandomize=="per_student":
            self.rerandomize="per_student"
        elif self.rerandomize=="never":
            self.rerandomize="never"
        else:
            raise Exception("Invalid rerandomize attribute "+self.rerandomize)

        if state!=None:
            state=json.loads(state)
        if state!=None and 'attempts' in state:
            self.attempts=state['attempts']

        self.filename=content_parser.item(dom2.xpath('/problem/@filename'))
        filename=settings.DATA_DIR+"/problems/"+self.filename+".xml"
        self.name=content_parser.item(dom2.xpath('/problem/@name'))
        self.weight=content_parser.item(dom2.xpath('/problem/@weight'))
        if self.rerandomize == 'never':
            seed = 1
        else:
            seed = None
        self.lcp=LoncapaProblem(filename, self.item_id, state, seed = seed)

    def handle_ajax(self, dispatch, get):
        if dispatch=='problem_get':
            response = self.get_problem(get)
        elif False: #self.close_date > 
            return json.dumps({"error":"Past due date"})
        elif dispatch=='problem_check': 
            response = self.check_problem(get)
        elif dispatch=='problem_reset':
            response = self.reset_problem(get)
        elif dispatch=='problem_save':
            response = self.save_problem(get)
        elif dispatch=='problem_show':
            response = self.get_answer(get)
        else: 
            return "Error"
        return response

    def closed(self):
        ''' Is the student still allowed to submit answers? '''
        if self.attempts == self.max_attempts:
            return True
        if self.close_date != None and datetime.datetime.utcnow() > self.close_date:
            return True

        return False
        

    def answer_available(self):
        ''' Is the user allowed to see an answer? 
        ''' 
        if self.show_answer == '':
            return False
        if self.show_answer == "never":
            return False
        if self.show_answer == 'attempted' and self.attempts == 0:
            return False
        if self.show_answer == 'attempted' and self.attempts > 0:
            return True
        if self.show_answer == 'answered' and self.lcp.done:
            return True
        if self.show_answer == 'answered' and not self.lcp.done:
            return False
        if self.show_answer == 'closed' and self.closed():
            return True
        if self.show_answer == 'closed' and not self.closed():
            return False
        print "aa", self.show_answer
        raise Http404

    def get_answer(self, get):
        if not self.answer_available():
            raise Http404
        else: 
            return json.dumps(self.lcp.get_question_answers(), 
                              cls=ComplexEncoder)


    # Figure out if we should move these to capa_problem?
    def get_problem(self, get):
        ''' Same as get_problem_html -- if we want to reconfirm we
            have the right thing e.g. after several AJAX calls.'''
        return self.get_problem_html(encapsulate=False)        

    def check_problem(self, get):
        ''' Checks whether answers to a problem are correct, and
            returns a map of correct/incorrect answers'''
        event_info = dict()
        event_info['state'] = self.lcp.get_state()
        event_info['filename'] = self.filename

        answers=dict()
        # input_resistor_1 ==> resistor_1
        for key in get:
            answers['_'.join(key.split('_')[1:])]=get[key]

#        print "XXX", answers, get

        event_info['answers']=answers

        # Too late. Cannot submit
        if self.closed():
            event_info['failure']='closed'
            self.tracker('save_problem_check_fail', event_info)
            print "cp"
            raise Http404
            
        # Problem submitted. Student should reset before checking
        # again.
        if self.lcp.done and self.rerandomize == "always":
            event_info['failure']='unreset'
            self.tracker('save_problem_check_fail', event_info)
            print "cpdr"
            raise Http404

        try:
            old_state = self.lcp.get_state()
            lcp_id = self.lcp.problem_id
            filename = self.lcp.filename
            correct_map = self.lcp.grade_answers(answers)
        except StudentInputError as inst: 
            self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state)
            traceback.print_exc()
#            print {'error':sys.exc_info(),
#                   'answers':answers, 
#                   'seed':self.lcp.seed, 
#                   'filename':self.lcp.filename}
            return json.dumps({'success':inst.message})
        except: 
            self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state)
            traceback.print_exc()
            return json.dumps({'success':'Unknown Error'})
            

        self.attempts = self.attempts + 1
        self.lcp.done=True

        success = 'correct'
        for i in correct_map:
            if correct_map[i]!='correct':
                success = 'incorrect'

        js=json.dumps({'correct_map' : correct_map,
                       'success' : success})

        event_info['correct_map']=correct_map
        event_info['success']=success

        self.tracker('save_problem_check', event_info)

        return js

    def save_problem(self, get):
        event_info = dict()
        event_info['state'] = self.lcp.get_state()
        event_info['filename'] = self.filename

        answers=dict()
        for key in get:
            answers['_'.join(key.split('_')[1:])]=get[key]
        event_info['answers'] = answers

        # Too late. Cannot submit
        if self.closed():
            event_info['failure']='closed'
            self.tracker('save_problem_fail', event_info)
            return "Problem is closed"
            
        # Problem submitted. Student should reset before saving
        # again.
        if self.lcp.done and self.rerandomize == "always":
            event_info['failure']='done'
            self.tracker('save_problem_fail', event_info)
            return "Problem needs to be reset prior to save."

        self.lcp.student_answers=answers

        self.tracker('save_problem_fail', event_info)
        return json.dumps({'success':True})

    def reset_problem(self, get):
        ''' Changes problem state to unfinished -- removes student answers, 
            and causes problem to rerender itself. '''
        event_info = dict()
        event_info['old_state']=self.lcp.get_state()
        event_info['filename']=self.filename

        if self.closed():
            event_info['failure']='closed'
            self.tracker('reset_problem_fail', event_info)
            return "Problem is closed"
            
        if not self.lcp.done:
            event_info['failure']='not_done'
            self.tracker('reset_problem_fail', event_info)
            return "Refresh the page and make an attempt before resetting."

        self.lcp.done=False
        self.lcp.answers=dict()
        self.lcp.correct_map=dict()
        self.lcp.student_answers = dict()


        if self.rerandomize == "always":
            self.lcp.context=dict()
            self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. 
            self.lcp.seed=None

        filename=settings.DATA_DIR+"problems/"+self.filename+".xml"
        self.lcp=LoncapaProblem(filename, self.item_id, self.lcp.get_state())

        event_info['new_state']=self.lcp.get_state()
        self.tracker('reset_problem', event_info)

        return json.dumps(self.get_problem_html(encapsulate=False))
