#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>   /* for malloc(),etc */
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#include "../global.h"

#include "../xmlparse.h"  /* header for libexpat.a */

#include "shared_util.h"  /* for cs_critical_error() */
#include "shared_cs_util.h"  /* for cs_critical_error() */
#include "shared_hotpot_parser.h"  /* data types for this module */


QUIZ_DATA quiz_data;
char stored_text[MAX_TEXT];
char hotpot_answer_letters[MAX_HOTPOT_CHOICES + 1] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";



/** Hot Potatoes 6 uses a hexadecimal escape sequence to represent 
*** < and >, while Hot Potatoes 5.x uses &lt; and &gt;
***
*** This parser needs to 'unescape' these character sequences,
*** which start with the following string of characters:
**/
#define ESCAPE_CHARS "&#x0"


static void
get_tag(const char *name, TAGS *tag)
{
 if(!strcasecmp(name,"title"))
   *tag = TITLE;
 else
  if(!strcasecmp(name,"instructions"))  
    *tag = INSTRUCTIONS;
 else
  if(!strcasecmp(name,"exercise-subtitle"))  
    *tag = EXERCISE_SUBTITLE;
 else
  if(!strcasecmp(name,"case-sensitive"))  
    *tag = CASE_SENSITIVE;
 else
  if(!strcasecmp(name,"question"))  
    *tag = QUESTION;
 else
  if(!strcasecmp(name,"must-select-all"))
    *tag= MUST_SELECT_ALL;
 else
  if(!strcasecmp(name,"text"))
    *tag= TEXT;
 else
  if(!strcasecmp(name,"correct"))
    *tag= CORRECT;
 else
  if(!strcasecmp(name,"feedback"))
    *tag= FEEDBACK;
 else
  if(!strcasecmp(name,"page-bg-color"))
    *tag= PG_BG_COLOR;
 else
  if(!strcasecmp(name,"ex-bg-color"))
    *tag= EX_BG_COLOR;
 else
  if(!strcasecmp(name,"text-color"))
    *tag= TEXT_COLOR;
 else
  if(!strcasecmp(name,"title-color"))
    *tag= TITLE_COLOR;
 else
  if(!strcasecmp(name,"link-color"))
    *tag= LINK_COLOR;
 else
  if(!strcasecmp(name,"vlink-color"))
    *tag= VLINK_COLOR;
 else
  if(!strcasecmp(name,"font-face"))
    *tag= FONT_FACE;
 else
  if(!strcasecmp(name,"jquiz"))
    *tag= JQZ_QUIZ;
 else
  if(!strcasecmp(name,"hotpot-jbc-file"))
    *tag= JBC_QUIZ;
 else
   if(!strcasecmp(name, "version"))
    *tag = HOTPOT_VERSION;   
 else
   if(!strcasecmp(name, "question-type"))
    *tag = QUES_TYPE;   
 else
   if(!strcasecmp(name, "include-in-mc-options"))
    *tag = INCLUDE_IN_MC_OPTIONS;   
 else
    *tag= UNUSED;
}    
    
    
    

/** If string starts with a Hot Potatoes 6
*** escape sequence of the form:
***
***   &#003C;
***
*** then this function:
***    returns 1
***    sets unescaped_char to the character represented
***       by the sequence
***    returns in escape_len, the the length of the
***       escape sequence.
***
*** This is really necessary because Hot Potatoes
*** version 6 writes:
*** 
***  &#003C;   when it should write &lt;
***  &#003E;   when it should write &gt;
***
***
***  WARNING: this modifies 'string'
***
**/


static
int unescape(char *string, char *unescaped_char, int *escape_len)
{
 char *start_escape;
 char *end_escape;
 
 
 /* if string doesn't start with ESCAPE_CHARS, 
 ** return 0
 */
 start_escape = strstr(string, ESCAPE_CHARS);
 if(!start_escape  || (start_escape != string))
    return 0;
   
   
 /* string must also contain a semi-colon
 ** for it to be a valid escape sequence
 */    
 end_escape = strchr(string,';');
 if(!end_escape)
    return 0;
    
   
   
 *(string + 1) = '0';    /* change "&#x00" to &0x00" because strol() requires it   */
 *unescaped_char = (char)strtol(string + 1, &end_escape, 16);  /* note we skip leading & */
 

 /* yet another test */
 if(*end_escape != ';')
    return 0;   
    
 /* how many characters made up the escape sequence */   
 *escape_len = (int) (end_escape - start_escape + 1);
 return 1;  /* string started with an escape sequence */
}



    
        
