/* File  calculator.js
** Application:  new ROI/Savings Calculator app prototype.
**    
** Calculates costs for customer in payroll and bill payments
** Validates form inputs to avoid ugly 'NaN' displays
** Highlights errors in color.
** Includes utility code 'sprintf' from another source 
**        
** April 17/2008 G Rogerson
*/

/*----- Calculator Constants and Display Controls ----=*/

var cost_per_payroll_transaction =  0.15;  // $Cost of each payroll deposit
var cost_per_bill_transaction    =  0.60;  // $Cost of each bill
var telpay_payroll_min_charge    =  5.00;  // Per occurence, eg one payroll run. 
var telpay_bill_min_charge       =  0.00;  // Per month. 
var telpay_base_charge           = 15.00;  // Monthly fee. 

var useCommas = true;       // Format o/p: true = n,nnn.nn false = nnnn.nn

/*----- Global result area -----*/

/* Results here. No need to read then out of the html. */
// Bill payment sums
var gbill_per_item = 0;
var gbill_per_mon  = 0;
var gbill_telpay  = 0; 
// Payroll sums
var gpay_per_deposit = 0;
var gpay_per_mon  = 0;
var gpay_telpay  = 0; 
// Summary values (monthly charges)
var g_service_charges = 0;
var g_total_customer  = 0;
var g_total_telpay    = 0;


/*-----------------------------------------
**  calc_all_costs()
**
**  Called by form submit to update page
*/
function calc_all_costs()
{
  calc_payment_cost();
  calc_payroll_cost();
  return false;
}

/*-----------------------------------------
**  flag_error( field )
**
**  Turn surround of field pink on error
*/
function flag_error( field )
{
  field.parentNode.style.backgroundColor = "#f99";
  return true;
}

/*-----------------------------------------
**  num_ok( field )
**
**  Parameter field must be a DOM input field.
**  Validates field contains nicely construct number, eg as
**  0, 0.2, -32.105, etc, not 4.2d0.xy.90dwe;
**  Returns true if good, false if bad.
*/
function num_ok( field )
{
  var txt = field.value;
  if( txt == "." )
    return false;
  var re = /^[+-]?[0-9]*\.?[0-9]*$/;
  if( !txt.match( re ) )
    return false;
  return true;  
}



/*-----------------------------------------
** handle_validation_error()
**
** Called when a form field error is detected.
** 1) Put up alert box [ Correct errors ... ]
** 2) Set all calculated fieldds back to ? character.
** 3) Reset all calculated values to zero. 
*/
function handle_validation_error()
{
  alert("Please fix the errors in boxes that are highlighted in pink.\nNegative numbers, letters and $ signs are not allowed.");
  var f;  // html field holder
  f = document.getElementById("bill_total_mon");
  f.innerHTML = "?"; 
  f = document.getElementById("bill_total_telpay");
  f.innerHTML = "?"; 
  f = document.getElementById("pay_total_mon");
  f.innerHTML = "?"; 
  f = document.getElementById("pay_total_telpay");
  f.innerHTML = "?"; 
  f = document.getElementById("mon_telpay_service_charges");
  f.innerHTML = "?"; 
  f = document.getElementById("mon_telpay_base");
  f.innerHTML = "?"; 
  f = document.getElementById("mon_total_customer");
  f.innerHTML = "?"; 
  f = document.getElementById("mon_total_telpay");
  f.innerHTML = "?"; 
  f = document.getElementById("save_permon_telpay");
  f.innerHTML = "?"; 
  f = document.getElementById("save_peryear_telpay");
  f.innerHTML = "?"; 
            
  // Bill payment sums
  var gbill_per_item = 0;
  var gbill_per_mon  = 0;
  var gbill_telpay  = 0; 
  // Payroll sums
  var gpay_per_deposit = 0;
  var gpay_per_mon  = 0;
  var gpay_telpay  = 0; 
  // Summary values (monthly charges)
  var g_service_charges = 0;
  var g_total_customer  = 0;
  var g_total_telpay    = 0;        
}

