//
// $Id: gpacalc.js 217 2005-08-05 00:25:20Z hncaldwell $
//
// Heath Caldwell
// hncaldwell@csupomona.edu
// Studio 6, I&IT - Cal Poly Pomona
//
////////////////////////////////////////////////////////////
//
// Copyright 2005 California State Polytechnic University, Pomona
//
// This is free software; you can redistribute it and/or
// modify it under the terms of:
//
//      The GNU General Public License as published by the
//      Free Software Foundation; either version 2, or
//      (at your option) any later version, obtainable from:
//      http://www.gnu.org/licenses/gpl.txt
//
////////////////////////////////////////////////////////////
//
// Description:
//
//   A javascript tool to perform calculations regarding
//   a student's grade point average. 
//

var gpacalc = new Object();

/////////////////////////////////////////////
///// gpacalc Object Definition /////////////
/////////////////////////////////////////////

//////////////////////////////
// Static Variables //////////
//////////////////////////////

gpacalc.button_text =
{
  help_on:               "Turn off help text",
  help_off:              "Turn on help text"
}

gpacalc.messages =
{
  inconsistent:          "Inconsistent data.",
  not_enough_info:       "Not enough information.",
  impossible_gpa:        "GPA out of range.",
  target_impossible_gpa: "Target GPA out of range.",
  target_invalid_gpa:    "Invalid target GPA.",
  current_inconsistent:  "Inconsistent current data (at top).",
  add_inconsistent:      "Future work inconsistent.",
  add_not_enough_info:   "Not enough information.",
  add_impossible_gpa:    "Impossible future work GPA.",
  impossible:            "Impossible",
  error:                 "Error:",
  not_a_number:          "Please enter only numbers.",
  data_ok:               "Data entered.",
  success:               "Successfully calculated."
}

gpacalc.shared_form_id   = "current_data";
gpacalc.help_class_name  = "helptext";

gpacalc.round_digits          = 2;
gpacalc.consistency_tolerance = 0.01;
gpacalc.max_units             = 9999;

// Grade values:
gpacalc.a       = 4.0;
gpacalc.a_minus = 3.7;
gpacalc.b_plus  = 3.3;
gpacalc.b       = 3.0;
gpacalc.b_minus = 2.7;
gpacalc.c_plus  = 2.3;
gpacalc.c       = 2.0;
gpacalc.c_minus = 1.7;
gpacalc.d_plus  = 1.3;
gpacalc.d       = 1.0;
gpacalc.d_minus = 0.7;
gpacalc.f       = 0.0;
//

//// Special methods to change the constants between
//// the ones needed for graduate and for undergraduate
//// student calculations.

// Set constants to values needed for graduate student
// calculations.
gpacalc.set_graduate_values = function()
{
  gpacalc.gpb_center = 3.0;
}
//

// Set constants to values needed for undergraduate
// student calculations.
gpacalc.set_undergraduate_values = function()
{
  gpacalc.gpb_center = 2.0;
}
//

// Default to using undergraduate values.
gpacalc.set_undergraduate_values();

////

//////////////////////////////
// End Static Variables //////
//////////////////////////////



//////////////////////////////
// Methods ///////////////////
//////////////////////////////

// Previously rounded 'number', but now truncates it to
// gpacalc.round_digits digits after the decimal.
gpacalc.round = function(number)
{
  var multiplier = Math.pow(10, gpacalc.round_digits);

  return Math.floor(number * multiplier) / multiplier;
};
//

gpacalc.strip = function(input)
{
  var result = "";

  if(input.charAt)
  {
    for(var i=0; i < input.length; i++)
    {
      if(input.charAt(i) != " ")
        result = result + input.charAt(i);
    }
  }

  return result;
};

gpacalc.get_by_id = function(id)
{
  if(id)
  {
    if(document.getElementById)
      return document.getElementById(id);
  }

  return null;
};

// Write to a read only element.
// This is to get around the problem with IE for mac where
// it won't draw the updated value in a read only element.
gpacalc.write_to_ro = function(element, value)
{
  if(element && element.readOnly)
  {
    element.readOnly = false;
    element.value = value;
    element.readOnly = true;
  }
};
//

