/* ======================================================================
   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 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 static final byte Q_FLAG_IS_PARTIALLY_ANONYMOUS = 16;
	private byte questionnaire_flags;
	public static final int DEFAULT_ORDINAL_INCREMENT = 100;
	private Vector questions;
    private Timestamp deadline;
	
    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. By default, the created question is a
     * <em>standard</em> one.
     * @param ordinal the ordinal that the question should have.
     * @return the newly created question.
     * @see QuestionnaireQuestion#getOrdinal()
     * @see QuestionnaireQuestion#isStandardQuestion()
     */
    public QuestionnaireQuestion createQuestion( int ordinal )
        throws BuildingServerException
        {
        initialize();
        QuestionnaireQuestion question = new QuestionnaireQuestion();
        question.setQuestionnaireId( getResourceId() );
        question.setIntroduction( "*** Enter your question text here ***" );
        question.setStandardQuestion( true );
        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>.
     * @see #isPartiallyAnonymous()        
     */
    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) );
        if ( isPartiallyAnonymous() )
            {
            setQuestionnaireFlags( 
                (byte)(questionnaire_flags ^ Q_FLAG_IS_PARTIALLY_ANONYMOUS) );
            }
        }
    
    /**
     * Indicates whether or not this questionnaire is <em>partially</em>
     * anonymous. If this method returns <code>true</code>, then this
     * indicates that certain attribution information will be revealed, for
     * example, the names of people in the list of non-respondents.
     * @return <code>true</code> if this questionnaire is <em>partially</em>
     *         anonymous, otherwise <code>false</code>.
     * @see #isAnonymous()
     */
    public boolean isPartiallyAnonymous()
        {
        return (questionnaire_flags & Q_FLAG_IS_PARTIALLY_ANONYMOUS) != 0;
        }

    /**
     * Set whether or not this questionnaire is <em>partially</em> anonymous. If
     * this is set to <code>true</code> then in order to avoid leaking of
     * anonymity over time, a deadline must be set. Unfortunately, at present, 
     * this constraint must be enforced externally to this class.
     * @param b the value to be set to.
     * @see #isPartiallyAnonymous()
     */
    public void setPartiallyAnonymous( boolean b )
        {
        if ( isPartiallyAnonymous() == b )
            return;
        setQuestionnaireFlags( 
            (byte)(questionnaire_flags ^ Q_FLAG_IS_PARTIALLY_ANONYMOUS) );
        if ( isPartiallyAnonymous() && isAnonymous() )
            {
            setQuestionnaireFlags(  
                (byte)(questionnaire_flags ^ Q_FLAG_IS_ATTRIBUTABLE) );
            }
        }
    
    /**
     * Get the deadline. The deadline refers to a timestamp indicating a cut-off
     * point for submissions.
     * @return the deadline.
     */
    public Timestamp getDeadline()
        {
        return deadline;
        }

    /**
     * Set the deadline. 
     * @param deadline the deadline to be set.
     * @see #getDeadline()
     */
    public void setDeadline( Timestamp deadline )
        {
        if ( this.deadline == null && deadline == null )
            return;
        if ( this.deadline != null && this.deadline.equals( deadline ) )
            return;
        this.deadline = deadline;
        setUnsaved();
        }
    
    /**
     * Refresh this questionnaire. This forces this object to re-populate itself
     * from persistent storage.
     * @throws BuildingServerException
     */
    public void refresh() throws BuildingServerException
        {
        updateQuestionList();
        }
	}