/*-----------------------------------------
** currency_string( value )
**
** Make nice value such as "$ 7.50" for 7.5
** if global useCommas is true, it will add commas as:
**    1234567.33 becomes 1,234,567.33
*/
function currency_string( value )
{
  var dval = sprintf( "%5.2f", value );
  if( useCommas )
  {
    var pattern = /\d\d\d\d\.\d\d/;  // find last point of insertion
    var pos = dval.search( pattern );
    if( pos >= 0 ) // We need a comma.
    {
      dval = dval.substr(0,pos+1) + "," + dval.substr(pos+1,6);
      // Do all subsequent sets of three, based on first comma
      pattern = /\d\d\d\d,/;
      do
      {
        pos = dval.search( pattern )
        if( pos >= 0 )
        {
          var len = dval.length;
          dval = dval.substr(0,pos+1) + "," + dval.substr(pos+1,len-pos);
        }
      } while( pos >= 0 )
    }  
  }
  
  return "$ " + dval;
}


/*-----------------------------------------
** calc_payment_cost()
**
** Calculate and display results for Payments form
** Only the  basic TelPay transaction cost is applied for 
** the Telpay total.
**/
function calc_payment_cost()
{    
  /* Get the form inputs for customer's current payment per transaction cost */
  var payments   = document.getElementById("bill_number_of_payments");
  var bankcost   = document.getElementById("bill_bankcost_now");
  var chequecost = document.getElementById("bill_chequecost_now");
  var postcost   = document.getElementById("bill_postcost_now");
  var envcost    = document.getElementById("bill_envcost_now");
  var assemcost  = document.getElementById("bill_assemcost_now");
  var reconcost  = document.getElementById("bill_reconcost_now");
  /* Extract numeric values */
  var err = false;  // No errors
  var v_payments   = parseInt( payments.value.valueOf() );
  if( !num_ok(payments) || isNaN(v_payments) || (v_payments < 0) ) 
    err = flag_error( payments ); 
  var v_bankcost   = parseFloat( bankcost.value.valueOf() );
  if( !num_ok(bankcost) || isNaN(v_bankcost) || (v_bankcost < 0) ) 
    err = flag_error( bankcost );  
  var v_chequecost = parseFloat( chequecost.value.valueOf() );
  if( !num_ok(chequecost) || isNaN(v_chequecost) || (v_chequecost < 0) ) 
    err = flag_error( chequecost );
  var v_postcost   = parseFloat( postcost.value.valueOf() );
  if( !num_ok(postcost) || isNaN(v_postcost) || (v_postcost < 0) ) 
    err = flag_error( postcost );
  var v_envcost    = parseFloat( envcost.value.valueOf() );
  if( !num_ok(envcost) || isNaN(v_envcost) || (v_envcost < 0) )  
    err = flag_error( envcost );
  var v_assemcost  = parseFloat( assemcost.value.valueOf() );
  if( !num_ok(assemcost) || isNaN(v_assemcost) || (v_assemcost < 0) )
    err = flag_error( assemcost );
  var v_reconcost  = parseFloat( reconcost.value.valueOf() );
  if( !num_ok(reconcost) || isNaN(v_reconcost) || (v_reconcost < 0) )
    err = flag_error( reconcost );
  
  if( true == err ) // One or more errors occured.
  {
    handle_validation_error();
    return;
  } 
  
  /* Set values per month for each item */
  var out = document.getElementById("bill_bankcost_mon");
  out.innerHTML = currency_string( v_payments * v_bankcost);
   
  out = document.getElementById("bill_chequecost_mon");
  out.innerHTML = currency_string( v_payments * v_chequecost); 
  
  out = document.getElementById("bill_postcost_mon");
  out.innerHTML = currency_string( v_payments * v_postcost); 
  
  out = document.getElementById("bill_envcost_mon");
  out.innerHTML = currency_string( v_payments * v_envcost); 
  
  out = document.getElementById("bill_assemcost_mon");
  out.innerHTML = currency_string( v_payments * v_assemcost); 
  
  out = document.getElementById("bill_reconcost_mon");
  out.innerHTML = currency_string( v_payments * v_reconcost); 

  /* payments * sum( everything else ) */
  gbill_per_item = v_bankcost + v_chequecost + v_postcost + v_envcost + v_assemcost + v_reconcost;
  gbill_per_mon =  gbill_per_item * v_payments;
  /* Display summary result for payments */
  var mon = document.getElementById("bill_totalpay_now");
  /* Optional: show per-item summary:
     mon.innerHTML = currency_string( gbill_per_item );
  */
  mon = document.getElementById("bill_total_mon");
  mon.innerHTML = currency_string(  gbill_per_mon );

  /* Display Telpay cost per month */
  mon = document.getElementById("bill_total_telpay");
  gbill_telpay = v_payments * cost_per_bill_transaction;
  if( gbill_telpay < telpay_bill_min_charge ) 
    gbill_telpay = telpay_bill_min_charge;
  mon.innerHTML = currency_string( gbill_telpay );
 document.getElementById("bill_tt_top").innerHTML = mon.innerHTML;
 
  /*--- Update the rest of the page as required ---*/
  update_monthly_costs();
}