static void
store_string (char **dest)
{

 int escape_len;
 int len;
 char unescaped_char;
 int dest_x = 0, source_x = 0;
 
 len = strlen(stored_text) + 1;
 *dest = (char *)malloc(len * sizeof(char));
 
 if(!strstr(stored_text,ESCAPE_CHARS))
    strncpy(*dest,stored_text,len);
 else
 {
   while(source_x < len)
    {
     if(unescape( stored_text + source_x, &unescaped_char, &escape_len))
       {
        *(*dest + dest_x) = unescaped_char;
        dest_x++;
        source_x += escape_len;
       }  
     else
      {
       *(*dest + dest_x) = stored_text[source_x];
       dest_x++;
       source_x++;
      }
   }       
 } 
 
} 
 
 
 
 


static 
void new_question()
{
 QUESTION_NODE *ptr;

 
 if(!quiz_data.last_question)
  {
   quiz_data.head = (QUESTION_NODE *)malloc(sizeof(QUESTION_NODE));
   ptr = quiz_data.head;
  }
 else
  {
   quiz_data.last_question->next = (QUESTION_NODE *)malloc(sizeof(QUESTION_NODE));
   ptr = quiz_data.last_question->next;
  }
 
  store_string(&(ptr->question));
  
  ptr->response =  0;   /* no answer yet from student */
  
  switch(quiz_data.type)
  {
   case JBC: ptr->question_type = MC; break;                     /* ver 5 jbc is mc, 
                                                                 ** but might be changed to mc-multi later if
                                                                 ** must-select-all is set
                                                                 **/

   case JQZ: ptr->question_type = SHORT; break;                  /* ver 5 jqz is always short answer */

   default:  ptr->question_type = UNKNOWN_QUESTION_TYPE; break;  /* for ver 6, we have to read the 
                                                                 **question-type later
                                                                 */
  } 
  
  ptr->correct_answer_string = (char *) 0;
  ptr->correct_answer_count = 0;     
  ptr->answer_correct = 0;          /* don't know if student answered this correctly */
  ptr->answer_count = 0;
  ptr->head = (ANSWER_NODE *) 0;
  ptr->last_answer = (ANSWER_NODE *) 0;
  ptr->next = (QUESTION_NODE *) 0;
  quiz_data.last_question = ptr;
}   
        

static void new_answer()
{
 ANSWER_NODE *ptr;

 
 if(!quiz_data.last_question)
     return;   /* should never happen */
     
 if(!quiz_data.last_question->last_answer)
  {
   quiz_data.last_question->head = (ANSWER_NODE *)malloc(sizeof(ANSWER_NODE));
   ptr = quiz_data.last_question->head;
  }
 else
  {
   quiz_data.last_question->last_answer->next = (ANSWER_NODE *)malloc(sizeof(ANSWER_NODE));
   ptr = quiz_data.last_question->last_answer->next;
  }
 
  ptr->is_correct = 0;
  ptr->feedback = 0;
  ptr->next = (ANSWER_NODE *) 0;
  quiz_data.last_question->last_answer = ptr;
}   



/** the must-select-all tag should only appear in 
*** HOTPOT 5 JBC tests.  If this value is one,
*** and this is a HOTPOT 5 JBC quiz then change
*** the question type to MC-MULTI
**/

static void
must_select_all()
{
 int value;
 
 value = atoi(stored_text);
 
 if((value == 1) && (quiz_data.last_question) && 
           ((quiz_data.type == JBC) || (quiz_data.last_question->question_type == UNKNOWN_QUESTION_TYPE)) )
			   quiz_data.last_question->question_type = MC_MULTI;
}  



/* the question-type tag is new to HotPot 6 the values are:
** 1 - straight multiple-choice
** 2 - short answer
** 3 - 'hybrid' - Manhattan switches this to straight multiple-choice
** 4 - multi-select (multiple-choice where user has to select all of the correct answers 
**/

