/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  All rights reserved.
  
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
=========================================================

This software was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

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

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;
        
        
        try
            {
            Node questestinterop = document.getFirstChild();
            Node item = questestinterop.getFirstChild();
            if ( item.getNodeType() != Node.ELEMENT_NODE || !item.getNodeName().equals( "item" ) )
                throw new SAXException( "File format error" );
                
            NodeList 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 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)
                                        {
                                            
                                            Element e = (Element) varsubset.item( 0 );
                                            
                                            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>" );
                
            question.setQuestion( intro.toString() );
            question.setExplanation( explanation.toString() );
            
            if ( !rcardinality.equals( "Multiple" ) )
                question.setQuestionType( McqQuestion.TYPE_ONE_TRUE );
            else if ( response_grp_present )
            {
                if ( negative_marking )
                    question.setQuestionType( McqQuestion.TYPE_MULTIPLE_TFD );
                else
                    question.setQuestionType( McqQuestion.TYPE_MULTIPLE_TF );
                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>" );
            }
            else
                question.setQuestionType( McqQuestion.TYPE_MULTIPLE_TRUE );
            
            if ( !rcardinality.equals( "Multiple" ) && !rcardinality.equals( "Single" ) )
                intro.append( "<P>This imported question contained unsupported features.</P>" );
                
                
            return question;
            }
       catch (SAXParseException spe)
       {
          LoggingUtils.logSAXException(log,spe);
       }
            
        
        return null;
        }

    }