/*-----------------------------------------
** calc_payroll_cost()
** Calculate and display results for Payments form
**/
function calc_payroll_cost()
{
  /* Get the form inputs for customer's current paypayroll per transaction cost */
  
  var payrolls   = document.getElementById("pay_payroll_count");
  var deposits   = document.getElementById("pay_deposits_count");
  var bankcost   = document.getElementById("pay_bankcost_now");
  var chequecost = document.getElementById("pay_chequecost_now");
  var envcost    = document.getElementById("pay_envcost_now");
  var assemcost  = document.getElementById("pay_assemcost_now");
  var reconcost  = document.getElementById("pay_reconcost_now");
  /* Extract numeric values */
  var err = false;  // No errors
  var v_payrolls   = parseFloat( payrolls.value.valueOf() );
  if( !num_ok(payrolls) || isNaN(v_payrolls) || (v_payrolls < 0) )    
    err = flag_error( payrolls ); 
  var v_deposits   = parseInt( deposits.value.valueOf() );
  if( !num_ok(deposits) || isNaN(v_deposits) || (v_deposits < 0) )
    err = flag_error( deposits );
  var v_bankcost   = parseFloat( bankcost.value.valueOf() );
  if( !num_ok(bankcost) || isNaN(v_bankcost) || (v_bankcost < 0) )  
    err = flag_error( bankcost );
  var v_chequecost = parseFloat( chequecost.value.valueOf() );
  if( !num_ok(chequecost) || isNaN(v_chequecost) || (v_chequecost < 0) )
    err = flag_error( chequecost );
  var v_envcost    = parseFloat( envcost.value.valueOf() );
  if( !num_ok(envcost) || isNaN(v_envcost) || (v_envcost < 0) )   
    err = flag_error( envcost );
  var v_assemcost  = parseFloat( assemcost.value.valueOf() );
  if( !num_ok(assemcost) || isNaN(v_assemcost) || (v_assemcost < 0) ) 
    err = flag_error( assemcost );
  var v_reconcost  = parseFloat( reconcost.value.valueOf() );
  if( !num_ok(reconcost) || isNaN(v_reconcost) || (v_reconcost < 0) ) 
    err = flag_error( reconcost );

  if( true == err ) // One or more errors occured.
  {
    handle_validation_error();
    return;
  } 

  var total_deposits = v_payrolls * v_deposits;
  /* Set values per month for each item */
  var out = document.getElementById("pay_bankcost_mon");
  out.innerHTML = currency_string( total_deposits * v_bankcost);
   
  out = document.getElementById("pay_chequecost_mon");
  out.innerHTML = currency_string( total_deposits * v_chequecost); 
  
  
  out = document.getElementById("pay_envcost_mon");
  out.innerHTML = currency_string( total_deposits * v_envcost); 
  
  out = document.getElementById("pay_assemcost_mon");
  out.innerHTML = currency_string( total_deposits * v_assemcost); 
  
  out = document.getElementById("pay_reconcost_mon");
  out.innerHTML = currency_string( total_deposits * v_reconcost); 

  /* total_deposits * sum( everything else ) */

  gpay_per_deposit = v_bankcost + v_chequecost + v_envcost + v_assemcost + v_reconcost;
  var per_payroll_cycle =  gpay_per_deposit * v_deposits;
  /* Display summary result for payments */
  var mon = document.getElementById("pay_total_now");
  /* Optional: show per-item summary:
     mon.innerHTML = currency_string( gpay_per_deposit );
  */
  gpay_per_mon = v_payrolls * per_payroll_cycle;
  mon = document.getElementById("pay_total_mon");
  mon.innerHTML = currency_string(  gpay_per_mon );
  
  /* Display Telpay cost per month */
  gpay_telpay = v_deposits * cost_per_payroll_transaction;
  /* Minimums charged on a per-payroll basis */
  if( gpay_telpay < telpay_payroll_min_charge ) 
    gpay_telpay = telpay_payroll_min_charge;
  gpay_telpay = gpay_telpay * v_payrolls;  /* Cost of all payrolls */
  /* Display result */
  mon = document.getElementById("pay_total_telpay"); 
  mon.innerHTML = currency_string( gpay_telpay );
  document.getElementById("pay_tt_top").innerHTML = mon.innerHTML;
  
  /*--- Update the rest of the page as required ---*/
  update_monthly_costs();
}


