module TestTemplate;

from Questions import Class;
from Questions import LastUsed;
from Questions import Time;
from Questions import Type;
from Questions import Question;
from Questions import Author;
from Questions import QKeyword;
from Questions import Difficulty;
from Questions import QuestionName;
from Database import LocalDatabase;
from Database import Database;
from Tests import Test;
from Tests import QuestionGroup;

object TestTemplate is 
	components: TestTemplateItem*;  
	description: (* 
		A TestTemplate any number of Groups and Questions
		in an ordered List
	*); 
end TestTemplate;

object TestTemplateItem is
	components: g:Group or q:Question;
	description: (*
		A test template item is either a Group or a specific 
		Queestion
	*);
end TestTemplateItem;


object Group is
	components: c:criteria*, t:GroupTime, q:NumberOfQuestions,
			d:GroupDifficulty;
	description: (*
		A Group is comprise of a list of criteria, a 
		time that the Group should take to complete,
		the number of questions in the Group, and a
		specification of the variability of the 
		difficulty of the questions in the Group
	*);
end Group;

object criteria is
	components: a:QuestionAttribute, v:QuestionAttributeValue;
	description: (*
		A criteria is a QuestionAttribute and the value that
		the given attribute must assume to vailidate the 
		criteria
	*);
end criteria;

object GroupTime is integer;

object NumberOfQuestions is integer;

object GroupDifficulty is
	components: one:all1 or two:all2 or three:all3 or four:all4 or five:all5 or inc:Increasing
			or random:Random;
	description: (*
		The specification of of the difficulty variability in
		a group
	*);
end GroupDifficulty;

object Text is string;
object Keywords is string;

object all1 is boolean;
object all2 is boolean;
object all3 is boolean;
object all4 is boolean;
object all5 is boolean;
object Increasing is boolean;
object Random is boolean;

object QuestionAttribute is
	components:   c:Class or t:Type or k:Keywords or a:Author;
	description: (*
		The possible attributes of a question,
		NOTICE, this is the name of the attribute, NOT
		its value.
	*);
end QuestionAttribute;

object QuestionAttributeValue is
	components: string*;
	description: (*
		The desired value(s) for Class, Type, Keywork, or Author.
	*);
end QuestionAttributeValue;


operation CreateGroup 
	inputs: c:criteria*, t:GroupTime, q:NumberOfQuestions, d:GroupDifficulty;
	outputs: g:Group;
	precondition: (*
			 *  The criteria list is not empty. The time is non-negative.
			 *  The number of questions is non-negative. The GroupDifficulty is defined.
			 *)

			(c != nil)
			and
			(t > 0)
			and
			(q > 0)
			and
			( (d.one = true) or (d.two = true) or (d.three = true) or (d.four = true) or 
			  (d.five = true) or (d.random = true) or (d.inc = true) );

	postcondition:(* 
			 *  The inputs become the contents of the output  
			 *)

			(g.c = c)
			and
			(g.d = d)
			and
			(g.t = t)
			and
			(g.q = q);

	description: (* Creates a group given criteria, time, 
			number of questions, and difficulty *);
end;

operation CreateTemplate
	inputs: l:TestTemplateItem*;
	outputs: t:TestTemplate;
	precondition: (*
			 * The list of TestTemplateItems is not empty.
			 *)

		(l != nil);

	postcondition:(*
			 * The TestTemplate is now equal to the list of TestTemplateItems.
			 *)

		(t = l);

	description: (* Creates a template given a list of TestTemplateItems *);
end;