static void
question_type()
{
 int value;
 

 value = atoi(stored_text);
 

 if(value && (quiz_data.last_question)) 
 {
  switch(value)
   {
    case 1: quiz_data.last_question->question_type = MC; break;
    
    case 2: quiz_data.last_question->question_type = SHORT; break;
    
    case 3: quiz_data.last_question->question_type = MC; break;  /* hotpot 6 hybrid becomes multiple-choice */
    
    case 4: quiz_data.last_question->question_type = MC_MULTI; break;
    
    default: quiz_data.last_question->question_type = UNKNOWN_QUESTION_TYPE; break;
   }
   

 }  
}    
 
 
/* include_in_mc_options is new to hotpot 6
** it should only be '0' for JQZ6 quizzes, and multiple
** choice questions.  If it is found to be zero, clear out
** the answer field, so we can skip it when it is presented
*/    
    
static void
include_in_mc_options()
{
 int value;
 
 value = atoi(stored_text);
 
 if(!value &&
    (quiz_data.last_question) && 
    (quiz_data.last_question->question_type == MC) && 
    (quiz_data.last_question->last_answer))
      if(quiz_data.last_question->last_answer->answer)
          *(quiz_data.last_question->last_answer->answer) = '\0';
          
}      
      



static void
answer_text()
{

 
 if(quiz_data.last_question && quiz_data.last_question->last_answer)
   store_string(&(quiz_data.last_question->last_answer->answer));  
}


static void
correct()
{
 
 if(quiz_data.last_question && quiz_data.last_question->last_answer)
   quiz_data.last_question->last_answer->is_correct = atoi(stored_text);
}

static void
feedback()
{
 
 if(quiz_data.last_question && quiz_data.last_question->last_answer)
   store_string(&(quiz_data.last_question->last_answer->feedback));  
}


static void
clear_stored_text()
{
 if(*stored_text)
   memset(stored_text, 0, MAX_TEXT);
}   

void startElement(void *userData, const char *name, const char **atts)
{
  if(!strcasecmp(name,"answer"))
       new_answer();
  clear_stored_text();
}

static
void jbc_quiz()
{
 QUESTION_NODE *qptr;

 quiz_data.type = JBC;
 for(qptr=quiz_data.head; qptr; qptr=qptr->next)
     if(qptr->question_type == UNKNOWN_QUESTION_TYPE)
         qptr->question_type = MC;
}


         

static
void jqz_quiz()
{
 QUESTION_NODE *qptr;
 
  quiz_data.type = (quiz_data.version && strchr(quiz_data.version,'6'))?  JQZ6: JQZ;
  if(quiz_data.type == JQZ)
   {
    for(qptr=quiz_data.head; qptr; qptr=qptr->next)
     if(qptr->question_type == UNKNOWN_QUESTION_TYPE)
         qptr->question_type = SHORT;
   }
}         
  
  
  

void endElement(void *userData, const char *name)
{

 TAGS tag;
 
 get_tag(name,&tag);
 switch(tag)
 {
  case TITLE:        		store_string(&quiz_data.title);   		break;
  case INSTRUCTIONS: 		store_string(&(quiz_data.instructions)); 	break;
  case EXERCISE_SUBTITLE:   	store_string(&(quiz_data.sub_title));		break;
  case CASE_SENSITIVE:    	quiz_data.case_sensitive = atoi(stored_text);   break;
  case QUESTION:                new_question();  				break;
  case MUST_SELECT_ALL:         must_select_all();                              break;
  case TEXT:                    answer_text();                                  break;
  case CORRECT:                 correct();   	 				break;
  case FEEDBACK:                feedback();	                                break;
  case PG_BG_COLOR:             store_string(&(quiz_data.pg_bg_color));		break;
  case EX_BG_COLOR:             store_string(&(quiz_data.ex_bg_color));		break;
  case TITLE_COLOR:		store_string(&(quiz_data.title_color));		break;
  case TEXT_COLOR:		store_string(&(quiz_data.text_color));		break;
  case LINK_COLOR:		store_string(&(quiz_data.link_color));		break;
  case VLINK_COLOR:		store_string(&(quiz_data.vlink_color));		break;
  case FONT_FACE:		store_string(&(quiz_data.font_face));		break;
  case JBC_QUIZ:                jbc_quiz();                                     break;
  case JQZ_QUIZ:                jqz_quiz();                                    break;
  case HOTPOT_VERSION:          store_string(&(quiz_data.version));             break;
  case QUES_TYPE:               question_type();                                break;
  case INCLUDE_IN_MC_OPTIONS:   include_in_mc_options();                        break;
  
  default:                      break;
 }

 clear_stored_text();
}                                     		              
                                                                    		                                 


