module Autograde;

  from Questions import all;
  from Tests import all;
  export SumLengths;

  operation GradeTest is
    inputs: t:TakenTest;
    outputs: t':GradedTest;
    description: (*
      Given a TakenTest, grade each question on the test, and return
      a GradedTest.
    *);
    precondition:
    (*
     * There are as many questions in the test as answers
     *)
      SumLengths(t.questions) = #t.answers;
    postcondition:
    (*
     * For each question on the test, grade that question.
     *)
      (forall (i:integer | i >= 1 and i <= #t.questions)
        forall (j:integer | j >= 1 and j <= #t.questions[i].qs)
          t'.s[SumLengths(t.questions[0:i]) + j] = 
            GradeQuestion(t.questions[i].qs[j], t.answers[SumLengths(t.questions[0:i]) + j]))
      and
    (*
     * The grade on the test is a sum of all the scores.
     *)
      (t'.g = SumReals(t'.s));
  end;
  
  operation GradeQuestion is
    inputs: q:Question, a:GivenAnswer;
    outputs: s:Score;
    precondition:
    (*
     * The answer is not empty
     *)
      not (a = nil);
    postcondition:
    (*
     * The question is graded according to its type
     *)
      (if (q?<MultipleChoice) then s = GradeMC(q.<MultipleChoice, a))
      (if (q?<Coding) then s = GradeCode(q.<Coding, a))
      (if (q?<Essay) then s = GradeEssay(q.<Essay, a))
      (if (q?<FillInTheBlank) then s = GradeFitB(q.<FillInTheBlank, a))
      (if (q?<TrueFalse) then s = GradeTF(q.<TrueFalse, a))
      (if (q?<Matching) then s = GradeMatch(q.<Matching, a));
  end;
  
  function GradeMC is
    inputs: q:MultipleChoice, a:GivenAnswer;
    outputs: s:Score;
    postcondition:
    (*
     * The score corresponding to the student's answer is returned
     *)
      exists (i:integer | i >= 1 and i <= #q.ans)
        if (q.ans[i].at = a) then s = (q.pv * q.ans[i].ap)/100;
  end;

  function GradeCode is
    inputs: q:Coding, a:GivenAnswer;
    outputs: s:Score;
    postcondition: 
    (*
     * The coding question is autograded based on its autograde
     * parameter - by program output, code text, or test script.
     *)
      (if (q.ans?co) then s = GradeCodeOutput(q, a))
      (if (q.ans?pt) then s = GradeEssayDesc(q, a))
      (if (q.ans?ts) then s = GradeCodeScript(q, a));
  end;
  
  function GradeCodeOutput is
    inputs: q:Coding, a:GivenAnswer;
    outputs: s:Score;
    description: (*
      Runs the given answer code and compares the output to
      the correct output.
    *);
    precondition:
    (*
     * The given code compiles and runs without errors.
     *);
    postcondition: 
    (*
     * Assign full credit if the code's output is the correct
     * output; assign no credit otherwise.
     *)
      if (q.ans.co = RunCode(a)) then s = q.pv
      else s = 0;
  end;

  function GradeCodeScript is
    inputs: q:Coding, a:GivenAnswer;
    outputs: s:Score;
    description: (*
      Runs through the grading script, and sums the total
      score for the question using the grading comments
      in the script.
    *);
    precondition:
    (*
     * The given code compiles and runs without errors.
     *);
    postcondition:
    (*
     * For each line of the grading script, check the output of
     * that line against the correct output, and if it is correct,
     * add the appropriate amount to the score.
     *)
      forall(i:integer | i >= 1 and i <= #q.ans.ts.lines)
        if (Execute(q.ans.ts.lines[i]) = q.ans.ts.result[i].at) 
          then (s = s + (q.ans.ts.result[i].ap * q.pv)/100);
  end;
  
  function RunCode is
    inputs: c:string;
    outputs: o:string;
    description: (*
      Compiles & runs the given code, and returns the output. 
      Implementation of this is outside the scope of pre & post 
      conditions.
    *);
  end;

  function Execute is
    inputs: l:string;
    outputs: o:string;
    description: (*
      Given a line to execute in a shell, executes it and returns
      the output, if any. Implementation of this is outside the scope
      of pre & post conditions.
    *);
  end;

  function GradeEssay is
    inputs: q:Essay, a:GivenAnswer;
    outputs: s:Score;
    precondition:
    (*
     * The essay's answer is a FormattedObject object.
     *)
      a?<FormattedAnswer;
    postcondition: 
    (*
     * The essay question is autograded based on its autograde
     * parameter - by keyword or by description.
     *)
      (if (q.ans?kw) then s = GradeEssayKeyword(q, a.<FormattedAnswer))
      (if (q.ans?at) then s = GradeEssayDesc(q, a));
  end;
  
  function GradeEssayKeyword is
    inputs: q:Essay, a:FormattedAnswer;
    outputs: s:Score;
    description: (*
      Highlights all keywords that appear in the student's answer.
    *);
    postcondition:
    (*
     * The text of the answer is formatted such that the keywords
     * are highlighted. This formatting itself is outside the scope of
     * this function, so this is merely a check if the student's answer
     * is a FormatedString after the autograde.
     *)
      a.fa?fs
      and
    (*
     * The score for a keyword-autograded essay question is 0 until
     * the instructor manually grades it.
     *)
      s = 0;
  end;
  
  function GradeEssayDesc is
    inputs: q:Essay, a:GivenAnswer;
    outputs: s:Score;
    description:
    (*
      Autogrades by textually comparing the entirety of the student's 
      answer with the correct answer.
    *);
    postcondition:
      (*
       * If the student's answer matches the correct answer, full credit
       * is assigned; otherwise, no credit is assigned.
       *)
      if (q.ans.at = a) then s = q.pv
      else s = 0;
  end;
  
  function GradeFitB is
    inputs: q:FillInTheBlank, a:GivenAnswer;
    outputs: s:Score;
    description:
    (*
      Checks the student's answer for each blank of the question,
      and sums together the score based on the correct blanks.
    *);
    postcondition:
    (*
     * For each blank, check answer for that blank
     * against the correct answer, and if it is correct,
     * add the appropriate amount to the score.
     *)
      forall(i:integer | i >= 1 and i < #q.ans)
        (if (ParseFitB(a)[i] = q.ans[i].at) then (s = s + (q.ans[i].ap * q.pv)/100));
  end;
  
  function ParseFitB is
    inputs: up:GivenAnswer;
    outputs: p:GivenAnswer*;
    description: (*
      Parses the given Fill-in-the-blank answer to provide a number of
      answers to check.
    *);
  end;
  
  function GradeTF is
    inputs: q:TrueFalse, a:GivenAnswer;
    outputs: s:Score;
    description: (*
      Grades a true/false question by turning the GivenAnswer into a 
      boolean and comparing it to the correct answer.
    *);
    postcondition:
      (*
       * If the student's answer is correct, assign full credit;
       * otherwise, assign no credit.
       *)
      if (q.ans = (a = "True")) then s = q.pv
      else s = 0;
  end;
  
  function GradeMatch is
    inputs: q:Matching, a:GivenAnswer;
    outputs: s:Score;
    description: (*
      Checks each answer the student provides against the correct answer
      and assigns a sum total score for the question.
    *);
    postcondition:
    (*
     * For each student answer, check the answer for that match
     * against the correct answer, and if it is correct,
     * add the appropriate amount to the score.
     *)
      forall(i:integer | i >= 1 and i < #q.ans)
        (if (ParseMatch(a)[i] = q.ans[i].at) then (s = s + (q.ans[i].ap * q.pv)/100));
  end;
  
  function ParseMatch is
    inputs: up:GivenAnswer;
    outputs: p:GivenAnswer*;
    description: (*
      Parses the given Matching answer to provide a number of
      answers to check.
    *);
  end;

  function SumReals(r:real*) =
    if (#r = 0) then 0
    else r[1] + SumReals(r[2:#r]);
  
  function SumLengths is 
    inputs: qg:QuestionGroup*;
    outputs: i:integer;
    postcondition:
      if (#qg = 0) then i=0
      else i=(#qg[1].qs + SumLengths(qg[2:#qg]));
  end;

end Autograde;