gpacalc.auto_calc_third = function(form, u_input, gp_input, gpa_input)
{
  if(form && form[u_input] && form[gp_input] && form[gpa_input])
  {
    var message = "";

    var units        = form[u_input].value;
    var grade_points = form[gp_input].value;
    var gpa          = form[gpa_input].value;

    parsed_units        = parseFloat(units);
    parsed_grade_points = parseFloat(grade_points);
    parsed_gpa          = parseFloat(gpa);
    if(isNaN(parsed_gpa))
    {
      var number = gpacalc.lettergrade_to_number(gpa);

      if(number >= 0)
      {
        gpa = number;
        form[gpa_input].value = number;
        parsed_gpa = number;
      }
    }

    if((units         != "" && isNaN(parsed_units)) ||
       (grade_points  != "" && isNaN(parsed_grade_points)) ||
       (gpa           != "" && isNaN(parsed_gpa)))
    {
      gpacalc.write_to_ro(form.message, gpacalc.messages.not_a_number);
      return;
    }

    var message;

    if(!units && grade_points && gpa)
    {
      parsed_units = gpacalc.round(parsed_grade_points / parsed_gpa);
      form[u_input].value = parsed_units;
   
      message = gpacalc.messages.data_ok;
    }
    else if(!grade_points && units && gpa)
    {
      parsed_grade_points = gpacalc.round(parsed_units * parsed_gpa);
      form[gp_input].value = parsed_grade_points;

      message = gpacalc.messages.data_ok;
    }
    else if(!gpa && grade_points && units)
    {
      parsed_gpa = gpacalc.round(parsed_grade_points / parsed_units);
      form[gpa_input].value = parsed_gpa;

      message = gpacalc.messages.data_ok;
    }
    else if(gpa && grade_points && units)
    {
      if(parsed_grade_points / parsed_units > 
           parsed_gpa + gpacalc.consistency_tolerance ||
         parsed_grade_points / parsed_units <
           parsed_gpa - gpacalc.consistency_tolerance)
      {
        message = gpacalc.messages.inconsistent;
      }
      else
      {
        message = gpacalc.messages.data_ok;
      }
    }

    if(parsed_gpa < 0.0 || parsed_gpa > 4.0)
    {
      message = gpacalc.messages.impossible_gpa;
    }

    // If this is the top set of values, calculate the grade
    // point balance and put it in its spot.  This is
    // a bit filty, but it is the only place to do it.
    if(message == gpacalc.messages.data_ok && form["top_gpbalance"])
    {
      var gpb = parsed_grade_points - (gpacalc.gpb_center * parsed_units);
      gpacalc.write_to_ro(form["top_gpbalance"], gpacalc.round(gpb));
    }
    //

    gpacalc.write_to_ro(form.message, message);
  }
};

gpacalc.toggle_help = function(button)
{
  if(button)
  {
    if(button.value == gpacalc.button_text.help_off)
    {
      gpacalc.toggle_help_in_branch(document.documentElement, 1);
      button.value = gpacalc.button_text.help_on;
    }
    else
    {
      gpacalc.toggle_help_in_branch(document.documentElement, 0);
      button.value = gpacalc.button_text.help_off;
    }
  }
};

gpacalc.toggle_help_in_branch = function(element, on)
{
  if(element && element.nodeType == 1)
  {
    if(element.className == gpacalc.help_class_name && element.style)
    {
      if(on)
      {
        if(element.tagName == "SPAN")
          element.style.display = "inline";
        else
	  element.style.display = "block";
      }
      else
      {
        element.style.display = "none";
      }
    }

    var child = element.firstChild;
    while(child)
    {
      gpacalc.toggle_help_in_branch(child, on);
      child = child.nextSibling;
    }
  }
};

