/* ======================================================================
   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;

import org.apache.log4j.Logger;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Hashtable;
import java.sql.Timestamp;
import java.io.*;

import org.xml.sax.*;


import org.bodington.assessment.ims.QuestionnaireImsQtiHandler;
import org.bodington.database.PrimaryKey;
import org.bodington.logging.LoggingUtils;
import org.bodington.server.*;
import org.bodington.text.BigString;
import org.bodington.server.events.ExportEvent;
import org.bodington.server.realm.User;
import org.bodington.server.realm.Permission;
import org.bodington.server.resources.Resource;
import org.bodington.util.CSVWriter;
import org.bodington.xml.ClasspathEntityResolver;
import org.bodington.xml.XMLUtils;


/**
 * Concrete implementation of a questionnaire session.
 * @see QuestionnaireQuestion
 * @author Jon Maber
 * @author Alexis O'Connor
 */
public class QuestionnaireSessionImpl
	extends org.bodington.server.SingleResourceSession 
	implements QuestionnaireSession 
	{
    private static Logger log = Logger.getLogger(QuestionnaireSessionImpl.class);
    private Questionnaire questionnaire;
	
	public QuestionnaireSessionImpl()
		throws BuildingServerException
		{
		super();
		}
	
	public Questionnaire getQuestionnaire()
		throws BuildingServerException
		{
        return questionnaire;
		}
		
	public List getQuestionnaireQuestionIds() 
        throws BuildingServerException
        {
        Vector list = new Vector();
        Iterator iterator = questionnaire.getQuestions().iterator();
        while ( iterator.hasNext() )
            {
            QuestionnaireQuestion question 
                = (QuestionnaireQuestion) iterator.next();
            list.addElement( question.getQuestionnaireQuestionId() );
            }

        return list;
        }
	
	public Map getQuestionnaireQuestions()
		throws BuildingServerException
		{
		Hashtable table = new Hashtable();
        Iterator iterator = questionnaire.getQuestions().iterator();
		while ( iterator.hasNext() )
			{
            QuestionnaireQuestion question 
                = (QuestionnaireQuestion)iterator.next();
			table.put( question.getQuestionnaireQuestionId(), question );
			}
		
		return table;
		}
	
	public List getQuestionnaireQuestionsInOrder()
		throws BuildingServerException
		{
        return questionnaire.getQuestions();
		}
	
	public Map getAllQuestionnaireResults() throws BuildingServerException
        {
        Map allResults = new Hashtable();
        List userList = getQuestionnaireUsers();
        Iterator users = userList.iterator();
        while ( users.hasNext() )
            {
            User user = (User)users.next();
            List results = getQuestionnaireResults( user.getUserId() );
            allResults.put( user, results );
            }

        return allResults;
        }
	
	public QuestionnaireQuestion createQuestion()
		throws BuildingServerException
		{
		return questionnaire.createQuestion();
		}
    
	public QuestionnaireQuestion createQuestion( int ordinal ) 
        throws BuildingServerException
        {
        return questionnaire.createQuestion( ordinal );
        }

    public void removeQuestion( PrimaryKey id )
		throws BuildingServerException
		{
        questionnaire.removeQuestion( id );
		}
	
	public void changeQuestion( PrimaryKey id, QuestionnaireQuestion model )
		throws BuildingServerException
		{
        questionnaire.changeQuestion( id, model);
		}
	
	public void changeQuestionText( PrimaryKey qid, PrimaryKey tid, String new_text )
		throws BuildingServerException
		{
        questionnaire.changeQuestionText( qid, tid, new_text );
		}
	
	public PrimaryKey record( boolean newResult, PrimaryKey[] q_question_ids,
        List selections, String[] comments ) throws BuildingServerException
        {
        User user = (User)BuildingContext.getContext().getUser();
        Map q_questions = this.getQuestionnaireQuestions();
        QuestionnaireResponse q_response;
        QuestionnaireQuestion q_question;
        QuestionnaireResult q_result 
            = newResult ? null : getQuestionnaireResult( user.getUserId() );
        if ( q_result == null )
            {
            q_result = new QuestionnaireResult();
            q_result.setQuestionnaireId( getResource().getResourceId() );
            q_result.setUserId( user.getUserId() );
            q_result.setWhenStarted( new Timestamp( System.currentTimeMillis() ) );
            q_result.setWhenSaved( new Timestamp( System.currentTimeMillis() ) );
            q_result.save();
            }
        Map responses = q_result.getQuestionnaireResponses();
        q_result.setWhenSaved( new Timestamp( System.currentTimeMillis() ) );
        q_result.save();

        for ( int i = 0; i < q_question_ids.length; i++ )
            {
            q_question = (QuestionnaireQuestion)q_questions.get( 
                q_question_ids[i] );
            if ( q_question == null )
                throw new BuildingServerException(
                    "Unable to find question data." );
            q_response = (QuestionnaireResponse)responses.get( 
                q_question_ids[i] );
            if ( q_response == null )
                {
                q_response = new QuestionnaireResponse();
                q_response.setQuestionnaireResultId( 
                    q_result.getQuestionnaireResultId() );
                q_response.setQuestionnaireQuestionId( q_question_ids[i] );
                q_response.save();
                }
            byte[] selection = (byte[])selections.get( i );
            
            if ( q_question.isMultipleResponse() )
                {
                q_response.setSelection( selection );
                }
            else
                {
                byte item = 100; // no selection.
                if ( selection.length > 0 )
                    {
                    item = selection[0];
                    if ( item < 0 || item >= q_question.getStatementCount() )
                        item = 100;
                    }
                q_response.setItem( item );
                }
            q_response.setComment( comments[i] );
            q_response.save();
            }

        return q_result.getQuestionnaireResultId();
        }
	
	public QuestionnaireResult getQuestionnaireResult()
		throws BuildingServerException
		{
		return getQuestionnaireResult( getUserId() );
		}
		
	public QuestionnaireResult getQuestionnaireResult( PrimaryKey uid )
        throws BuildingServerException
        {
        List list = getQuestionnaireResults( uid );
        return list.isEmpty() 
            ? null : (QuestionnaireResult)list.get( list.size() - 1 );
        }
    
    private List getQuestionnaireResults( PrimaryKey userID )
        throws BuildingServerException
        {
        Vector list = new Vector();
        // NOTE: the resource_id of the result is that of the questionnaire!
        Enumeration enumeration = QuestionnaireResult.findQuestionnaireResults( 
                "resource_id = " + getQuestionnaire().getQuestionnaireId() + 
                " AND user_id = " + userID );

        while ( enumeration.hasMoreElements() )
            list.addElement( enumeration.nextElement() );

        return list;
        }
		
	public List getQuestionnaireResults() throws BuildingServerException
        {
        return getQuestionnaireResults( getUserId() );
        }

	public List analyse( String whereSQL, QuestionnaireAnalysis analysis )
		throws BuildingServerException
		{
		Vector list = new Vector();
		Enumeration enumeration;
		QuestionnaireResult q_result;
		QuestionnaireResponse q_response;
		QuestionnaireQuestion q_question;
		Map responses;
		List questions=null;
		
        if (whereSQL == null) 
            whereSQL = "resource_id = " + getResource().getResourceId();
            
		enumeration = QuestionnaireResult.findQuestionnaireResults( whereSQL );

		if ( analysis != null )
			questions = getQuestionnaireQuestionsInOrder();	

		while ( enumeration.hasMoreElements() )
			{
			q_result = (QuestionnaireResult)enumeration.nextElement();
			list.addElement( q_result );
			
			if ( analysis == null )
				continue;
			
			//now add analysis...
			
			analysis.addQuestionnaireResult( q_result );
			responses = q_result.getQuestionnaireResponses();
			
			for ( int i=0; i<questions.size(); i++ )
				{
				q_question = (QuestionnaireQuestion)questions.get( i );
				q_response = (QuestionnaireResponse)responses.get( q_question.getQuestionnaireQuestionId() );
				if ( q_response != null )
					analysis.addQuestionnaireResponse( q_question, q_result, q_response );
				}
			
			}

		return list;
		}
	
	/** Export XML file (IMS QTI) containing questions.
	 * Calling method is responsible for closing the OutputStream passed in as a parameter.
	 */
	public void exportXML( OutputStream stream )
	throws BuildingServerException
	{
		Questionnaire paper = getQuestionnaire();
		List questions = getQuestionnaireQuestionsInOrder();

		if ( paper == null || questions == null )
			throw new BuildingServerException("Unable to find questionnaire data." );

		try
		{
			PrintWriter xmlout = new PrintWriter(new BufferedWriter( new OutputStreamWriter( stream, "UTF8" )));

			xmlout.print( "<?xml version=\"1.0\" standalone=\"no\"?>" );
			xmlout.println();
			xmlout.print( "<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
			xmlout.println();

			xmlout.println( "<questestinterop>" );
			xmlout.print( "<assessment ident=\"" + paper.getResourceId() + "\"");
			xmlout.println( " title=\"" + paper.getTitle() + "\">" );
			
			xmlout.println( "\t<presentation_material>");
			xmlout.println( "\t\t<flow_mat>");
			xmlout.println( "\t\t\t<material>");
			xmlout.print( "\t\t\t\t<mattext><![CDATA[");
			xmlout.print(paper.getDescription());
			xmlout.println("]]></mattext>");
			xmlout.println( "\t\t\t</material>");
			xmlout.println( "\t\t</flow_mat>");
			xmlout.println( "\t</presentation_material>");
			
			xmlout.println( "\t<section ident=\"default\">");

			for ( int i = 0; i < questions.size(); i++ )
			{
				QuestionnaireQuestion question = 
					(QuestionnaireQuestion)questions.get( i );

				// ask the question to export itself, allowing composite output
				question.exportXml( xmlout, true );
			}
			xmlout.println( "\t</section>" );
			xmlout.println( "</assessment>" );
			xmlout.println( "</questestinterop>" );
			xmlout.flush();

			ExportEvent event = new ExportEvent(ExportEvent.EVENT_EXPORT_ASSESSMENT, paper);
			event.save();

		}
		catch ( IOException ioex )
		{
			log.error( ioex.getMessage(), ioex );
			throw new BuildingServerException( "Problem creating XML file: "
					+ ioex.getMessage() );
		}
	}
	    
	public void importXML( String file_name )
		throws BuildingServerException
	    {
        try
            {
            // Parse the input
            XMLReader reader = XMLUtils.getXMLReader();
            QuestionnaireImsQtiHandler handler = new QuestionnaireImsQtiHandler( reader );
            reader.setContentHandler( handler );
            
            reader.setProperty( "http://xml.org/sax/properties/lexical-handler", handler );
            ClasspathEntityResolver resolver = new ClasspathEntityResolver( handler.getClass() );
            reader.setEntityResolver( resolver );
            
            File input_file = new File( file_name );
            if ( !input_file.exists() )
                throw new BuildingServerException( "Couldn't open the uploaded file." );
                
            reader.parse( "file:" + input_file );
            
            Vector questions = handler.getQuestions();
            QuestionnaireQuestion question=null, previous_question=null;
            
            for ( int i=0; i<questions.size(); i++ )
                {
                previous_question = question;
                question = (QuestionnaireQuestion)questions.elementAt( i );
                
                if (    previous_question != null &&
                        question.getStatementCount() == 0 && 
                        question.isCanComment() && 
                        question.getIntroduction().indexOf( "This is the comment box for the previous question." )>=0 )
                    {
                    // merge this question with previous
                    previous_question.setCanComment( true );
                    previous_question.save();
                    //remove current
                    questions.removeElementAt( i );
                    //fix index so we point at next question that has now moved down one place
                    i--;
                    continue;
                    }
                
                question.setQuestionnaireId( getResource().getResourceId() );
                question.setOrdinal( questionnaire.nextOrdinal());
		        question.save();
                }
            // Tell the questionnaire to refresh itself.
            questionnaire.refresh();
            }
       catch (SAXParseException spe)
            {
            LoggingUtils.logSAXException(log,spe);
            throw new BuildingServerException( "Problem importing XML file: " + spe.getMessage() );
            }
        catch (Throwable t)
            {
            log.error(	t.getMessage(), t );
            throw new BuildingServerException( "Problem importing XML file: " + t.getMessage() );
            }
	    }
    
    /**
     * Overridden so that we can initialize our corresponding questionnaire
     * instance. Otherwise, we just delegate to the corresponding method in our
     * superclass.
     * @param resource_id the primary key of the associated resource (questionnaire).
     * @see Questionnaire
     * @throws IllegalStateException if the corresponding questionnaire could
     * not be found.
     */
    public void setResourceId( PrimaryKey resource_id ) 
        {
        // NOTE: we override this method so as to initialize our questionnaire:
        super.setResourceId( resource_id );
        try
            {
            this.questionnaire = Questionnaire.findQuestionnaire( 
                getResourceId() );
            }
        catch ( BuildingServerException e ) 
            {
            IllegalStateException e2 = new IllegalStateException();
            e2.initCause( e );
            throw e2;
            }
        }

    /**
     * Indicates whether or not it is acceptable to display the non-respondents.
     * This implementation of the method will return <code>true</code> in the
     * following circumstances:
     * <ul>
     * <li>for <strong>all</strong> <em>fully attributable</em> questionnaires.
     * <li>partially anonymous surveys with more than 1 respondent.
     * </ul>
     * @return <code>true</code> if it is possible to show the
     *         non-respondents, otherwise <code>false</code>.
     * @see Questionnaire#isAnonymous()
     * @throws BuildingServerException
     */
    public boolean isCanShowNonRespondents() throws BuildingServerException
        {
        // TODO: it's likely that a more sophisticated algorithm is needed.
        Questionnaire q = getQuestionnaire();
        return !q.isAnonymous()
            || (q.isPartiallyAnonymous() && getQuestionnaireUsers().size() > 1);
        }

    public List getNonRespondents() throws BuildingServerException
        {
        List nonRespondents = Collections.list( 
            getQuestionnaire().everyoneWhoCan( Permission.RECORD, false ) );
        nonRespondents.removeAll( getQuestionnaireUsers() );

        return nonRespondents;
        }
    
    public List getSpecialGroupsWithPermission() throws BuildingServerException
    {
        Hashtable groups = getQuestionnaire().everySpecialGroupWhoCan(
            Permission.RECORD, false );
        
        return Collections.list( groups.elements() );
    }

    public List getQuestionnaireUsers() throws BuildingServerException
        {
        List users = new Vector();
        // TODO: factoring out candidate - (taken from TextQ equivalent).
        Enumeration enumeration = User.findUsers( "user_id IN (SELECT user_id "
            + "FROM questionnaire_results WHERE resource_id = "
            + getResource().getResourceId() + ")", "surname, initials" );

        while ( enumeration.hasMoreElements() )
            {
            users.add( enumeration.nextElement() );
            }

        return users;
        }

    public boolean isAvailable() throws BuildingServerException
        {
        // TODO Auto-generated method stub
        Timestamp deadline = getQuestionnaire().getDeadline();
        if (deadline == null)
            return true;
        
        return new Date().before( deadline );
        }

    public boolean isReviewable() throws BuildingServerException
        {
        // TODO: is this OK?
        return !(getQuestionnaire().isPartiallyAnonymous() && isAvailable());
        }

    public boolean getCanModifyDeadline() throws BuildingServerException
        {
        return isAvailable();
        }
	}
	