/*-----------------------------------------
**  update_monthly_costs()
**
**  If all monthly costs have been calculated in the Payroll
**  and Payment calculators, the final monthly totals will
**  be shown.
**  If only one has ben calculated, process it and assume the 
**  other is zero / not used.
**  If neither have been calculated, display an alert box,
**  but do not calculate anything.
**/
function update_monthly_costs()
{
 
  g_service_charges = gbill_telpay + gpay_telpay;
  g_total_customer  = gbill_per_mon + gpay_per_mon;
  g_total_telpay    = g_service_charges + telpay_base_charge;
  // Outputs
  //var mon = document.getElementById("mon_telpay_service_charges");
  //mon.innerHTML = currency_string( g_service_charges );
  mon = document.getElementById("mon_total_customer"); 
  mon.innerHTML = currency_string( g_total_customer  );
  mon = document.getElementById("mon_telpay_base");
  mon.innerHTML = currency_string( telpay_base_charge );
  mon = document.getElementById("mon_total_telpay");
  mon.innerHTML = currency_string( g_total_telpay );
  
  update_savings_summary();  // Show cumulative savings
}

/*-----------------------------------------
**  update_savings_summary()
**
**  This routine shall be called from update_montly_costs()
**  to avoid duplicate validation.
**  Savings values are calculated and displayed.
*/
function update_savings_summary()
{
  var saving = g_total_customer - g_total_telpay;
  
  var mon = document.getElementById("save_permon_telpay");
  mon.innerHTML = currency_string( saving );

  mon = document.getElementById("save_peryear_telpay");
  mon.innerHTML = currency_string( saving * 12 );
  
  if( saving <= 0 )  // Do we want to say something about that?
  {
    ;
  }
} 

/*-----------------------------------------
** check_num( id )
**
** Event handler to validate form field input
** If filed is NaN, set container red and set blockCalculation
** otherwise set container white and clear blockCalculation
**/
function check_num( id )
{
  // Extract field data, and if NaN:
  //  set containing  red and attempt to force focus
  // Otherwise,  make field the normal background color.
  var field = document.getElementById( id );
  var v = parseFloat( field.value.valueOf() );
  var fx = -1;
  if( !num_ok(field) || isNaN(v) || (v < 0) )
  {
    field.parentNode.style.backgroundColor = "#f99";
    /* Force focus back into the input box. Browser dependent:
       focus() not work in Firefox.
    */
    if (window.opera) /* Opera OK, but does not like select(). */
    {
      field.focus();
    }
    else  // Firefox and IE can use select() to force focus.
    {
      /* Identify which form the input field is in */
      for( var num = 0; num < document.forms.length; num++ )
      {
        var f = document.forms[num];
        for (var i=0; i<f.length; i++) /* scan for matching field */
        {
         if( f.elements[i] == field ) /* Form i has our field. */
         {
           var fx = num; /* Need form number */
           break; 
         }
        }
      }
      /* Force focus the only way the works in Firefox */
      setTimeout("document.forms["+fx+"]."+field.name+".select();",0);
    }
  }
  else  // Input field contains a good number - reset bg color 
  {
    field.parentNode.style.backgroundColor = "#fff";
  }
}  