// Takes a string containing a letter grade and
// returns the corresponding grade point value for
// that letter grade.
// Returns -1 if the string doesn't contain a valid letter grade.
gpacalc.lettergrade_to_number = function(lettergrade)
{
  var number = -1;

  switch(gpacalc.strip(lettergrade))
  {
    case "A":
    case "a":
      number = gpacalc.a;
      break;
    case "A-":
    case "a-":
      number = gpacalc.a_minus;
      break;
    case "B+":
    case "b+":
      number = gpacalc.b_plus;
      break;
    case "B":
    case "b":
      number = gpacalc.b;
      break;
    case "B-":
    case "b-":
      number = gpacalc.b_minus;
      break;
    case "C+":
    case "c+":
      number = gpacalc.c_plus;
      break;
    case "C":
    case "c":
      number = gpacalc.c;
      break;
    case "C-":
    case "c-":
      number = gpacalc.c_minus;
      break;
    case "D+":
    case "d+":
      number = gpacalc.d_plus;
      break;
    case "D":
    case "d":
      number = gpacalc.d;
      break;
    case "D-":
    case "d-":
      number = gpacalc.d_minus;
      break;
    case "F":
    case "f":
      number = gpacalc.f;
      break;
    default:
      number = -1;
      break;
  }

  return number;
};

/////////////////////////////////////////////
///// Replacement Courses Module ////////////
/////////////////////////////////////////////

gpacalc.replacement_submit = function(form)
{
  var message;

  var shared_form = gpacalc.get_by_id(gpacalc.shared_form_id);

  if(form        && form.result &&
     shared_form && shared_form.units &&
                    shared_form.grade_points &&
                    shared_form.gpa)
  {
    var error = false;

    var units        = parseFloat(shared_form.units.value);
    var grade_points = parseFloat(shared_form.grade_points.value);
    var gpa          = parseFloat(shared_form.gpa.value);

    if(gpa && grade_points && units)
    {
      if(grade_points / units > gpa + gpacalc.consistency_tolerance ||
         grade_points / units < gpa - gpacalc.consistency_tolerance)
      {
        message = gpacalc.messages.current_inconsistent;
      }
    }
    else
    {
      message = gpacalc.messages.not_enough_info;
    }

    if(gpa < gpacalc.f || gpa > gpacalc.a)
    {
      message = gpacalc.messages.impossible_gpa;
    }

    if(message)
    {
      gpacalc.write_to_ro(form.message, message);
      gpacalc.write_to_ro(form.result, "");
    }
    else
    {
      for(var i=1; i <= 4; i++)
      {
        if(form["c_grade"   + i] &&
           form["new_grade" + i] &&
	   form["c_grade"   + i].selectedIndex > 0 &&
           form["new_grade" + i].selectedIndex > 0)
        {
          var add_grade;
          var sub_grade;

          switch(form["c_grade" + i].selectedIndex)
          {
            case 1: // C
              sub_grade = gpacalc.c;
              break;
            case 2: // C-
              sub_grade = gpacalc.c_minus;
              break;
            case 3: // D+
              sub_grade = gpacalc.d_plus;
              break;
            case 4: // D
              sub_grade = gpacalc.d;
              break;
            case 5: // D-
              sub_grade = gpacalc.d_minus;
              break;
            case 6: // F, Fall through.
            case 7: // WU, Fall through.
            default:
              sub_grade = 0.0;
              break;
          }

          switch(form["new_grade" + i].selectedIndex)
          {
            case 1: // A
              add_grade = gpacalc.a;
              break;
            case 2: // A-
              add_grade = gpacalc.a_minus;
              break;
            case 3: // B+
              add_grade = gpacalc.b_plus;
              break;
            case 4: // B
              add_grade = gpacalc.b;
              break;
            case 5: // B-
              add_grade = gpacalc.b_minus;
              break;
            case 6: // C+
              add_grade = gpacalc.c_plus;
              break;
            case 7: // C
              add_grade = gpacalc.c;
              break;
            case 8: // C-
              add_grade = gpacalc.c_minus;
              break;
            case 9: // D+
              add_grade = gpacalc.d_plus;
              break;
            case 10: // D
              add_grade = gpacalc.d;
              break;
            case 11: // D-
              add_grade = gpacalc.d_minus;
              break;
            case 12: // F, Fall through.
            default:
              add_grade = gpacalc.f;
              break;
          }

          // Get grade_units, 1 is added because the selections are 0 indexed.
          var grade_units = 0;
          if(form["c_units" + i])
            grade_units = form["c_units" + i].selectedIndex + 1;
          //

          if(form["units_included" + i] &&
	     form["units_included" + i].checked)
          {
            units        -= grade_units;
            grade_points -= sub_grade * grade_units;
          }
          else
          {
            grade_points -= sub_grade * grade_units;
            grade_points += add_grade * grade_units;
          }
        }
      }

      var result = grade_points / units;
      gpacalc.write_to_ro(form.result, gpacalc.round(result));

      if(form["gpbalance"])
      {
        var gpb = grade_points - (gpacalc.gpb_center * units);
        gpacalc.write_to_ro(form["gpbalance"], gpacalc.round(gpb));
      }

      gpacalc.write_to_ro(form.message, gpacalc.messages.success);
    }
  }

  return false;
};