operation SelectQuestions
	inputs: i:TestTemplateItem, l:Database;
	outputs: qg:QuestionGroup;
	precondition:	(*
			 * The Database has enough questions of the given criteria.
			 *)
		(if (i.g != nil) then
			AreEnough(l, i.g.c, i.g.q);
		)
		and
		(if (i.q != nil) then
			(i.q in l));

	postcondition:(*
			 * The questions satsify the criteria.
			 * The questions add up to within 10% of the required time.
			 * The questions have approximately the required difficulty distribution.
			 *) 
		(if (i.g != nil) then 
		(
			(forall (q in qg.qs) (QuestionFits(q,i.g.c)))
			and
			((TotalTime(qg.qs) - i.g.t) < (i.g.t * 0.1)) 
			and 
			((i.g.t - TotalTime(qg.qs)) > (i.g.t * 0.1))
			and
			DifficultyMatch(l, qg, i.g.d, i.g.c)
		))
		and
		(if (i.q != nil) then 
			((#qg.qs = 1) and (forall (q in qg.qs) q = i.q)));  
		
	description: (* Creates a QuestionGroup populated with questions satisfying the conditions of
			TestTemplateItem.  This QuestionGroup can then be added to a test using 
			InsertQuestionGroup, found in tests.fmsl *);
end;

operation ReplacQuestion
	inputs: q:Question, t:Test, l:Database;
	outputs: t':Test;
	precondition: (*
			 *  q is in T, there exists a question similar to q in l
			 *)

		(exists (qg in t.questions) q in qg.qs)
		and
		(exists (q' in l) (q'.ty = q.ty) and (q'.cl = q.cl) and (q'.d <= q.d + 1) and (q'.d >= q.d -1));

	postcondition:(*
			 * t' is identical to t except t' has q' in place of q
			 *)
		
		(forall (i:integer| (i>=1) and (i<=#t.questions)) (if (not(q in t.questions[i].qs)) 
			then (t.questions[i] = t'.questions[i])
			else (forall (index:integer|(index>=1)and(index<=#t'.questions[i].qs))
				(if (q != t'.questions[i].qs[index]) 
					then (t'.questions[i].qs[index] = t.questions[1].qs[index])
					else (	
						(t'.questions[i].qs[index].ty = q.ty) 
						and (t'.questions[i].qs[index].cl = q.cl)
						and (t'.questions[i].qs[index].d <= q.d + 1) 
						and (t'.questions[i].qs[index].d >= q.d -1)
					)
				)
			)));

	description: (* Replaces a question in a test with a similar question*);
end;



function AreEnough(l:Database, cl:criteria*, n:NumberOfQuestions) =
	(*
	 * Are there n questions in l that satisfy c?
	 *)
	(forall (c in cl) (HowMany(l, c) >= n));

function HowMany(l:Database, c: criteria) =
	(*
	 *  How many questions in the database satisfy c?  Notice that the condition is equal to "QuestionFits".
	 *)
	(if (QuestionFits(l[1], [c])) then (HowMany(l[2:#l],c) + 1) 
		else (if (#l != 0 ) then (HowMany(l[2:#l],c)) else 0));

function QuestionFits(q:Question, cl:criteria*) -> boolean =
	(*
	 * Does q satsify c?
	 *)
	(forall (c in cl)
	(if (c.a.t != nil) then (exists (v in c.v) v = q.ty)) 
	or
	(if (c.a.c != nil) then (exists (v in c.v) v in q.cl))
	or
	(if (c.a.a != nil) then (exists (v in c.v) v = q.a)) 
	or
	(if (c.a.k != nil) then (exists (v in c.v) v in q.k)));

function TotalTime(ql: Question*) =
	(*
	 *  The sum of the time of each question in a list of question.
	 *)
	(if (#ql = 0) then 0 
		else (ql[1].t + TotalTime(ql[2:#ql])));

function DifficultyMatch(l: Database, qg:QuestionGroup,  d:GroupDifficulty, cl:criteria*) =
	(*
	 * Are the questions in the group of the right Difficulty?
	 *)
	(if (d.one) then ((forall (q in qg.qs) q.d = 1)
				or
				((forall (q in l) (if ((q.d = 1) and QuestionFits(q,cl))
					then (q in qg.qs)))
					and
				 (forall (q in qg.qs) ((q.d =1) or (q.d=2)))
				)
			    ))
	and
	(if (d.two) then ((forall (q in qg.qs) q.d = 2)
				or
				((forall (q in l) (if ((q.d = 2) and QuestionFits(q,cl)) 
					then (q in qg.qs)))
					and
				 (forall (q in qg.qs) ((q.d =1) or (q.d=2) or (q.d=3)))
				)
			     ))
	and
	(if (d.three) then ((forall (q in qg.qs) q.d = 3)
				or
				((forall (q in l) (if ((q.d = 3) and QuestionFits(q,cl)) 
					then (q in qg.qs)))
					and
				 (forall (q in qg.qs) ((q.d =2) or (q.d=3) or (q.d=4)))
				)
			     ))
	and
	(if (d.four) then ((forall (q in qg.qs) q.d = 4)
				or
				((forall (q in l) (if ((q.d = 4) and QuestionFits(q,cl)) 
					then (q in qg.qs)))
					and
				 (forall (q in qg.qs) ((q.d =3) or (q.d=4) or (q.d=5)))
				)
			     ))
	and
	(if (d.five) then ((forall (q in qg.qs) q.d = 5)
				or
				((forall (q in l) (if ((q.d = 5) and QuestionFits(q,cl)) 
					then (q in qg.qs)))
					and
				 (forall (q in qg.qs) ((q.d =4) or (q.d=5)))
				)
			     ))
	(if ((d.random or d.inc) and (#qg.qs >=5)) then (forall (i:integer| (i >= 1) and (i <= 5))
			(if (exists (q in l) ((q.d = i) and QuestionFits(q,cl)))
				then (exists (q in qg.qs) q.d = i))))
	and
	(if ((d.random or d.inc) and (#qg.qs <= 4)) then (forall (q in qg.qs) 
			(forall (q2 in qg.qs) (if (q.d = q2.d) then ((q = q2) or
				(not (exists (q3 in l) (QuestionFits(q,cl) 
								and (q3.d != q.d) 
								and (not (q3 in qg.qs))))))))))
	and	
	(if (d.inc) then (forall (i:integer| (i >=0) and (i < #qg.qs)) qg.qs[i].d <= qg.qs[i+1].d)); 
	
end TestTemplate;