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

import java.rmi.RemoteException;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;

import org.bodington.database.*;
import org.bodington.server.*;
import org.bodington.server.resources.*;
import org.bodington.text.BigString;

/**
 * Class that encapsulates a questionnaire. This class represents a
 * questionnaire as a whole. An instance is an aggregation of individual
 * questions (see {@link QuestionnaireQuestion}).
 * @see QuestionnaireQuestion
 * @author Jon Maber
 * @author Alexis O'Connor
 */
public class Questionnaire extends org.bodington.server.resources.Resource
	{
    // NOTE: there are 8 bits in a byte!
    private static final byte Q_FLAG_IS_SINGLE_ATTEMPT = 1;
    private static final byte Q_FLAG_IS_HIDE_ANSWERS_ON_SUBMIT = 2;
    private static final byte Q_FLAG_IS_MULTIPLE_RESULT = 4;
    private static final byte Q_FLAG_IS_ATTRIBUTABLE = 8; // NOTE: !anonymous.
	private byte questionnaire_flags;
	public static final int DEFAULT_ORDINAL_INCREMENT = 100;
	private Vector questions;
	
    public Class sessionClass()
    	{
        // This method is derived from class org.bodington.server.resources.Resource
        // to do: code goes here
        return org.bodington.assessment.QuestionnaireSessionImpl.class;
    	}
    	

   	/**
     * Find the specified questionnaire.
   	 * @param key the ID of the questionnaire you want to find.
     * @see #getQuestionnaireId()
   	 */
   	public static Questionnaire findQuestionnaire( PrimaryKey key )
		throws BuildingServerException
		{
		return (Questionnaire)findPersistentObject( key, Questionnaire.class.getName() );
		}
	/**
     * Find the specified questionnaires. This method returns an enumeration
     * whose elements are instances of {@link Questionnaire}.
     * @param whereSQL a string representing the <code>WHERE</code> clause of
     *        some SQL representing the criteria upon which to select the
     *        questionnaires.
     * @return an enumeration of questionnaires.
     * @see PersistentObject#findPersistentObjects(String, String)
     */
	public static Enumeration findQuestionnaires( String whereSQL )
	    throws BuildingServerException
	    {
	    return findPersistentObjects( whereSQL, Questionnaire.class.getName() );
	    }
    /**
     * Find the specified questionnaires. This method returns an enumeration
     * whose elements are instances of {@link Questionnaire}.
     * @param whereSQL a string representing the <code>WHERE</code> clause of
     *        some SQL representing the criteria upon which to select the
     *        questionnaires.
     * @param orderSQL a string representing the <code>ORDER BY</code> clause
     *        of some SQL representing the criteria upon which to the returned
     *        questionnaire results.
     * @return an enumeration of questionnaires.
     * @see PersistentObject#findPersistentObjects(String, String)
     */
	public static Enumeration findQuestionnaires( String whereSQL, String orderSQL )
	    throws BuildingServerException
	    {
	    return findPersistentObjects( whereSQL, orderSQL, Questionnaire.class.getName() );
	    }
	    
    public PrimaryKey getQuestionnaireId()
		{
        return getResourceId();
        }

    public void setQuestionnaireId(PrimaryKey key)
    	{
    	setResourceId( key );
    	}
    	
	public byte getQuestionnaireFlags()
		{
		return questionnaire_flags;
		}
	public void setQuestionnaireFlags( byte f )
		{
		if ( f==questionnaire_flags ) return;
		questionnaire_flags=f;
    	setUnsaved();
		}
		

	/**
     * Update the list of questions. This method is used internally to synch
     * the list of questions with persistent storage and to ensure that the value
     * of the <code>maxOrdinal</code> property is up to date.
     * @see #getMaxOrdinal()
	 */
	private void updateQuestionList()
		throws BuildingServerException
		{
		questions = new Vector();
        // NOTE: looks wrong, but see QuestionnaireQuestion.getResourceId().
		Enumeration e = QuestionnaireQuestion.findQuestionnaireQuestions( 
            "resource_id = " + getQuestionnaireId(), "ordinal" );
		while ( e.hasMoreElements() )
			questions.addElement( e.nextElement() );
        getMinOrdinal();
        getMaxOrdinal();
		}
		
	/**
     * Get a list of the questions. The items in this list are instances of
     * {@link QuestionnaireQuestion}. The list returned is a shallow copy of the
     * underlying list used by this object.
	 * @return a list of the questions.
	 * @throws BuildingServerException
	 */
	public List getQuestions()
		throws BuildingServerException
		{
        initialize();
		return (List) questions.clone();
		}

	/**
     * Get the question at the specified index. If the index has no
     * corresponding entry, this method returns <code>null</code>.
     * @param n the specified index.
     * @return the question at the specified index, or <code>null</code> if
     *         the index has no corresponding entry.
     */
	public QuestionnaireQuestion getQuestionAt( int n )
		throws BuildingServerException
		{
        initialize();
		if ( n<0 || n>= questions.size() )
			return null;
			
		return (QuestionnaireQuestion)questions.elementAt( n );
		}
		
	/**
     * Create a new question. This method will append the question to the end
     * of the current list of questions.
     * @return the newly created question.
     * @see #createQuestion(int)
     * @see #nextOrdinal()
     * @see QuestionnaireQuestion#getOrdinal()
     */
	public QuestionnaireQuestion createQuestion()
        throws BuildingServerException
        {
        return createQuestion( nextOrdinal() );
        }

    /**
     * Create a new question. The <code>ordinal</code> parameter allows for
     * callers to specify where the question should be placed relative to the
     * existing list of questions.
     * @param ordinal the ordinal that the question should have.
     * @return the newly created question.
     * @see QuestionnaireQuestion#getOrdinal()
     */
    public QuestionnaireQuestion createQuestion( int ordinal )
        throws BuildingServerException
        {
        initialize();
        QuestionnaireQuestion question = new QuestionnaireQuestion();
        question.setQuestionnaireId( getResourceId() );
        question.setIntroduction( "The text of the question goes here." );
        question.setOrdinal( ordinal );
        question.save();
        updateQuestionList();
        return question;
        }
    

	/**
     * Remove the specified question. The specified parameter corresponds to the
     * value of {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}.
     * @param questionID the ID of the question to be removed.
	 * @throws BuildingServerException
	 */
	public void removeQuestion( PrimaryKey questionID ) throws BuildingServerException
		{
        initialize();
        QuestionnaireQuestion question 
            = QuestionnaireQuestion.findQuestionnaireQuestion( questionID );
        if ( question == null )
            throw new BuildingServerException( "Question not found." );

        if ( !question.getQuestionnaireId().equals( getQuestionnaireId() ) )
            throw new BuildingServerException( "Question doesn't belong in this Questionnaire." );

        question.delete();
        updateQuestionList();
		}


    public int getResourceType()
    	{
    	return RESOURCE_QUESTIONNAIRE;
    	}
    

    private int ordinalIncrement = DEFAULT_ORDINAL_INCREMENT;
    
    /**
     * Set the ordinal increment value. This is the value by which the ordinal
     * should be incremented by when, for example, creating a new question.
     * @param ordinalIncrement the ordinal increment value to set.
     */
    public void setOrdinalIncrement( int ordinalIncrement )
        {
        this.ordinalIncrement = ordinalIncrement;
        }


    /**
     * Get the ordinal increment value. 
     * @return the ordinal increment value.
     * @see #setOrdinalIncrement(int)
     */
    public int getOrdinalIncrement()
        {
        return ordinalIncrement;
        }
    
    public static final int INITIAL_MAX_ORDINAL = 0;
    private int maxOrdinal = INITIAL_MAX_ORDINAL;
    private int minOrdinal = INITIAL_MAX_ORDINAL;
    
    /**
     * Get the maximum ordinal value. This is the maximum ordinal of any
     * question within this questionnaire.
     * @return the maximum ordinal value.
     */
    public int getMaxOrdinal() throws BuildingServerException
        {
        initialize();
        Vector v = this.questions;
        Object q = !v.isEmpty() ? v.lastElement() : null;
        maxOrdinal = (q != null) ? ((QuestionnaireQuestion)q).getOrdinal()
                        : INITIAL_MAX_ORDINAL;
        return maxOrdinal;
        }
    
    /**
     * Get the minimum ordinal value. This is the minimum ordinal of any
     * question within this questionnaire.
     * @return the maximum ordinal value.
     */
    public int getMinOrdinal() throws BuildingServerException
        {
        initialize();
        Vector v = this.questions;
        Object q = !v.isEmpty() ? v.firstElement() : null;
        minOrdinal = (q != null) ? ((QuestionnaireQuestion)q).getOrdinal()
                        : INITIAL_MAX_ORDINAL;
        return minOrdinal;
        }
    
    /**
     * Get the next available ordinal value. The difference between this method
     * and {@link #getNextOrdinal()}, is that this method implicitly increments
     * the value returned by {@link #getMaxOrdinal()}. By default, when a new
     * question is created it is appended to the end of the list, so this method
     * is useful to code that performs such operations.
     * @return the next available ordinal value.
     * @see #getNextOrdinal()
     * @see #getMaxOrdinal()
     */
    public int nextOrdinal()
        {
        return maxOrdinal = getNextOrdinal();
        }
    
    /**
     * Get the next available ordinal value. This method returns the next
     * available ordinal value.
     * @return the next available ordinal value.
     * @see #getMaxOrdinal()
     * @see #getOrdinalIncrement()
     * @see QuestionnaireQuestion#getOrdinal()
     */
    public int getNextOrdinal()
        {
        return maxOrdinal + getOrdinalIncrement();
        }
    
    /**
     * Get the previous available ordinal value. The difference between this
     * method and {@link #getPreviousOrdinal()}, is that this method implicitly
     * decrements the value returned by {@link #getMinOrdinal()}. This method
     * can be used by code that wishes to prepend questions to the existing
     * question list.
     * @return the next available ordinal value.
     * @see #getPreviousOrdinal()
     * @see #getMinOrdinal()
     */
    public int previousOrdinal()
        {
        return minOrdinal = getPreviousOrdinal();
        }
    
    /**
     * Get the previous available ordinal value. This method returns an ordinal
     * value to give a question in order to prepend it to the list of questions.
     * @return the previous available ordinal value.
     * @see #getMinOrdinal()
     * @see #getOrdinalIncrement()
     * @see QuestionnaireQuestion#getOrdinal()
     */
    public int getPreviousOrdinal()
        {
        // NOTE: an empty questionnaire is a special case, hence ...
        return !questions.isEmpty() 
            ? minOrdinal - getOrdinalIncrement() : getNextOrdinal();
        }
    
    /**
     * Initialize this object. This method is used internally to lazy load the 
     * questions of this questionnaire from persistent storage.
     */
    private void initialize() throws BuildingServerException
        {
        if (questions == null)
            updateQuestionList();
        }
    
    /**
     * Change the specified question. The <code>questionID</code> parameter
     * corresponds to the value of
     * {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}.
     * @param questionID the ID of the question to be changed.
     * @param newQuestion an object containing the new values for the specified
     *        question.
     * @throws BuildingServerException
     */
    public void changeQuestion( PrimaryKey questionID, QuestionnaireQuestion newQuestion )
        throws BuildingServerException
        {
        initialize();
        QuestionnaireQuestion question 
            = QuestionnaireQuestion.findQuestionnaireQuestion( questionID );
        if ( question == null )
            throw new BuildingServerException( "Question not found." );

        if ( !question.getQuestionnaireId().equals( getResourceId() ) )
            throw new BuildingServerException(
                "Question does not belong in this Questionnaire." );

        question.setOrdinal( newQuestion.getOrdinal() );
        question.setFlags( newQuestion.getFlags() );
        question.save();
        updateQuestionList();
        }

    /**
     * Change the specified question. The <code>questionID</code> parameter
     * corresponds to the value of
     * {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}. The text
     * that can be changed includes the introduction and the text of the
     * statements / responses.
     * @param questionID the ID of the question whose text is to be changed.
     * @param textID the ID of the text to be changed.
     * @param newText the value of the new text.
     * @throws BuildingServerException
     */
    public void changeQuestionText( PrimaryKey questionID, PrimaryKey textID,
        String newText ) throws BuildingServerException
        {
        initialize();
        QuestionnaireQuestion question 
            = QuestionnaireQuestion.findQuestionnaireQuestion( questionID );
        if ( question == null )
            throw new BuildingServerException( "Question not found." );

        if ( !question.getQuestionnaireId().equals( getResourceId() ) )
            throw new BuildingServerException(
                "Question does not belong in this Questionnaire." );

        boolean found = false;
        found = found || textID.equals( question.getIntroductionBigStringId() );
        found = found || textID.equals( question.getStatementABigStringId() );
        found = found || textID.equals( question.getStatementBBigStringId() );
        found = found || textID.equals( question.getStatementCBigStringId() );
        found = found || textID.equals( question.getStatementDBigStringId() );
        found = found || textID.equals( question.getStatementEBigStringId() );
        found = found || textID.equals( question.getStatementFBigStringId() );
        found = found || textID.equals( question.getStatementGBigStringId() );
        found = found || textID.equals( question.getStatementHBigStringId() );
        found = found || textID.equals( question.getStatementIBigStringId() );
        found = found || textID.equals( question.getStatementJBigStringId() );

        if ( !found )
            throw new BuildingServerException(
                "Can not identify which part of question to edit." );

        BigString big = BigString.findBigString( textID );
        big.setString( newText );
        big.save();
        question.setUnsaved();
        updateQuestionList();
        }
    
    /**
     * Indicates whether this questionnaire allows for multiple attempts. If
     * this method returns <code>true</code>, then a user is allowed to have
     * multiple attempts at completing the questionnaire. If this method returns
     * <code>false</code>, then the user is only allowed a single attempt at
     * completing the questionnaire.
     * @return <code>true</code> if this questionnaire allows for multiple
     *         attempts, otherwise <code>false</code>.
     */
    public boolean isMultipleAttempts()
        {
        return (questionnaire_flags & Q_FLAG_IS_SINGLE_ATTEMPT) == 0;
        }
    
    /**
     * Set whether or not this questionnaire allows for multiple attempts.
     * @param b the value to be set to.
     * @see #isMultipleAttempts()
     */
    public void setMultipleAttempts( boolean b )
        {
        if ( isMultipleAttempts() == b )
            return;
        setQuestionnaireFlags( 
            (byte)(questionnaire_flags ^ Q_FLAG_IS_SINGLE_ATTEMPT) );
        }
    
    /**
     * Indicates whether the answers are hidden from the user on submit. The
     * primary motivation for this property is when used in conjunction with an
     * instance where {@link #setMultipleResults(boolean)} has been set to
     * <code>true</code>. In this case, each time a user has submitted a
     * response, they are then presented with a new blank paper to fill in (and
     * corresponding new set of answers that can be submitted).
     * @return <code>true</code> if the answers are hidden from the user on
     *         submit, otherwise <code>false</code>.
     * @see #isMultipleResults()
     */
    public boolean isHideAnswersOnSubmit()
        {
        return (questionnaire_flags & Q_FLAG_IS_HIDE_ANSWERS_ON_SUBMIT) != 0;
        }
    
    /**
     * Set whether or not answers are hidden from the user on submit.
     * @param b the value to be set to.
     * @see #isHideAnswersOnSubmit()
     */
    public void setHideAnswersOnSubmit( boolean b )
        {
        if ( isHideAnswersOnSubmit() == b )
            return;
        setQuestionnaireFlags( 
            (byte)(questionnaire_flags ^ Q_FLAG_IS_HIDE_ANSWERS_ON_SUBMIT) );
        }
    
    /**
     * Indicates whether this questionnaire allows for multiple results. If this
     * method returns <code>true</code>, then a user is allowed to submit
     * multiple responses to the same questionnaire. If this method returns
     * <code>false</code>, then the user is only able to submit a single
     * response to the questionnaire.
     * @return <code>true</code> if this questionnaire allows for multiple
     *         results, otherwise <code>false</code>.
     */
    public boolean isMultipleResults()
        {
        return (questionnaire_flags & Q_FLAG_IS_MULTIPLE_RESULT) != 0;
        }
    
    /**
     * Set whether or not this questionnaire allows for multiple results.
     * @param b the value to be set to.
     * @see #isMultipleResults()
     */
    public void setMultipleResults( boolean b )
        {
        if ( isMultipleResults() == b )
            return;
        setQuestionnaireFlags( 
            (byte)(questionnaire_flags ^ Q_FLAG_IS_MULTIPLE_RESULT) );
        }
    
    /**
     * Indicates whether or not this questionnaire is anonymous. Strictly
     * speaking, this means whether or not the attribution of questionnaire
     * results to system users will be revealed to the creator of the
     * questionnaire resource instance.
     * @return <code>true</code> if this questionnaire is anonymous, otherwise
     *         <code>false</code>.
     */
    public boolean isAnonymous()
        {
        return (questionnaire_flags & Q_FLAG_IS_ATTRIBUTABLE) == 0;
        }

    /**
     * Set whether or not this questionnaire is anonymous.
     * @param b the value to be set to.
     * @see #isAnonymous()
     */
    public void setAnonymous( boolean b )
        {
        if ( isAnonymous() == b )
            return;
        setQuestionnaireFlags( 
            (byte)(questionnaire_flags ^ Q_FLAG_IS_ATTRIBUTABLE) );
        }
    
    /**
     * Refresh this questionnaire. This forces this object to re-populate itself
     * from persistent storage.
     * @throws BuildingServerException
     */
    public void refresh() throws BuildingServerException
        {
        updateQuestionList();
        }
	}


