//CVS:       $Id: nutrient_calculator.js,v 1.17 2011/10/25 22:33:29 cvsdevel Exp $
//Title:     botanicare_calculator.js
//Version:   1.00
//Copyright: Copyright (c) 2011
//Author:    Donovan Mueller (donovan@rhinointernet.com)
//Company:   Rhino Internet

/**
 * Javascript library for the Botanicare nutrient calculator.
 *
 * <p>
 * <b>Changelog:</b><pre>
 *  1.00   DDM 2011/06/30   created.
 * </pre>
 *
 * @author  DDM
 * @version 1.00
 */

/* jQuery dependent code encapsulation
   ------------------------------------------------------------------------- */
(function NutrientCalculator($)
{

   var $settings = {
      nutrients: {
         units: { ml: ['mL', 1], oz: ['oz', 29.5735296] },
         // [<display>, <divisor>]
         default_units: 'ml'
      },
      reservoir: {
         units: { gal: ['gal', 1], l: ['liter', 3.78541178] },
         // [<display>, <divisor>]
         default_units: 'gal'
      },
      time: {
         units: ['Week', 'Weeks'],
         // [<singular>, <plural>]
         grow: { min: 4, max: 8 },
         bloom: { min: 6, max: 10 }
      },
      ppm_warning_trigger: 1800
   }; // settings{}
   // Bring in settings from the global calculator object.
   $settings = $.extend({}, Calculator.settings, $settings);

   var $nutrients = Calculator.nutrients;

   var $recipes = {
      'Power Plant': {
         base_nutrients: { grow: 'Power Plant', bloom: 'Power Flower' },
         time: { grow: 4, bloom: 7 },
         supplements:
            ['Pure Blend Original Grow', 'Pure Blend Original Bloom',
              'Liquid Karma', 'Sweet', 'Hydroplex', 'Cal-Mag Plus', 'Clearex',
              'ZHO']
      }, // Power Plant
      'Pure Blend': {
         base_nutrients:
            { grow: 'Pure Blend Pro Grow', bloom: 'Pure Blend Pro Bloom' },
         time: { grow: 4, bloom: 7 },
         supplements:
            ['Liquid Karma', 'Sweet', 'Hydroplex', 'Blast Off',
              'Cal-Mag Plus', 'Clearex', 'ZHO']
      }, // Pure Blend
      'CNS-17 Grow': {
         base_nutrients:
            { grow: 'CNS 17 Grow', bloom: 'CNS 17 Bloom' },
         time: { grow: 4, bloom: 7 },
         supplements:
            ['Liquid Karma', 'Sweet', 'Hydroplex', 'Blast Off',
              'Cal-Mag Plus', 'Clearex', 'ZHO']
      } // CNS-17 Grow
   }; // recipes

   // Declare some variables for use in this scope.
   var $wrap, $controls, $supplements, $recipe_list, $fields, $results,
       $growbox, $bloombox, $ppm_chart, $ppm_chart_values, $warnings = {},
       $overrides = { grow: {}, bloom: {} };
   // List of overriden values for specific nutrients.


   /** When the DOM is ready...
   --------------------------------------------------------------------- */
   $(document).ready(function onDOMReady()
   {
      $('#content.calculator').not('.predefined.recipes').each(function setupCalculator()
      {
         $wrap = $(this);

         // Let everyone know JavaScript is available.
         $wrap.addClass('js-available');

         // Get the wrappers for the various fields.
         $controls = $wrap.find('#overlay.calculator');
         $supplements = $controls.find('.menu.supplements');
         $recipe_list = $controls.find('.menu.recipes');

         // Build list of supplements and add them to the page.
         var list = [],
             obj = {};
         $.extend( // Adds grow and bloom supplement objects to obj.
            obj, $nutrients.grow.supplements, $nutrients.bloom.supplements
         );
         for (var s in obj) {
            list.push(s);
         }
         list.sort();
         $supplements.empty(); // Clear loading message.
         for (var s = 0; s < list.length; s++) {
            var li = $('<li>').
               addClass((s % 2 === 0) ? 'odd' : 'even');
            $('<input type="checkbox" name="supplements[]" />').
               attr('id', 'fSupplement' + s).
               attr('value', list[s]).
               appendTo(li);
            $('<label>').
               attr('for', 'fSupplement' + s).
               html(list[s]).
               appendTo(li);
            $supplements.append(li);
         }

         // Get handles on all of the fields.
         $fields = {
            base_nutrients: {
               grow: $controls.find('#fGrowBaseNutrient'),
               bloom: $controls.find('#fBloomBaseNutrient')
            },
            nutrient_units: $controls.find('#fNutrientUnits'),
            reservoir: {
               size: $controls.find('#fReservoirSize'),
               units: $controls.find('#fReservoirUnits')
            },
            time: {
               grow: $controls.find('#fGrowTime'),
               bloom: $controls.find('#fBloomTime')
            },
            supplements: $supplements.find(':checkbox')
         }; // var fields

         // Where the results will go.
         $results = $wrap.children('.outline-content');
         $growbox = $results.find('.phase.grow');
         $bloombox = $results.find('.phase.bloom');
         $ppm_chart = $results.find('#PPMChart');

         // Build fields
         buildNutrientOptions($fields.base_nutrients.grow, $nutrients.grow.base);
         buildNutrientOptions(
            $fields.base_nutrients.bloom, $nutrients.bloom.base
         );
         buildUnitsOptions($fields.nutrient_units, $settings.nutrients);
         buildUnitsOptions($fields.reservoir.units, $settings.reservoir);
         buildTimeOptions($fields.time.grow, $settings.time.grow);
         buildTimeOptions($fields.time.bloom, $settings.time.bloom);

         // Add list of recipes.
         $recipe_list.empty(); // Clear loading message.
         for (var r in $recipes) {
            var a = $('<a href="#UseRecipe"></a>').
               attr('title', r).
               html(r);
            $('<li>').append(a).appendTo($recipe_list);
         }

         // Expand the minimum height of the results area to fit the sidebar.
         $results.css('minHeight', $controls.outerHeight());

         // Initially, we want to hide all but the first fields to help guide
         // the user through the fields.
         $.each(
            [$fields.base_nutrients.bloom, $fields.reservoir.size,
              $fields.reservoir.units, $fields.time.grow, $fields.time.bloom,
              $supplements],
            function ()
            {
               $(this).hide().prev('label').hide();
            }
         );

         // Setup when to build results.
         var buildAndShow = function ()
         {
            buildResults();
            showNextField(this);
         };
         $controls.find('input, select').change(buildAndShow);
         var keyup_timeout;
         $controls.find('input[type=text]').keyup(function onKeyUp()
         {
            clearTimeout(keyup_timeout);
            var _this = this;
            keyup_timeout = setTimeout(function keyUpTimeout()
            {
               buildAndShow.call(_this);
            }, 350);
         });
         $controls.find('input[type=submit]').click(function onSubmit(event)
         {
            event.preventDefault();
            buildAndShow();
         });

         // Setup loading recipes.
         $recipe_list.find('a').click(function onRecipeClick(event)
         {
            event.preventDefault();
            var recipe = $recipes[$(this).attr('title')];
            if (typeof recipe === 'undefined') {
               console.log('Recipe not found.');
               return false;
            }

            setFieldValues(recipe, 1);
            showNextField('all');
         });

         // Block AbleCommerce from trying to run a search when "Enter" is 
         // typed no any fields.
         $controls.find('input, select').keydown(function onKeyDown(event)
         {
            // If <kbd>enter</kbd> was hit.
            if (event.which === 13) {
               event.preventDefault();
               event.stopImmediatePropagation();
            }
         });

         /** Reset Button **/
         $controls.find('button.reset').click(function onClickReset()
         {
            $fields.base_nutrients.grow.val('');
            $fields.base_nutrients.bloom.val('');
            $fields.reservoir.size.val('');
            $fields.time.grow.val('');
            $fields.time.bloom.val('');
            $fields.supplements.attr('checked', false);

            CookieMonster.erase($settings.cookies.choices);
            CookieMonster.erase($settings.cookies.overrides);
            $overrides = { grow: {}, bloom: {} };

            buildResults();
         });


         /** Grab saved overrides **/
         var overrides = Calculator.choices.getSaved('overrides');
         console.log(overrides);
         if (overrides && typeof overrides === 'object') {
            $.extend($overrides, overrides);
         }

         /** Grab any saved settings. */
         var values = Calculator.choices.getSaved();
         if (values) {
            setFieldValues(values);
         }
      }); // each calculator

      $('.print-calculator').click(function onClickPrint()
      {
         printCalculator();
         return false;
      });
   }); // document ready

   /**
   * Sets the values of the fields based on the passed settings.
   *
   * @param object values An object with a structure mimicking the fields
   *                      object but with values instead of fields elements. 
   * @param Number backup_reservoir_size Used if the reservoir size is not
   *                      set in the passed values or previously.
   */
   function setFieldValues(values, backup_reservoir_size)
   {
      $fields.base_nutrients.grow.val(values.base_nutrients.grow || '');
      if (values.base_nutrients.grow) {
         showNextField($fields.base_nutrients.grow.get(0));
      }
      $fields.base_nutrients.bloom.val(values.base_nutrients.bloom || '');
      if (values.base_nutrients.bloom) {
         showNextField($fields.base_nutrients.bloom.get(0));
      }
      $fields.nutrient_units.val(
         values.nutrient_units || $settings.nutrients.default_units
      );
      if ('reservoir' in values) {
         if ('size' in values.reservoir && values.reservoir.size !== '') {
            $fields.reservoir.size.val(values.reservoir.size);
            showNextField($fields.reservoir.size.get(0));
         }
         $fields.reservoir.units.val(
            values.reservoir.units || $settings.reservoir.default_units
         );
      }
      if ('time' in values) {
         if ('grow' in values.time) {
            $fields.time.grow.val(values.time.grow);
            if (values.time.grow) {
               showNextField($fields.time.grow.get(0));
            }
         }
         if ('bloom' in values.time) {
            $fields.time.bloom.val(values.time.bloom);
            if (values.time.bloom) {
               showNextField($fields.time.bloom.get(0));
            }
         }
      }
      $fields.supplements.attr('checked', false);
      $fields.supplements.val(values.supplements);

      // Make sure a reservoir is set.
      if (backup_reservoir_size && !$fields.reservoir.size.val()) {
         $fields.reservoir.size.val(backup_reservoir_size);
      }

      buildResults();
   } // setFieldValues()

   /**
   * Builds the results based on the selections made by the user.
   */
   function buildResults()
   {
      $ppm_chart_values = { grow: {}, bloom: {} };
      $warnings =
         { grow: { volume: {}, ppm: {} },
            bloom: { volume: {}, ppm: {} }
         };
      var choices = getFieldChoices($fields);
      // Save choices to cookie.
      Calculator.choices.save(choices);
      Calculator.choices.save('overrides', $overrides);
      // Get the values the user's choices correspond to.
      choices = {
         base_nutrients: {
            grow:
               [choices.base_nutrients.grow,
                $nutrients.grow.base[choices.base_nutrients.grow]] || null,
            bloom:
               [choices.base_nutrients.bloom,
                $nutrients.bloom.base[choices.base_nutrients.bloom]] || null
         },
         nutrient_units:
            $settings.nutrients.units[choices.nutrient_units] || null,
         reservoir: {
            size: Number(choices.reservoir.size),
            units: $settings.reservoir.units[choices.reservoir.units] || null
         },
         time: {
            grow: (choices.time.grow >= $settings.time.grow.min &&
                   choices.time.grow <= $settings.time.grow.max)
                     ? Number(choices.time.grow) : null,
            bloom: (choices.time.bloom >= $settings.time.bloom.min &&
                    choices.time.bloom <= $settings.time.bloom.max)
                     ? Number(choices.time.bloom) : null
         },
         supplements: choices.supplements
      };

      // Build list of supplements.
      var list = { grow: [], bloom: [] };
      for (var s = 0; s < choices.supplements.length; s++) {
         var name = choices.supplements[s];
         if (name in $nutrients.grow.supplements) {
            list.grow.push([name, $nutrients.grow.supplements[name]]);
         }
         if (name in $nutrients.bloom.supplements) {
            list.bloom.push([name, $nutrients.bloom.supplements[name]]);
         }
      }
      choices.supplements = list;

      /** Build tables of results. */
      var vol_units =
         '<span class="units">(' +
            choices.nutrient_units[0] + ' / ';
      if (choices.reservoir.size != '1') {
         vol_units += formatNumber(choices.reservoir.size) + ' ';
      }
      vol_units += choices.reservoir.units[0] + ')</span>';

      // grow
      var grow_tables = buildResultTables(
         'grow',
         [choices.base_nutrients.grow].concat(choices.supplements.grow),
         choices.nutrient_units, choices.reservoir, choices.time.grow
      );
      function appendMsgs(where, type, warnings)
      {
         for (var e in warnings) {
            where.append(
               '<p class="' + type + ' ' + e + '">' + warnings[e] + '</p>'
            );
         }
      }
      $growbox.empty().
         append('<h3>Grow Phase ' + vol_units + '</h3>');
      appendMsgs($growbox, 'warning', $warnings.grow.volume);
      $growbox.
         append(grow_tables.volume).
         append(
            '<h3>Grow Phase <span class="units">(appx. ppm)</span></h3>'
         );
      appendMsgs($growbox, 'warning', $warnings.grow.ppm);
      $growbox.append(grow_tables.ppm);

      // bloom
      var bloom_tables = buildResultTables(
         'bloom',
         [choices.base_nutrients.bloom].concat(choices.supplements.bloom),
         choices.nutrient_units, choices.reservoir, choices.time.bloom
      );
      $bloombox.empty().
         append('<h3>Bloom Phase ' + vol_units + '</h3>');
      appendMsgs($bloombox, 'warning', $warnings.bloom.volume);
      $bloombox.
         append(bloom_tables.volume).
         append(
            '<h3>Bloom Phase <span class="units">(appx. ppm)</span></h3>'
         );
      appendMsgs($bloombox, 'warning', $warnings.bloom.ppm);
      $bloombox.
         append(bloom_tables.ppm);

      // Are the results worth displaying yet?
      if ('1' in $ppm_chart_values.grow || '1' in $ppm_chart_values.bloom) {
         $results.addClass('populated');
      } else {
         $results.removeClass('populated');
      }

      updatePPMChart();
   } // buildResults()

   /**
   * Build a map of choices based on the passed map of fields.
   * 
   * @param object fields
   */
   function getFieldChoices(fields)
   {
      var values = {};
      for (var name in fields) {
         if (fields[name] instanceof jQuery) {
            if (fields[name].length > 1) {
               var value = [];
               fields[name].filter(':checked').each(function forEachChkBox()
               {
                  value.push($(this).val());
               });
               values[name] = value;
            } else {
               values[name] = fields[name].val();
            }
         } else {
            values[name] = getFieldChoices(fields[name]);
         }
      }
      return values;
   } // getFieldChoices()

   /**
   * Builds a nutrient volume and PPM tables based on the passed parameters.
   * 
   * @param string phase The lowercase name of the phase the tables are being 
   *                     generated for.
   * @param array nutrients An ordered list of nutrients for each row.
   * @param array nutrient_units A unit settings list from the settings.
   * @param object reservoir The reservoir size and units.
   * @param number time Number of weeks.
   *
   * @return object with two properties, `volume` and `ppm` that represent
   the two result tables.
   */
   function buildResultTables(phase, nutrients, nutrient_units, reservoir,
      time
   )
   {
      // Clear out the first nutrient as it's likely to be empty.
      if (nutrients.length > 0 && nutrients[0][0] == '') {
         nutrients.shift();
      }

      var voltbl = $('<table class="volume">'),
          ppmtbl = $('<table class="ppm">');

      /** Header row units. **/
      var header_row = $('<tr class="headers">');
      header_row.append('<th class="nutrients"></th>');
      // Week headers.
      if (time !== null) {
         for (var n = 1; n <= time; n++) {
            header_row.append(
               '<th class="week">' +
                  $settings.time.units[0] + ' ' + n +
               '</th>'
            );
         }
         if (phase === 'bloom') {
            header_row.append('<th class="flush">Flush</th>');
         }
      } // if
      // Make PPM specifc changes to header row and append to PPM table.
      var ppm_header_row = header_row.clone();
      ppm_header_row.find('th.units').
         attr('rowspan', nutrients.length + 2).
         empty().html('<span>Approx PPM</span>');
      ppmtbl.append(ppm_header_row);
      // Add PPM total volume column header and append to volume table.
      if (nutrients.length > 0) {
         header_row.append('<th class="total">Total Needed</th>');
      }
      voltbl.append(header_row);

      /** Nutrient rows **/
      var row_parity = 'odd';
      var ppm_totals = {};
      // Initialize PPM totals for each week.
      for (var w = 1; w <= time; w++) { ppm_totals[w] = 0; }
      if (phase === 'bloom' && time > 0) {
         ppm_totals.flush = 0;
      }
      var converted_res_size = reservoir.size / reservoir.units[1];
      for (var n = 0; n < nutrients.length; n++) {
         var nutrient = nutrients[n];
         if (nutrient[0] === '' || typeof nutrient[1] === 'undefined') {
            continue;
         }
         var overrides = (nutrient[0] in $overrides[phase])
            ? $overrides[phase][nutrient[0]] : null;

         var volrow = $('<tr class="nutrient">').
            data('nutrient', nutrient[0]).
            addClass(row_parity);
         var name = $('<th class="name">').
            append(formatNutrientName(nutrient[0]));
         volrow.append(name);
         var ppmrow = volrow.clone(true);
         if ('units' in nutrient[1]) {
            name.append(
               ' <span class="units">in ' + nutrient[1].units + '</span>'
            );
         }

         // Initiate the volumes by week.
         var volumes = {};
         for (var w = 1; w <= time; w++) { volumes[w] = 0; }
         if (phase === 'bloom' && time > 0) { volumes.flush = 0; }
         // Figure out which week has what volume of this nutrient.
         for (var w in nutrient[1].weeks) {
            if (time <= 0) { break; }
            var volume = nutrient[1].weeks[w];
            if (w.indexOf(',') !== -1) {
               // We're dealing with a range of weeks.
               var limits = w.split(',', 2);
               var min = Number(limits[0]),
                   max = Number(limits[1]);
               if (min < 0 && max < 0) {
                  // We're dealing with a negative range. These count back from
                  // the end of the weeks.
                  min = time + min + 1;
                  max = time + max + 1;
               } else { // both should be positive.
                  if (max > time) {
                     max = time;
                  }
               }
               for (var x = min; x <= max; x++) {
                  volumes[x] = volume;
               }
            } else if (w === 'flush') {
               volumes[w] = volume;
            } else { // Just a single week.
               var x = Number(w);
               if (x < 0) {
                  volumes[time + x + 1] = volume;
               } else {
                  if (x > time) {
                     x = time;
                  }
                  volumes[x] = volume;
               }
            }
         } // for each week

         /** Add values to tables **/
         var cell_parity = 'odd',
             vol_total = 0,     // The total volume for a row.
             voloverride,         // The override of a volume cell.
             ppmoverride;         // The override of a PPM cell.
         for (var w in volumes) {
            if (w === 'null') { continue; }
            // Check for overrides
            if (overrides && w in overrides.volume) {
               voloverride = overrides.volume[w];
               volumes[w] = voloverride;
               ppmoverride = null;
            } else if (overrides && w in overrides.ppm &&
                       nutrient[1].ppm > 0
            ) {
               ppmoverride = overrides.ppm[w];
               volumes[w] = ppmoverride / nutrient[1].ppm;
               voloverride = null;
            } else {
               voloverride = null;
               ppmoverride = null;
            }

            // Build volume cell.
            // Convert to correct units.
            var volume = (volumes[w] * converted_res_size) / nutrient_units[1];
            if (nutrient[0] === "ZHO") {
               // The amount needed for the "ZHO" supplement is unaffected by
               // the reservoir size.
               volume = volumes[w];
            }
            vol_total += Number(volume);
            // Fix up the display to avoid really long numbers.
            var disp_vol = (nutrient[1].units === 'tsp') // display volume
               ? formatAsQuarters(volume)
               : formatNumber(volume);
            var volcell = $('<td class="week">').
               addClass(cell_parity).
               addClass('num' + w);
            var ppmcell = volcell.clone();
            var num = $('<span class="number">' + disp_vol + '</span>').
               click(editResultValue);
            if (voloverride !== null) {
               volcell.addClass('overridden').
               append(makeResetValueBtn());
            }
            volcell.append(num);
            if (volume == 0) {
               volcell.addClass('zero');
            }
            volrow.append(volcell);

            // Build PPM cell.
            var ppm = volumes[w] * nutrient[1].ppm;
            ppm_totals[w] += ppm;
            var num =
               $('<span class="number">' + formatNumber(ppm) + '</span>').
                  click(editResultValue);
            if (ppmoverride !== null) {
               ppmcell.
                  addClass('overridden').
                  append(makeResetValueBtn());
            }
            ppmcell.append(num);
            ppmrow.append(ppmcell);
            cell_parity = (cell_parity === 'odd') ? 'even' : 'odd';
         } // for each week
         // Add the total volume amount needed.
         var disp_vol = (nutrient[1].units === 'tsp')
            ? formatAsQuarters(vol_total)
            : formatNumber(vol_total);
         var totalcell = $('<td class="total">').
            addClass(cell_parity);
         totalcell.append('<span class="number">' + disp_vol + '</span>');
         if (vol_total == 0 && typeof volcell !== 'undefined') {
            volcell.addClass('zero');
         } else {
            var units = ('units' in nutrient[1])
               ? nutrient[1].units : nutrient_units[0];
            totalcell.append(
               '<span class="units ' + units + '">' + units + '</span>'
            );
         }
         volrow.append(totalcell);

         voltbl.append(volrow);
         ppmtbl.append(ppmrow);

         row_parity = (row_parity === 'odd') ? 'even' : 'odd';
      } // for each nutrient

      /** Add the PPM total row to PPM table. */
      if (nutrients.length > 0) {
         var ppmtotalrow = $('<tr class="total">').
            addClass(row_parity).
            append('<th>Total PPM</th>');
         var cell_parity = 'odd';
         for (var w in ppm_totals) {
            var cell = $('<td class="week">').
               addClass(cell_parity).
               addClass('num' + (w + 1));
            var total = formatNumber(ppm_totals[w]);
            if (ppm_totals[w] > $settings.ppm_warning_trigger) {
               total += '*';
               cell.addClass('warning').addClass('too_high');
               $warnings[phase].ppm.too_high = '*Warning: Exceeding ' +
                  $settings.ppm_warning_trigger + ' PPM can adversely affect ' +
                  'your plants. Please review the chart for PPM levels ' +
                  'above ' + $settings.ppm_warning_trigger + '.';
            }
            cell.
               html(total).
               appendTo(ppmtotalrow);
            cell_parity = (cell_parity === 'odd') ? 'even' : 'odd';
         }
         ppmtbl.append(ppmtotalrow);

         $.extend($ppm_chart_values[phase], ppm_totals);
      } // if nutrients count > 0

      return { volume: voltbl, ppm: ppmtbl };
   } // buildResultTables() 


   /**
   * Initiates and updates a line chart of the total PPM values for the grow
   * and bloom phases.
   */
   function updatePPMChart()
   {
      $ppm_chart.html(
         '<p>You must have Flash installed and enabled to see this chart. ' +
            '<a href="http://get.adobe.com/flashplayer/">Install Flash now.' +
               '</a>' +
         '</p>'
      );
      if ($('#PPMChart_img').length == 0) {
         $ppm_chart.after('<div id="PPMChart_img"></div>');
      }

      if (!'1' in $ppm_chart_values.grow && !'1' in $ppm_chart_values.bloom) {
         return null;
      }

      var min = 0, max = 0, labels = [],
          grow = [], bloom = [];
      for (var w = 0;
           w < $settings.time.bloom.max || w < $settings.time.grow.max; w++
      ) {
         if ((w + 1) in $ppm_chart_values.grow) {
            grow[w] = $ppm_chart_values.grow[w + 1];
            if (grow[w] > max) {
               max = grow[w];
            }
         }
         if ((w + 1) in $ppm_chart_values.bloom) {
            bloom[w] = $ppm_chart_values.bloom[w + 1];
            if (bloom[w] > max) {
               max = bloom[w];
            }
         }
         if (w in grow || w in bloom) {
            labels[w] = 'Week ' + (w + 1);
         }
      }
      if ('flush' in $ppm_chart_values.bloom) {
         bloom.push($ppm_chart_values.bloom.flush);
         labels.push('Flush');
      }

      var data = {
         'num_decimals': 0,
         'bg_colour': '#ffffff',

         'elements': [
            {
               'type': 'line',
               'colour': '#7fac41',
               'text': 'Grow Phase',
               'width': 2,
               'font-size': 10,
               'dot-size': 6,
               'values': grow
            },
             {
                'type': 'line',
                'colour': '#d37f81',
                'text': 'Bloom Phase',
                'width': 2,
                'font-size': 10,
                'dot-size': 6,
                'values': bloom
             }
         ],

         'y_axis': {
            'max': max,
            'min': min,
            'stroke': 1,
            'colour': '#e1e1e1',
            'grid-colour': '#e1e1e1'
         },

         'x_axis': {
            'stroke': 1,
            'tick_height': 0,
            'colour': '#ffffff',
            'grid-colour': '#ffffff',
            'labels': {
               steps: 1,
               rotate: 0,
               colour: '#000000',
               labels: labels
            }
         }
      };

      open_flash_chart_data = function ()
      {
         return JSON.stringify(data);
      }

      // (Embed and) get Flash chart
      swfobject.embedSWF(
         '/js/Botanicare/open-flash-chart.swf',
         'PPMChart', '720', '230', '9.0.0'
      );

      setTimeout('createChartImage()', 1000);
   } // updatePPMChart()

   /**
   * Handles editing values of volume and PPM in the result tables. This 
   * should be used as a click event handler for result number clicks.
   */
   function editResultValue()
   {
      var span,  // The <span> the original number was held in.
          input, // The <input> to transfer the number to.
          num;   // The number being moved from the <span> to the <input>.

      /** Switching to edit mode **/
      span = $(this);
      num = span.html().
         replace(',', ''); // Remove thousands separator.
      input = $('<input type="text" class="number">').
         val(num).
         data('oldvalue', num).
         width(span.width());
      span.replaceWith(input);
      input.focus();

      /** Switching from edit mode **/
      input.blur(finishEditResultValue);
      input.keydown(function onResultValueInputKeyDown(event)
      {
         // If "enter" was typed.
         if (event.which === 13 || event.which === 27) {
            event.preventDefault();
            event.stopImmediatePropagation();
            finishEditResultValue.apply(this, arguments);
         }
      });
   } // editResultValue()

   /**
   * Handles finishing editing a result value of volume or PPM. This is meant
   * to be used as an event handler.
   * 
   * @param {event} event The event object.
   */
   function finishEditResultValue(event)
   {
      var span,     // The <span> to transfer the number to.
          input,    // The <input> to transfer the number from.
          num,      // The number being moved from the <input> to the <span>.
          oldnum,   // The old value that is being replaced.
          phase,    // The phase this change happened in.
          type,     // The type of value, PPM or volume.
          week,     // The week number or name this change was made on.
          nutname,  // The name of the nutrient.
          nutrient; // The $nutrients nutrient object this change was made on.

      input = $(this);
      num = $.trim(input.val());
      oldnum = input.data('oldvalue');
      nutname = input.closest('tr').data('nutrient');
      phase = (input.closest('.phase').hasClass('grow')) ? 'grow' : 'bloom';
      nutrient = $nutrients[phase].base[nutname] ||
                 $nutrients[phase].supplements[nutname];
      type = (input.closest('table').hasClass('volume')) ? 'volume' : 'ppm';
      week = input.closest('td.week').attr('class').
           match(/[$ ]num(\d+|\w+)/)[1];

      if (event.which === 27 || num === oldnum ||
          (num === '' && (nutname in $overrides[phase] === false ||
              week in $overrides[phase][nutname][type] === false))
      ) {
         // <Esc> was typed or number hasn't changed or num is blank but no
         // overrides are specified for this cell so we don't need to worry
         // about recalculating the "normal" value.
         span = $('<span class="number">').
            append(('units' in nutrient) ? oldnum : formatNumber(oldnum));
         input.replaceWith(span);
         span.click(editResultValue);
         return;
      } // else, value has changed

      // Since the value has changed, update the overrides list and rebuild
      // result tables.
      if (num === '') {
         // Interpreting a blank value as wanting to go to the normal value.
         // Let's remove the override for this value (we know it exists from
         // the if statement above).
         delete $overrides[phase][nutname][type][week];
         buildResults();
         return;
      } // Otherwise we need to add the new override.

      // If volume, make sure it's in the proper units.
      if (type === 'volume') {
         var ressize = $fields.reservoir.size.val(),
         // Reservoir size
             resunits = $fields.reservoir.units.val(),
         // Reservoir units
             nutunits = $fields.nutrient_units.val();
         // Nutrient units


         if (resunits !== $settings.reservoir.default_units) {
            ressize = ressize / $settings.reservoir.units[resunits][1];
         }
         if ('units' in nutrient) {
            // Nutrient is using custom units, we need the value to a usable
            // form.
            if (nutrient.units === 'tsp') {
               num = convertFromQuarters(num);
            }
         } else if (nutunits !== $settings.nutrients.default_units) {
            num = num * $settings.nutrients.units[nutunits][1];
         }
         // The amount needed for the "ZHO" supplement is unaffected by the
         // reservoir size.
         if (nutname !== "ZHO") {
            num = num / ressize; // amount of volume per 1 unit of reservoir
         }
      } // if type is volume

      if (nutname in $overrides[phase] === false) {
         $overrides[phase][nutname] = { volume: {}, ppm: {} };
      }

      // Clear any overrides for the opposite type of the same cell.
      if (type === 'volume' && week in $overrides[phase][nutname]['ppm']) {
         delete $overrides[phase][nutname]['ppm'][week];
      } else if (week in $overrides[phase][nutname]['volume']) {
         delete $overrides[phase][nutname]['volume'][week];
      }

      $overrides[phase][nutname][type][week] = num;

      buildResults();
   } // finishEditResultValue()

   /**
   * Creates a "reset overridden result value" button along with the 
   * appropriate events and returns the jQuery object.
   */
   function makeResetValueBtn()
   {
      var btn; // The star of the show.

      btn = $('<a class="button reset">X</a>');

      /** On Hover */
      btn.hover(
         function onMouseOver()
         {
            $(this).html('Reset');
         },
         function onMouseOut()
         {
            $(this).html('X');
         }
      );

      /** On click */
      btn.click(function onClick()
      {
         // Clear the value override.
         var btn = $(this),
         // The button that was clicked.
             phase,    // The phase this change happened in.
             type,     // The type of value, PPM or volume.
             week,     // The week number or name this change was made on.
             nutname,  // The name of the nutrient.
             nutrient; // The $nutrients nutrient object this change was made 
         // on.

         nutname = btn.closest('tr').data('nutrient');
         phase = (btn.closest('.phase').hasClass('grow'))
                       ? 'grow' : 'bloom';
         nutrient = $nutrients[phase].base[nutname] ||
                       $nutrients[phase].supplements[nutname];
         type = (btn.closest('table').hasClass('volume'))
                       ? 'volume' : 'ppm';
         week = btn.closest('td.week').attr('class').
                       match(/[$ ]num(\d+|\w+)/)[1];

         delete $overrides[phase][nutname][type][week];
         buildResults();
         return;
      });

      return $('<span class="btn"></span>').append(btn);
   } // makeResetValueBtn()

   /**
   * Formats a number for display as a volume amount.
   * 
   * @param Number num
   */
   function formatNumber(num)
   {
      num = Number(num);
      if (isNaN(num)) {
         return '';
      }
      // Fix up the display to avoid really long numbers.
      if (num > 9.995) {
         num = Math.ceil(num);
         if (num > 9999) { // Don't display thousands separator for numbers
            // under 10,000.
            num = num.formatNumber(0);
         }
      } else if (num % 1 !== 0) {
         // There are numbers after the decimal place.
         num = num.formatNumber(2);
      }

      return num;
   } // formatNumber()


   /**
   * Formats a number in terms of quarters (1/4).
   * 
   * @param Number num
   */
   function formatAsQuarters(num)
   {
      var quarters = Math.ceil(num / .25);
      var wholes = Math.floor(quarters / 4);
      quarters = quarters % 4;

      if (wholes === 0 && quarters === 0) {
         return 0;
      }
      if (quarters === 2) {
         if (wholes === 0) {
            return '1/2';
         } else {
            return wholes + ' 1/2';
         }
      }
      if (quarters === 0) {
         return wholes;
      }
      if (wholes === 0) {
         return quarters + '/4';
      }
      return wholes + ' ' + quarters + '/4';
   } // formatAsQuarters()

   /**
   * Converts a number formatted in quarters to decimals. 
   *
   * @param string quarters
   */
   function convertFromQuarters(quarters)
   {
      // Make sure this looks as expected.
      if (quarters.search(/^(\d+ \d+\/[2,4]|\d+|\d+\/[2,4]|\d?\.\d+)$/) === -1) {
         return '';
      }
      if (quarters.indexOf('.') === -1) {
         // The number is in fractional format.
         var parts = quarters.split(' ', 2),
         // Split into a whole number and fourths
          dec,     // The decimal number to return.
          fourths; // Fourths of 1 (x/4)
         if (parts.length === 1) {
            if (parts[0].indexOf('/') === -1) {
               // This is only whole numbers, no fractions.
               return Number(parts[0]);
            }
            dec = 0;
            fourths = parts[0];
         } else { // length must be 2
            dec = parts[0], // Whole numbers
            fourths = parts[1]; // Fourths
         }

         parts = fourths.split('/', 2);
         if (parts.length > 1 && parts[1] === '2') {
            // The fractional part is 1/2, so convert to quarters.
            parts[0] = '2';
            parts[1] = '4';
         }

         return Number(dec) + Number(parts[0]) * .25;
      } else {
         // The number is already in decimal format.
         return Number(quarters);
      }
   } // convertFromQuarters

   /**
   * Adds <span>s to the passed nutrient name based on character length and
   * spaces to help with formatting.
   * 
   * @param string name
   */
   var targetLength = 8;
   function formatNutrientName(name)
   {
      if (name.length <= targetLength) {
         return '<span>' + name + '</span>';
      }

      var words = name.split(/\s/g),
          chars = 0,
          name = '<span>',
          split = false;
      for (var w = 0; w < words.length; w++) {
         if (!split && chars >= targetLength) {
            name += '</span> <span>' + words[w];
            split = true;
         } else {
            name += ' ' + words[w];
         }
         chars += words[w].length;
      }
      name += '</span>';

      return name;
   } // formatNutrientName()

   /**
   * Builds and adds a list of <option>s to the passed <select>.
   * 
   * @param jQuery select
   * @param object list The object's property names are used for both the
   *                    values and the display of the <option>s.
   */
   function buildNutrientOptions(select, list)
   {
      for (var item in list) {
         $('<option>')
            .attr('value', item)
            .html(item)
            .appendTo(select);
      }
   } // buildNutrientOptions()

   /**
   * Builds and adds a list of units as <option>s to the passed select
   * 
   * @param jQuery select
   * @param object list
   */
   function buildUnitsOptions(select, list)
   {
      select.empty(); // Clear out placeholder <option>.
      for (var val in list.units) {
         $('<option>')
            .attr('value', val)
            .html(list.units[val][0])
            .appendTo(select);
      }
      if ('default_units' in list) {
         select.val(list.default_units);
      }
   } // buildUnitsOptions()

   /**
   * Builds and adds a list of times as <option>s to the passed select.
   * 
   * @param jQuery select
   * @param object list
   */
   function buildTimeOptions(select, list)
   {
      for (var time = list.min; time <= list.max; time++) {
         $('<option>')
            .attr('value', time)
            .html(time + ' ' + $settings.time.units[1])
            .appendTo(select);
      }
   } // buildTimeOptions()

   /**
   * Reveals the field after the passed field. This is for only revealing a
   * single field at a time to help guide the user in the order they should
   * complete the fields.
   * 
   * @param DOMElement field The current field.
   */
   function showNextField(field)
   {
      // Show the next field if hidden.
      switch (field) {
         case 'all':
         case $fields.time.bloom.get(0):
            $supplements.show().prev('label').show();
         case $fields.time.grow.get(0):
            $fields.time.bloom.show().prev('label').show();
         case $fields.reservoir.size.get(0):
            $fields.time.grow.show().prev('label').show();
         case $fields.base_nutrients.bloom.get(0):
            $fields.reservoir.size.add($fields.reservoir.units).
               show().prev('label').show();
         case $fields.base_nutrients.grow.get(0):
            $fields.base_nutrients.bloom.show().prev('label').show();
            break;
      }
   } // showNextField()

   /**
   * Prints the calculator.  Converts the chart to an image first, since some
   * browsers have problems printing Flash objects.
   */
   function printCalculator()
   {
      window.print();
   } // printCalculator

})(jQuery);

/**
 * A hook function for Open Flash Chart to use for loading in data. This is
 * meant to be overriden.
 */
function open_flash_chart_data() {
   return JSON.stringify({});
}

var OFC = {
   version: function (src)
   {
      var v = null;
      if (document.getElementById(src)) {
         v = document.getElementById(src).get_version();
      }
      return v;
   },
   rasterize: function (src, dst)
   {
      var dstElem = document.getElementById(dst);
      e = document.createElement('div');
      e.innerHTML = this.image(src);
      if (dstElem) {
         dstElem.parentNode.replaceChild(e, dstElem);
      }
   },
   image: function (src)
   {
      var img = null;

      if (document.getElementById(src) && document.getElementById(src).get_img_binary) {
         img = '<img id="' + src + '_img" src="data:image/png;base64,' + document.getElementById(src).get_img_binary() + '" />';
      }

      return img;
   }
};

function createChartImage()
{
   OFC.rasterize('PPMChart', 'PPMChart_img');
}