void char_handler(void *userData, const char *txt, int len)
{

  strncat(stored_text,txt,len);
}


static void
init_quiz_data()
{
 quiz_data.type = UNKNOWN_QUIZ;
 quiz_data.version = 0;
 quiz_data.title = 0;
 quiz_data.sub_title = 0;
 quiz_data.instructions = 0;
 quiz_data.case_sensitive = 0;
 quiz_data.pg_bg_color = 0;
 quiz_data.ex_bg_color = 0;
 quiz_data.title_color = 0;
 quiz_data.text_color = 0;
 quiz_data.link_color = 0;
 quiz_data.vlink_color = 0;
 quiz_data.font_face = 0;
 
 
 
 quiz_data.head = 0;
 quiz_data.last_question = 0;
 clear_stored_text();
}




static
void free_answer_node(ANSWER_NODE *aptr)
{
     if(aptr->answer)
      {
       free(aptr->answer);
       aptr->answer = 0;
      } 
     if(aptr->feedback)
      {
       free(aptr->feedback);
       aptr->feedback = 0;
      } 
     free(aptr);
}
 
 

static 
void free_question_node(QUESTION_NODE *qptr)
{
 ANSWER_NODE *aptr;

   if(qptr->question)
    {
      free(qptr->question);
      qptr->question = 0;
    }  

   if(qptr->response)
    {
      free(qptr->response);
      qptr->response = 0;
    }  
    
   if(qptr->correct_answer_string)
    {
     free(qptr->correct_answer_string);
     qptr->correct_answer_string = 0;
    }
      
   
   while(qptr->head)
    {
     aptr = qptr->head;
     qptr->head = qptr->head->next;
     free_answer_node(aptr);

    }
    free(qptr);
}
 





void free_quiz_data()
{
 QUESTION_NODE *qptr;

 
 if(quiz_data.title)
     free(quiz_data.title);
 if(quiz_data.version)
     free(quiz_data.version);    
 if(quiz_data.sub_title)
     free(quiz_data.sub_title);
 if(quiz_data.pg_bg_color)
     free(quiz_data.pg_bg_color);
 if(quiz_data.pg_bg_color)
     free(quiz_data.ex_bg_color);
 if(quiz_data.title_color)
     free(quiz_data.title_color);
 if(quiz_data.text_color)
     free(quiz_data.text_color);
 if(quiz_data.link_color)
     free(quiz_data.link_color);
 if(quiz_data.vlink_color)
     free(quiz_data.vlink_color);
 if(quiz_data.font_face)
     free(quiz_data.font_face);
                                 
 while(quiz_data.head)
  {
   qptr = quiz_data.head;
   quiz_data.head = quiz_data.head->next;
   
   free_question_node(qptr);
  }      
}             





static
int string_is_empty(char *string)
{
char *ptr;

 if(!string)
    return 1;
 
 for(ptr=string; *ptr; ptr++)
   if(!isspace(*ptr))
      return 0;
      
 return 1;
}







/** Removes the question node 'target' 
*** Returns a pointer to the next answer node after 
*** the deleted target.
**/ 
          
    
QUESTION_NODE *delete_question(QUESTION_NODE *target)
{

QUESTION_NODE *next_qptr;
QUESTION_NODE *qptr;

 next_qptr = target->next;    /* value to be returned */
 
 if(quiz_data.head == target)
     quiz_data.head = target->next; 
 else
  {
   for(qptr = quiz_data.head; qptr && (qptr->next != target); qptr=qptr->next);
   if(qptr)
      qptr->next = target->next;
  }
 free_question_node(target);
 return next_qptr;
}





/** Removes the answer node 'target' from 'question'
*** Returns a pointer to the next answer node after 
*** the deleted target.
**/ 