/////////////////////////////////////////////
///// End Replacement Courses Module ////////
/////////////////////////////////////////////



/////////////////////////////////////////////
///// Additional Coursework Module //////////
/////////////////////////////////////////////

gpacalc.additional_submit = function(form)
{
  var message;

  var shared_form = gpacalc.get_by_id(gpacalc.shared_form_id);

  if(form        && form.result &&
     shared_form && shared_form.units &&
                    shared_form.grade_points &&
                    shared_form.gpa)
  {
    var error = false;

    var units        = parseFloat(shared_form.units.value);
    var grade_points = parseFloat(shared_form.grade_points.value);
    var gpa          = parseFloat(shared_form.gpa.value);

    if(gpa && grade_points && units)
    {
      if(grade_points / units > gpa + gpacalc.consistency_tolerance ||
         grade_points / units < gpa - gpacalc.consistency_tolerance)
      {
        message = gpacalc.messages.current_inconsistent;
      }
    }
    else
    {
      message = gpacalc.messages.not_enough_info;
    }

    if(gpa < gpacalc.f || gpa > gpacalc.a)
    {
      message = gpacalc.messages.impossible_gpa;
    }

    if(message)
    {
      gpacalc.write_to_ro(form.message, message);
      gpacalc.write_to_ro(form.result, "");

      if(form["gpbalance"])
      {
        gpacalc.write_to_ro(form["gpbalance"], "");
      }
    }
    else if(form.units && form.grade_points && form.gpa)
    {
      var add_units        = parseFloat(form.units.value);
      var add_grade_points = parseFloat(form.grade_points.value);
      var add_gpa          = parseFloat(form.gpa.value);

      if(form.units.value != "" &&
         form.grade_points.value != "" &&
         form.gpa.value != "")
      {
        if(add_grade_points / add_units > add_gpa + gpacalc.consistency_tolerance ||
           add_grade_points / add_units < add_gpa - gpacalc.consistency_tolerance)
        {
          message = gpacalc.messages.add_inconsistent;
        }
      }
      else
      {
        message = gpacalc.messages.add_not_enough_info;
      }

      if(add_gpa < gpacalc.f || add_gpa > gpacalc.a)
      {
        message = gpacalc.messages.add_impossible_gpa;
      }

      if(message)
      {
        gpacalc.write_to_ro(form.message, message);
        gpacalc.write_to_ro(form.result, "");

        if(form["gpbalance"])
        {
          gpacalc.write_to_ro(form["gpbalance"], "");
        }
      }
      else
      {
        var result = (grade_points + add_grade_points) / (units + add_units);
        gpacalc.write_to_ro(form.result, gpacalc.round(result));

        if(form["gpbalance"])
        {
          var gpb = (grade_points + add_grade_points) -
                    (gpacalc.gpb_center * (units + add_units));
          gpacalc.write_to_ro(form["gpbalance"], gpacalc.round(gpb));
        }

        gpacalc.write_to_ro(form.message, gpacalc.messages.success);
      }
    }
  }

  return false;
};

/////////////////////////////////////////////
///// End Additional Coursework Module //////
/////////////////////////////////////////////



/////////////////////////////////////////////
///// Target GPA Module /////////////////////
/////////////////////////////////////////////