/* End of TelPay code */

/**---------------------------------------------------------------------------
*
* Javascript sprintf (unencumbered code. Ref: http://www.webtoolkit.info/
*
*
**/

sprintfWrapper = {

    init : function () {

        if (typeof arguments == "undefined") { return null; }
        if (arguments.length < 1) { return null; }
        if (typeof arguments[0] != "string") { return null; }
        if (typeof RegExp == "undefined") { return null; }

        var string = arguments[0];
        var exp = new RegExp(/(%([%]|(\-)?(\+|\x20)?(0)?(\d+)?(\.(\d)?)?([bcdfosxX])))/g);
        var matches = new Array();
        var strings = new Array();
        var convCount = 0;
        var stringPosStart = 0;
        var stringPosEnd = 0;
        var matchPosEnd = 0;
        var newString = '';
        var match = null;

        while (match = exp.exec(string)) {
            if (match[9]) { convCount += 1; }

            stringPosStart = matchPosEnd;
            stringPosEnd = exp.lastIndex - match[0].length;
            strings[strings.length] = string.substring(stringPosStart, stringPosEnd);

            matchPosEnd = exp.lastIndex;
            matches[matches.length] = {
                match: match[0],
                left: match[3] ? true : false,
                sign: match[4] || '',
                pad: match[5] || ' ',
                min: match[6] || 0,
                precision: match[8],
                code: match[9] || '%',
                negative: parseInt(arguments[convCount]) < 0 ? true : false,
                argument: String(arguments[convCount])
            };
        }
        strings[strings.length] = string.substring(matchPosEnd);

        if (matches.length == 0) { return string; }
        if ((arguments.length - 1) < convCount) { return null; }

        var code = null;
        var match = null;
        var i = null;

        for (i=0; i<matches.length; i++) {

            if (matches[i].code == '%') { substitution = '%' }
            else if (matches[i].code == 'b') {
                matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(2));
                substitution = sprintfWrapper.convert(matches[i], true);
            }
            else if (matches[i].code == 'c') {
                matches[i].argument = String(String.fromCharCode(parseInt(Math.abs(parseInt(matches[i].argument)))));
                substitution = sprintfWrapper.convert(matches[i], true);
            }
            else if (matches[i].code == 'd') {
                matches[i].argument = String(Math.abs(parseInt(matches[i].argument)));
                substitution = sprintfWrapper.convert(matches[i]);
            }
            else if (matches[i].code == 'f') {
                matches[i].argument = String(Math.abs(parseFloat(matches[i].argument)).toFixed(matches[i].precision ? matches[i].precision : 6));
                substitution = sprintfWrapper.convert(matches[i]);
            }
            else if (matches[i].code == 'o') {
                matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(8));
                substitution = sprintfWrapper.convert(matches[i]);
            }
            else if (matches[i].code == 's') {
                matches[i].argument = matches[i].argument.substring(0, matches[i].precision ? matches[i].precision : matches[i].argument.length)
                substitution = sprintfWrapper.convert(matches[i], true);
            }
            else if (matches[i].code == 'x') {
                matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(16));
                substitution = sprintfWrapper.convert(matches[i]);
            }
            else if (matches[i].code == 'X') {
                matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(16));
                substitution = sprintfWrapper.convert(matches[i]).toUpperCase();
            }
            else {
                substitution = matches[i].match;
            }

            newString += strings[i];
            newString += substitution;

        }
        newString += strings[i];

        return newString;

    },

    convert : function(match, nosign){
        if (nosign) {
            match.sign = '';
        } else {
            match.sign = match.negative ? '-' : match.sign;
        }
        var l = match.min - match.argument.length + 1 - match.sign.length;
        var pad = new Array(l < 0 ? 0 : l).join(match.pad);
        if (!match.left) {
            if (match.pad == "0" || nosign) {
                return match.sign + pad + match.argument;
            } else {
                return pad + match.sign + match.argument;
            }
        } else {
            if (match.pad == "0" || nosign) {
                return match.sign + match.argument + pad.replace(/0/g, ' ');
            } else {
                return match.sign + match.argument + pad;
            }
        }
    }
}

sprintf = sprintfWrapper.init;
