/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

====================================================================== */

package org.bodington.assessment.ims;

import org.apache.log4j.Logger;



import org.bodington.assessment.*;
import org.bodington.assessment.ims.*;
import org.bodington.logging.LoggingUtils;
import org.bodington.server.*;

import java.io.*;

import org.xml.sax.*;


import javax.xml.parsers.ParserConfigurationException;  
import javax.xml.parsers.DocumentBuilderFactory;  
import javax.xml.parsers.FactoryConfigurationError;  
import javax.xml.parsers.DocumentBuilder;


import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.CharacterData;
import org.w3c.dom.NodeList;
import org.w3c.dom.DOMException;


 public class McqImsQtiHandler extends org.bodington.assessment.ims.ImsQtiHandler
 {
    private static Logger log = Logger.getLogger(ImsQtiHandler.class);
	public McqImsQtiHandler(XMLReader reader)
	    {
		super(reader);
	    }
    
    // 0 indicates that the statement no present in expression
    // 1 indicates it is present and can make the final result true
    // -1 indicates it is present and can make the final result false
    private int evaluateboolean( Element container, String ident, String value )
        throws Exception
        {
        log.debug( "Evaluate Boolean ident = "  + ident + 
                            " value = " + value + 
                            " nodename = " + container.getNodeName() );
        Node node;
        CharacterData cdata;
        NodeList list;
        int ret = 0;
        
        // true if this element matches the selected statement in the question
        if ( container.getNodeName().equals( "varequal" ) )
            {
            log.debug( "varequal respident = " + container.getAttribute( "respident" ) );
            if ( container.getAttribute( "respident" ).equals( ident ) )
                {
                cdata = (CharacterData)container.getFirstChild();
                if ( cdata != null )
                    {
                    log.debug( "varequal text = " + cdata.getData() );
                    if ( cdata.getData().equals( value ) )
                        ret = 1;
                    }
                /*
                list = container.getElementsByTagName( "#text" );
                if ( list!=null && list.getLength()==1 )
                    {
                    node = list.item( 0 );
                    if ( node!=null )
                        {
                        Logger.getLogger( "org.bodington" ).fine( "varequal text = " + ((CharacterData)node).getData() );
                        b = ((CharacterData)node).getData().equals( "value" );
                        }
                    }
                */
                }
            }
        // we aren;t running the logic to mark a response - just to find
        // statements that MIGHT give the student a mark given appropriate
        // other selection so all of the following three options are treated
        // as "OR"s.
        else if ( container.getNodeName().equals( "conditionvar" ) || 
                  container.getNodeName().equals( "not" ) || 
                  container.getNodeName().equals( "and" ) ||
                  container.getNodeName().equals( "or" ))
            {
            list = container.getChildNodes();
            for ( int i=0; i< list.getLength(); i++ )
                {
                node = list.item( i );
                if ( node.getNodeType() == Node.ELEMENT_NODE )
                    {
                    int b = evaluateboolean( (Element)node, ident, value );
                    if ( b == 1 || ret == 1 )
                        ret = 1;
                    else
                        if ( b == -1 || ret == -1 )
                            ret = -1;
                    }
                }
                
            // not "ors" the contents like other ops but then reverses the logic
            if ( container.getNodeName().equals( "not" ) )
                ret = -ret;
            }
        else
            {
            throw new Exception( "Scoring definition too complex." );
            }

        log.debug( "returned " + ret );
        return ret;
        }
    
    Object parseQuestion( Document document )
        throws SAXException, BuildingServerException
        {
        int i, j, k, l;
        
        try
            {
        	// itemmetadata/qmd_itemtype element contains Bod question type:
        	String item_type = null;
        	NodeList list = document.getElementsByTagName( "qmd_itemtype" );
        	if ( null != list && list.getLength() > 0 )
        	{
        		Element qmd_itemtype = (Element)list.item(0);
        		item_type = qmd_itemtype.getTextContent();
        	}
        	
        	
            list = document.getElementsByTagName( "presentation" );
            if ( list==null || list.getLength()!=1 )
                throw new SAXException( "File format error" );
            Node presentation = list.item( 0 );
            
            StringBuffer intro = new StringBuffer();
            StringBuffer explanation = new StringBuffer();
            StringBuffer[] statements = new StringBuffer[0];
            String[] statement_idents=new String[0];
            boolean[] statement_answers=new boolean[0];
            
            list = presentation.getChildNodes();
            NodeList matlist, alist;
            Node child, tnode;
            Element matthing;
            boolean response_lid_present = false;
            boolean response_str_present = false;
            boolean response_grp_present = false;
            boolean multiple_varsubset = false;
            boolean unsupported_response_present = false;
            boolean negative_marking = false;
            String rcardinality="Single";
            String rlid_ident=null;
            
            for ( i=0; i<list.getLength(); i++ )
                {
                child = list.item( i );
                // hunt out all the material elements that are children of the presentation element
                // (author may intend that material is scattered between user interface elements
                // but bod questionnaires can only have one block of text so these are concatenated.
                if ( child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals( "material" ) )
                    {
                    //for each one pull out all mattext elements and append to the intro text.
                    parseMaterial( (Element)child, intro );
                    continue;
                    }

                // look for a response_lid element
                if ( child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals( "response_lid" ) )
                    {
                    // ignore second and subsequent blocks of choices
                    if ( response_lid_present )
                        continue;
                    response_lid_present = true;
                    
                    // need to parse content....
                    Element response_lid = (Element)child;
                    
                    NodeList rlid_list = response_lid.getElementsByTagName( "render_choice" );

                    //parse the render_choice element
                    // material and response labels may be intermixed - we will discard material
                    // that lies between labels.
                    
                    // Only the first render choice element is looked at
                    Element render_choice = (Element)rlid_list.item( 0 );
                    NodeList rchoice_list = render_choice.getElementsByTagName( "response_label" );
                    if ( rchoice_list.getLength()== 0 )
                        {
                        // no choices in the multiple choice!
                        // skip this response_lid and look for another
                        response_lid_present = false;
                        continue;
                        }

                    rcardinality = response_lid.getAttribute( "rcardinality" );
                    rlid_ident =  response_lid.getAttribute( "ident" );
                        
                    Element response_label;
                    // put together text for first five choices...
                    statements = new StringBuffer[rchoice_list.getLength()];
                    statement_idents = new String[rchoice_list.getLength()];
                    statement_answers = new boolean[rchoice_list.getLength()];
                    for ( j=0; j< rchoice_list.getLength() && j<5; j++ )
                        {
                        response_label = (Element)rchoice_list.item( j );
                        statement_idents[j] = response_label.getAttribute( "ident" );
                        statements[j] = new StringBuffer();
                        rlid_list = response_label.getElementsByTagName( "material" ); 
                        if ( rlid_list != null )
                            for ( k=0; k< rlid_list.getLength(); k++ )
                                parseMaterial( (Element)rlid_list.item( k ), statements[j] );
                        }


                    // may be an extra material element in front of or after multiple choice section
                    // just add to the other introductory text.
                    rlid_list = response_lid.getElementsByTagName( "material" ); 
                    for ( j=0; j< rlid_list.getLength(); j++ )
                        {
                        matthing = (Element)rlid_list.item( j );
                        //check that material element is at top level
                        if ( matthing.getParentNode() == response_lid )
                            parseMaterial( matthing, intro );
                        }
                        
                    continue;
                    }
              
                  // look for a response_grp element
                if ( child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals( "response_grp" ) )
                {
                    // ignore second and subsequent blocks of choices
                    if ( response_grp_present )
                        continue;
                    response_grp_present = true;
                    
                    // need to parse content....
                    Element response_grp = (Element)child;
                    
                    rcardinality = response_grp.getAttribute( "rcardinality" );
                    rlid_ident =  response_grp.getAttribute( "ident" );
                  
                    // may be an extra material element in front of or after multiple choice section
                    // just add to the other introductory text.
                    NodeList rgrp_list = response_grp.getElementsByTagName( "material" ); 
                    
                    matthing = (Element)rgrp_list.item( 0 );
                    parseMaterial( matthing, intro );
                    
                    rgrp_list = response_grp.getElementsByTagName( "render_choice" );

                    //parse the render_choice element
                    // material and response labels may be intermixed - we will discard material
                    // that lies between labels.
                    
                    // Only the first render choice element is looked at
                    Element render_choice = (Element)rgrp_list.item( 0 );
                    
                    NodeList rchoice_list = render_choice.getElementsByTagName( "response_label" );
                    if ( rchoice_list.getLength()== 0 )
                    {
                        // no choices in the multiple choice!
                        // skip this response_grp and look for another
                        response_grp_present = false;
                        continue;
                    }
                        
                    Element response_label;
                    // put together text for first five choices...
                    statements = new StringBuffer[5];
                    statement_idents = new String[5];
                    statement_answers = new boolean[5];
                    for ( j=0; j<5; j++ )
                    {
                        response_label = (Element)rchoice_list.item( j );
                        statement_idents[j] = response_label.getAttribute( "ident" );
                        statements[j] = new StringBuffer();
                        rgrp_list = response_label.getElementsByTagName( "material" ); 
                        if ( rgrp_list != null )
                            for ( k=0; k< rgrp_list.getLength(); k++ )
                                parseMaterial( (Element)rgrp_list.item( k ), statements[j] );
                    }
                    continue;
                    
                }
                
                // look for a qticomment element
                if ( child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals( "qticomment" ) )
                    {
                    //skip it
                    continue;
                    }
                    
                // look for text at this level (e.g. whitespace) and ignore it
                if ( child.getNodeName().equals( "#text" ) )
                    {
                    //skip it
                    continue;
                    }
                    
                // if reached this far must have encountered unsupported tag.
                intro.append( "<P>This imported question contained unsupported features.</P>" );
                log.debug( "Unsupported question type, encountered " + child.getNodeName() + "." );
                // just drop through - if there is no multiple choice or comment box
                // then there is just introductory text with no user interface.
                }


            // try to work out which statements are true.
            if ( rlid_ident !=null && statements.length > 0 )
                {
                log.debug( "Start resprocessing...." );
                list = document.getElementsByTagName( "resprocessing" );
                if ( list==null || list.getLength()!=1 )
                    throw new SAXException( "File format error" );
                Element resprocessing = (Element)list.item( 0 );
                NodeList decvars, setvars, convars, list2;
                Element outcomes, decvar, respcondition, convar, setvar;
                // the question doesn't involve scoring unless there is a SCORE variable
                // in the outcomes section
                boolean has_score=false, changes_score=false, increases_score=false;
                String value;
                int v;
                
                list = resprocessing.getChildNodes();
                for ( i=0; i<list.getLength(); i++ )
                    {
                    child = list.item( i );

                    if ( !child.getNodeName().equals( "outcomes" ) )
                  		continue;

                     log.debug( "Start outcomes...." );
                     outcomes = (Element)child;
                     decvars = outcomes.getElementsByTagName( "decvar" );
                     for ( j=0; decvars!=null && j<decvars.getLength(); j++ )
                           {
                           decvar = (Element)decvars.item( j );
                           log.debug( "decvar varname = " + decvar.getAttribute( "varname" ) );
                           if ( decvar.getAttribute( "varname" ).equals( "SCORE" ) )
                              has_score=true;
                           }
                    }
                
                if ( has_score )
                    {
                    for ( i=0; i<list.getLength(); i++ )
                        {
                        child = list.item( i );
                        if ( child.getNodeName().equals( "respcondition" ) )
                            {
                            log.debug( "Next respcondition." );
                            changes_score=false;
                            respcondition = (Element)child;
                            // the respcondition element is only interesting if it has
                            // a setvar element that will change the score
                            setvars = respcondition.getElementsByTagName( "setvar" );
                            for ( j=0; j< setvars.getLength(); j++ )
                            {
                                setvar = (Element)setvars.item( j );
                                if ( setvar.getAttribute( "varname" ).equals( "SCORE" ) )
                                {
                                    log.debug( "Testing setvar in this respcondition." );

                                    value = ((CharacterData)setvar.getFirstChild()).getData();
                                    
                                    try
                                        {
                                        v = Integer.parseInt( value );
                                        }
                                    catch ( NumberFormatException nfex )
                                        {
                                        //doesn't change score after all - non numerical
                                        continue;
                                        }
                                    
                                    increases_score = 
                                        (   (setvar.getAttribute( "action" ).equals( "Set" )      ||
                                             setvar.getAttribute( "action" ).equals( "Multiply" ) ||
                                             setvar.getAttribute( "action" ).equals( "Add" )         )  && v>0  ) ||
                                        (   (setvar.getAttribute( "action" ).equals( "Subtract" )    )  && v<0  );
                                    
                                    if ( !negative_marking )
                                        negative_marking = 
                                        (   (setvar.getAttribute( "action" ).equals( "Add" )         )  && v<0  ) ||
                                        (   (setvar.getAttribute( "action" ).equals( "Subtract" )    )  && v>0  );
                                        
                                    changes_score = true;
                                    
                                }
                            }
                            
                            if ( changes_score && increases_score )
                            {
                                // now we know this respcondition could increase the score we need to
                                // look at conditionvar elements to see if they suggest true statements
                                
                                // hypothetically - if a varequal element for a statement appears
                                // and is surounded by zero or an even number of not elements it could
                                // activate the setvar element.  OK?
                                log.debug( "This respcondition is being processed." );
                                
                                convars = respcondition.getElementsByTagName( "conditionvar" );
                                for ( j=0; j< convars.getLength(); j++ )
                                {
                                    convar = (Element)convars.item( j );
                                    
                                    if (response_grp_present)
                                    {
                                    	NodeList varsubset = convar.getElementsByTagName("varsubset");
                                    	if (varsubset.getLength() > 1)
                                    		multiple_varsubset = true;
                                    	for (l=0; l<varsubset.getLength(); l++)
                                    	{
                                    		Element e = (Element) varsubset.item( l );
                                    		CharacterData cdata = (CharacterData)e.getFirstChild();
                                    		if ( cdata != null )
                                    		{
                                    			String item_and_type = cdata.getData();
                                    			k = (int)(item_and_type.charAt(0)-'A');
                                    			statement_answers[k] = (item_and_type.charAt(2)=='T');
                                    		}
                                    	}
                                    }
                                    else
                                    //parse separately for each statement
                                    for ( k=0; k<statement_idents.length; k++ )
                                        {
                                        try
                                            {
                                            statement_answers[k] |= evaluateboolean( convar, rlid_ident, statement_idents[k] ) > 0;
                                            }
                                        catch ( Exception ex ) 
                                            {
                                            intro.append( "<P>Scoring definition for option " + (k+1) + " was too complex.</P>" );
                                            }
                                        }
                                    
                                    }
                                }
                            }
                        }
                    }
                }


            list = document.getElementsByTagName( "itemfeedback" );
            Element itemfeedback;
            for ( i=0; i<list.getLength(); i++ )
                {
                itemfeedback = (Element)list.item( i );
                matlist = itemfeedback.getElementsByTagName( "material" );
                for ( j=0; j<matlist.getLength(); j++ )
                    parseMaterial( (Element)matlist.item( j ), explanation );
                }

            McqQuestion question = new McqQuestion();
            // this will store a BigString in the database so what if we need to roll back?
            // empty strings go into unused statements
            for ( i=0; i<5; i++ )
                {
                question.setStatement( i , (i<statements.length)?(statements[i].toString()):("") );
                }

            for ( i=0; i<5 && i<statements.length; i++ )
                {
                question.setAnswer( i, statement_answers[i] );
                }

            if ( statements.length > 5 )
                intro.append( "<P>This imported question had more than 5 statements.</P>" );
                
            
            byte type = determineQuestionType(item_type, intro, response_grp_present,
					negative_marking, multiple_varsubset, rcardinality);
            question.setQuestionType(type);
    
            question.setQuestion( intro.toString() );
            question.setExplanation( explanation.toString() );
            
            return question;
            
            }
       catch (SAXParseException spe)
       {
          LoggingUtils.logSAXException(log,spe);
       }

       return null;
        }
    
    /**
     * Alternative method of determining Bod question type, using item metadata field.
     * Expects simplified form of Bod XML export, if it fails, it tries original method.
     * @param type integer value representing Bod question type. Relies on those not changing!
     * @return byte representation of Bod question type.
     */
    private byte determineQuestionType(String type, StringBuffer intro, boolean response_grp_present, boolean negative_marking, boolean multiple_varsubset,
    		String rcardinality)
    {
    	try {
    		return new Byte(type).byteValue();
    	} catch (Exception e)
    	{
    		return determineQuestionType(intro, response_grp_present, negative_marking, multiple_varsubset, rcardinality);
    	}
    }

    /**
     * Uses given factors to determine which type of question to create.
     * Adds any warning messages to intro. (Extracted from existing code).
     * @param intro
     * @param response_grp_present
     * @param negative_marking
     * @param rcardinality
     * @return
     */
    private byte determineQuestionType(StringBuffer intro, boolean response_grp_present, boolean negative_marking, boolean multiple_varsubset,
    		String rcardinality) {

    	if ( !rcardinality.equalsIgnoreCase( "Multiple" ) && !rcardinality.equalsIgnoreCase( "Single" ) )
    	{
    		intro.append( "<P>This imported question contained unsupported features.</P>" );
    		return McqQuestion.TYPE_MULTIPLE_TRUE;
    	}

    	if ( rcardinality.equalsIgnoreCase( "Single" ) )
    		return McqQuestion.TYPE_ONE_TRUE ;

    	else if ( multiple_varsubset )
    		return McqQuestion.TYPE_STRICT_MULTIPLE_TF;

    	else if ( response_grp_present )
    	{
    		intro.append( "<P>WARNING - compound multiple choice question in the QTI file was assumed to be 'True/False/Don't know' type but may not be - please check. (QTI does not have a method for defining whether statements are true or false - this muse be assumed based on English language words in the labels..</P>" );
    		if ( negative_marking )
    			return McqQuestion.TYPE_MULTIPLE_TFD;
    		else		    	
    			return McqQuestion.TYPE_MULTIPLE_TF;
    	}

    	return McqQuestion.TYPE_MULTIPLE_TRUE;
    }
 }