gpacalc.target_submit = function(form)
{
  var message;

  var shared_form = gpacalc.get_by_id(gpacalc.shared_form_id);

  if(form        && form.target_gpa &&
     shared_form && shared_form.units &&
                    shared_form.grade_points &&
                    shared_form.gpa)
  {
    var error = false;

    var units        = parseFloat(shared_form.units.value);
    var grade_points = parseFloat(shared_form.grade_points.value);
    var gpa          = parseFloat(shared_form.gpa.value);

    var target_gpa;
    if(form.target_gpa.value == "")
      target_gpa = 0;
    else
    {
      target_gpa = parseFloat(form.target_gpa.value);
      if(isNaN(target_gpa))
      {
        target_gpa = gpacalc.lettergrade_to_number(form.target_gpa.value);
      }
    }

    if(target_gpa <= gpacalc.f || target_gpa >= gpacalc.a)
    {
      message = gpacalc.messages.target_impossible_gpa;
    }

    if(target_gpa < gpacalc.f)
    {
      message = gpacalc.messages.target_invalid_gpa;
    }
    else
    {
      if(gpa && grade_points && units)
      {
        if(grade_points / units > gpa + gpacalc.consistency_tolerance ||
           grade_points / units < gpa - gpacalc.consistency_tolerance)
        {
          message = gpacalc.messages.current_inconsistent;
        }
      }
      else
      {
        message = gpacalc.messages.not_enough_info;
      }

      if(gpa < gpacalc.f || gpa > gpacalc.a)
      {
        message = gpacalc.messages.impossible_gpa;
      }
    }

    var necessary_units = new Object();
    necessary_units["a"]       = "---";
    necessary_units["a_minus"] = "---";
    necessary_units["b_plus"]  = "---";
    necessary_units["b"]       = "---";
    necessary_units["b_minus"] = "---";
    necessary_units["c_plus"]  = "---";
    necessary_units["c"]       = "---";

    if(message)
    {
      gpacalc.write_to_ro(form.message, message);
    }
    else
    {
      necessary_units["a"]       = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.a);
      necessary_units["a_minus"] = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.a_minus);
      necessary_units["b_plus"]  = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.b_plus);
      necessary_units["b"]       = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.b);
      necessary_units["b_minus"] = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.b_minus);
      necessary_units["c_plus"]  = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.c_plus);
      necessary_units["c"]       = gpacalc.target_calc_units(grade_points,
                                                             target_gpa,
                                                             units, gpacalc.c);

      gpacalc.write_to_ro(form.message, gpacalc.messages.success);
    }

    for(var grade in necessary_units)
    {
      if(!message)
      {
        if(isNaN(necessary_units[grade]))
          necessary_units[grade] = 0;

        if(necessary_units[grade] < 0 || necessary_units[grade] > gpacalc.max_units)
          necessary_units[grade] = gpacalc.messages.impossible;
        else
          necessary_units[grade] = Math.ceil(necessary_units[grade]);
      }
 
      var element = gpacalc.get_by_id(grade + "_units");
      if(element)
      {
        // Remove all children text nodes from element.
        var child = element.firstChild;
        while(child)
        {
          if(child.nodeType == 3)
          {
            var next_child = child.nextSibling;
            child = element.removeChild(child);
            child = next_child;
          }
          else
            child = child.nextSibling;
        }
        //

        element.appendChild(document.createTextNode(necessary_units[grade]));
      }
    }
  }

  return false;
};

gpacalc.target_calc_units = function(grade_points, target_gpa, units, grade)
{
  return (grade_points - (target_gpa * units)) / (target_gpa - grade);
};

gpacalc.target_clear_units = function()
{
  var grades = ["a", "a_minus", "b_plus", "b", "b_minus", "c_plus", "c"];

  for(var i=0; i < grades.length; i++)
  {
    var element = gpacalc.get_by_id(grades[i] + "_units");
    if(element)
    {
      // Remove all children text nodes from element.
      var child = element.firstChild;
      while(child)
      {
        if(child.nodeType == 3)
        {
          var next_child = child.nextSibling;
          child = element.removeChild(child);
          child = next_child;
        }
        else
          child = child.nextSibling;
      }
      //

      element.appendChild(document.createTextNode("---"));
    }
  }
}

/////////////////////////////////////////////
///// End Target GPA Module /////////////////
/////////////////////////////////////////////

//////////////////////////////
// End Methods ///////////////
//////////////////////////////