static
ANSWER_NODE *delete_answer(QUESTION_NODE *question, ANSWER_NODE *target)
{
 ANSWER_NODE *next_aptr;
 ANSWER_NODE *aptr;

 next_aptr = target->next;    /* value to be returned */
 
 if(question->head == target)
     question->head = target->next; 
 else
  {
   for(aptr = question->head; aptr && (aptr->next != target); aptr=aptr->next);
   if(aptr)
      aptr->next = target->next;
  }
 free_answer_node(target);
 return next_aptr;
}




/** Deletes empty answers for question pointed to
*** by 'target'.  An answer is empty if it has no
*** text.
**/

static void
delete_empty_answers(QUESTION_NODE *target)
{
 ANSWER_NODE *aptr;
  
 aptr = target->head;
 while(aptr)
  {
   aptr = string_is_empty(aptr->answer)? delete_answer(target,aptr) : aptr->next;
  }
}
  



/** HotPot 6 (and possibly earlier versions) often stores empty questions
*** and answers - questions and answers with no text - in the XML file
*** The next function removes all empty questions and answers from the list.
***
*** An answer is empty if it has no text.  A question is empty if it has
*** no text, or if it has no answers
**/

static void
delete_empty_questions()
{
 QUESTION_NODE *qptr;
 
 qptr= quiz_data.head;
 while(qptr)
  {
   delete_empty_answers(qptr);
   qptr = string_is_empty(qptr->question) || !(qptr->head)? delete_question(qptr) : qptr->next;
  }
}



static 
void set_correct_answer_string(QUESTION_NODE *q_ptr)
{
 ANSWER_NODE *a_ptr;
 int a;
 int n_correct;
 
 
  q_ptr->correct_answer_string = (char *)malloc((q_ptr->correct_answer_count + 1) * sizeof(char));
  if(!q_ptr->correct_answer_string)
     cs_critical_error(ERR_MALLOC_FAILED, "");
  *(q_ptr->correct_answer_string + q_ptr->correct_answer_count) = '\0';
      
  for(a_ptr = q_ptr->head, n_correct = 0, a=0; a_ptr; a_ptr=a_ptr->next,a++)
   {
    if(a_ptr->is_correct)
      {
       if(a >= MAX_HOTPOT_CHOICES)   /* shared_hotpot_parser.h */
           break;
       *(q_ptr->correct_answer_string + n_correct) = hotpot_answer_letters[a];   /* shared_hotpot_parser.h */
       n_correct++;
      }
   }   
}





static void
set_answer_counts(QUESTION_NODE *q_ptr)
{
 ANSWER_NODE *a_ptr;
 
  q_ptr->correct_answer_count = 0;
  q_ptr->answer_count = 0;
 
  for(a_ptr = q_ptr->head; a_ptr; a_ptr=a_ptr->next, q_ptr->answer_count++)
   if(a_ptr->is_correct)
       q_ptr->correct_answer_count++;
}       
       



static
void count_answers()
{
 QUESTION_NODE *qptr;
 
 for(qptr= quiz_data.head; qptr; qptr=qptr->next)
 {
   set_answer_counts(qptr);
   if( (qptr->question_type == MC) ||
      (qptr->question_type == MC_MULTI))
         set_correct_answer_string(qptr);
 }
           
}






   

int parse_hotpot_file(const char *path)
{
  char *jbc_buf;
  XML_Parser parser; 
  struct stat buf;
  FILE *fp;
  
  if(stat(path,&buf))
    cs_critical_error(ERR_STAT_FAILED,"");
 
  jbc_buf = (char *)malloc(buf.st_size);
  if(!jbc_buf)
      cs_critical_error(ERR_MALLOC_FAILED,"");
  fp = fopen(path,"r");
  if(!fp)
    cs_critical_error(ERR_FOPEN_READ_FAILED,"");
  if(fread(jbc_buf,buf.st_size,1,fp) != 1)
     cs_critical_error(ERR_FREAD_FAILED, "");         
  fclose(fp);
  
  init_quiz_data();
  parser = XML_ParserCreate(NULL);
  
  XML_SetElementHandler(parser, startElement, endElement);
  XML_SetCharacterDataHandler(parser,char_handler);

 if (!XML_Parse(parser, jbc_buf, buf.st_size, 1)) 
    // cs_critical_error( XML_ErrorString(XML_GetErrorCode(parser)),"");
    return 1;

  XML_ParserFree(parser);
  free(jbc_buf);
  
  delete_empty_questions();
  count_answers();
  return 0;
}



