/** @file    CatalogOptions.js
 *  @brief   Implementation of dynamic price and image changes from product
 *           options changes.
 *  @date    Created:   10 July 2006
 *  @remark  CVS-ID:    $Id: CatalogOptions.js,v 1.11 2007/10/30 20:42:53 mike Exp $
 *  @remark  CVS-TAG:   $Name:  $
 *  @remark  Copyright: (c) 2006 Eznettools. All rights reserved.
 *
 *  Include  "<SCRIPT src='/javascript/CatalogOptions/CatalogOptions.js'>
 *  </SCRIPT>" in <head> tag.
 *
 *  Include Behaviour.js so we can use css selectors to apply javascript
 *  behaviours to enable unobtrusive javascript in html documents.
 */





var CatalogOptions = {
   start : function() {
      //Determines the server so it can write out a full url to Behaviour.js
      var scriptsArray = document.getElementsByTagName("script");
      var server = "http://abc.eznettools.net";

      for(i=0; i < scriptsArray.length; i++) {
         var uri = scriptsArray[i].src;
         var reg = /CatalogOptions.js$/;
         var result = uri.match( reg );

         if(result) {
            var parts = uri.split(/\/javascript/);
            server = parts[0];
         }
      }

      if ( typeof window.onload != 'function' ) {
         window.onload = function() {
            Behaviour.register( CatalogOptions.myrules );
         }
      }
      else {
         var oldonload = window.onload;
         window.onload = function() {
            oldonload();
            Behaviour.register( CatalogOptions.myrules );
         }
      }


      document.write("<script src='" + server + "/javascript/Behaviour/" +
                        "Behaviour.js' type='text/javascript'></script>");

   },

/** Uses css selectors to apply javascript behaviours to enable
 *  unobtrusive javascript in html documents.
 */
   myrules : {
      '.bCatalogOptionsPrice' : function( element ) {
         Price.Attach( element );
      },
      '.bCatalogOptionsSavings' : function( element ) {
         Savings.Attach( element );
      },
      '.bCatalogOptionsChoiceBox' : function( element ) {
         ChoiceBox.Attach( element );
      },
      '.bCatalogOptionsImage' : function( element ) {
         ProductImage.Attach( element );
      },
      '.bCatalogOptionsThumbImage' : function( element ) {
         ThumbImage.Attach( element );
      },
      '__postregister__' : function() {
         Observer_UpdateAll(); //Processed after everything else.
      }
   }
}

CatalogOptions.start();




// =============================================================================
// Product Image
// =============================================================================

/** This class contains the product image objects, and allows them to
 *  be dynamically changed when the product options change or the thumbnail
 *  images are mousedover.
 *
 *  @ctor
 *  Constructor.  The default constructor for the product image class.  It calls
 *  "ProductImage_init" to initialize the elements for the product image
 *  objects.
 */
function ProductImage( element ) {
   if ( arguments.length > 0 ) {
      this.ProductImage_init( element );
   }
   else {
      //Being used as default constructor.
   }
}


/** Initializes the data members for the product image objects.  The original
 *  uri refers to the default image that is displayed when no product option
 *  (via a thumbnail object) is specifying another image.  The current uri is
 *  that of the image being shown at the moment.
 */
function ProductImage_init( element ) {
   this.element      = element;
   this.id           = _get_class_id( element );
   this.name         = _get_class_name( element );
   this.original_uri = this.element.src;
   this.current_uri  = this.element.src;

   // get type as specified in the original src
   this.thumbnail_type = this.element.src.match(/type=.{1,}[\&\;]?/);
   if(this.thumbnail_type) {
      this.thumbnail_type = this.thumbnail_type.toString();
      this.thumbnail_type = this.thumbnail_type.replace("type=","");
      this.thumbnail_type = this.thumbnail_type.replace(/[;&]/,"");
   } else {
      this.thumbnail_type = "local";
   }
   
   // get fill as specified in the original src
   this.thumbnail_fill = this.element.src.match(/fill=.{1,}[\&\;]?/);
   if(this.thumbnail_fill) {
      this.thumbnail_fill = this.thumbnail_fill.toString();
      this.thumbnail_fill = this.thumbnail_fill.replace("fill=","");
      this.thumbnail_fill = this.thumbnail_fill.replace(/[;&]/,"");
   } else {
      this.thumbnail_fill = false;
   }

   // get pos as specified in the original src
   this.thumbnail_pos = this.element.src.match(/pos=.{1,}[\&\;]?/);
   if(this.thumbnail_pos) {
      this.thumbnail_pos = this.thumbnail_pos.toString();
      this.thumbnail_pos = this.thumbnail_pos.replace("pos=","");
      this.thumbnail_pos = this.thumbnail_pos.replace(/[;&]/,"");
   } else {
      this.thumbnail_pos = "center";
   }

   
   // get the width & height as specified in the original src
   var original_thumbgen_width = this.element.src.match(/width=[0-9]{1,}/);
   if(original_thumbgen_width) {
      original_thumbgen_width = original_thumbgen_width.toString();
      original_thumbgen_width = original_thumbgen_width.replace("width=","");
   }
   var original_thumbgen_height = this.element.src.match(/height=[0-9]{1,}/);
   if(original_thumbgen_height) {
      original_thumbgen_height = original_thumbgen_height.toString();
      original_thumbgen_height = original_thumbgen_height.replace("height=","");
   }

   // get the width & height as specified in the <img> tag
   var img_tag_width;
   var img_tag_height;
   if(this.element.attributes.width && this.element.attributes.width.specified){
      img_tag_width = this.element.attributes.width.value;
   }
   if(this.element.attributes.height && this.element.attributes.height.specified) {
      img_tag_height = this.element.attributes.height.value;
   }

   this.thumbnail_width =  original_thumbgen_width ||
                           img_tag_width ||
                           original_thumbgen_height ||
                           img_tag_height ||
                           200;
                          
   this.thumbnail_height = original_thumbgen_height ||
                           img_tag_height ||
                           original_thumbgen_width ||
                           img_tag_width ||
                           200;
}

ProductImage.prototype.ProductImage_init = ProductImage_init;


/** Called via the observer object when a publisher fires an event in which the
 *  product image objects are interested.  Right now it is interested in
 *  mouseovers or onchanges from the thumbnail objects and changes its current
 *  uri to match (except for the cgi parameters) the uri of the thumbnail object
 *  that called it.
 */
function ProductImage_update( object, l_event ) {

   this.element.style.visibility = "hidden";

   var temp = "";
   
   if(l_event == "ChoiceBoxOnChange") { // object is a choicebox
      if(object.replacement_image != "undefined") {
         if(!object.replacement_image.match(/^http/)) { //image on our server
            temp += "/cgi-bin/ThumbGen.cgi?location=";
            temp += object.replacement_image;
            temp += ";width=" + this.thumbnail_width;
            temp += ";height=" + this.thumbnail_height;
            temp += ";type=" + this.thumbnail_type;
            if(this.fill) {
               temp += ";fill=" + this.thumbnail_fill;
            }
            temp += ";pos=" + this.thumbnail_pos;
            this.element.style.width="auto";
         } else {                              // for images on other servers
            temp = object.replacement_image;
            this.element.style.width = this.thumbnail_width;
            this.current_uri = temp;
            this.element.src = temp;
         }
      } else {
         temp = this.original_uri;
      }         
   } else { // object is a thumbnail
      if(object.element.src.match(/ThumbGen.cgi/)) { // image is on our server
         temp = object.element.src;
         temp = temp.split(/[;&]+/);
         temp = temp[0];
         temp += ";width=" + this.thumbnail_width;
         temp += ";height=" + this.thumbnail_height;
         temp += ";type=" + this.thumbnail_type;
         if(this.fill) {
            temp += ";fill=" + this.thumbnail_fill;
         }
         temp += ";pos=" + this.thumbnail_pos;
         this.element.style.width="auto";
      } else {                                // for images on other servers
         temp = object.element.src;
         this.element.style.width = this.thumbnail_width;
      }
   }
   
   this.current_uri = temp;
   this.element.src = temp;
   this.element.style.visibility = "visible";
}

ProductImage.prototype.update = ProductImage_update;

/** This function attaches the product image objects to the observer's register
 *  as listeners.
 */
function ProductImage_Attach( element ) {
   var object = new ProductImage( element );
   var l_event = new Array( "ThumbOnMouseover", "ChoiceBoxOnChange" );
   //Above are events that product image objects are interested in.

   for( var i = 0; i < l_event.length; i++ ) {
      Observer.ListenerRegister( object, l_event[i] );
      /** Puts each product image object into the listener array under a
       *  different section for each event that it is interested in.
       */
   }
}

ProductImage.Attach = ProductImage_Attach;




// =============================================================================
// Thumbnail Image
// =============================================================================

/** This class contains the thumbnail image objects, and allows them to
 *  be dynamically changed when the product options change.
 *
 *  @ctor
 *  Constructor.  The default constructor for the thumbnail image class.  It
 *  calls "ThumbImage_init" to initialize the elements for the thumbnail image
 *  objects.
 */
function ThumbImage( element ) {
   if ( arguments.length > 0 ) {
      this.ThumbImage_init( element );
   }
   else {
      //Being used as default constructor.
   }
}


/** Initializes the data members for the thumbnail image objects.  The original
 *  uri refers to the default image that is displayed when no product option
 *  is specifying another image.  The current uri is that of the image being
 *  shown at the moment.
 */
function ThumbImage_init( element ) {
   this.element         = element;
   this.id              = _get_class_id( element );
   this.name            = _get_class_name( element );
   this.replacement_att = _get_att_name( element );
   this.original_uri    = this.element.src;
   this.sourcePath      = 0;

   // get type as specified in the original src
   this.thumbnail_type = this.element.src.match(/type=.{1,}[\&\;]?/);
   if(this.thumbnail_type) {
      this.thumbnail_type = this.thumbnail_type.toString();
      this.thumbnail_type = this.thumbnail_type.replace("type=","");
      this.thumbnail_type = this.thumbnail_type.replace(/[;&]/,"");
   } else {
      this.thumbnail_type = "local";
   }
   
   // get fill as specified in the original src
   this.thumbnail_fill = this.element.src.match(/fill=.{1,}[\&\;]?/);
   if(this.thumbnail_fill) {
      this.thumbnail_fill = this.thumbnail_fill.toString();
      this.thumbnail_fill = this.thumbnail_fill.replace("fill=","");
      this.thumbnail_fill = this.thumbnail_fill.replace(/[;&]/,"");
   } else {
      this.thumbnail_fill = false;
   }

   // get pos as specified in the original src
   this.thumbnail_pos = this.element.src.match(/pos=.{1,}[\&\;]?/);
   if(this.thumbnail_pos) {
      this.thumbnail_pos = this.thumbnail_pos.toString();
      this.thumbnail_pos = this.thumbnail_pos.replace("pos=","");
      this.thumbnail_pos = this.thumbnail_pos.replace(/[;&]/,"");
   } else {
      this.thumbnail_pos = "center";
   }
   
   // get the width & height as specified in the original src
   var original_thumbgen_width = this.element.src.match(/width=[0-9]{1,}/);
   if(original_thumbgen_width) {
      original_thumbgen_width = original_thumbgen_width.toString();
      original_thumbgen_width = original_thumbgen_width.replace("width=","");
   }
   var original_thumbgen_height = this.element.src.match(/height=[0-9]{1,}/);
   if(original_thumbgen_height) {
      original_thumbgen_height = original_thumbgen_height.toString();
      original_thumbgen_height = original_thumbgen_height.replace("height=","");
   }

   // get the width & height as specified in the <img> tag
   var img_tag_width;
   var img_tag_height;
   if(this.element.attributes.width && this.element.attributes.width.specified){
      img_tag_width = this.element.attributes.width.value;
   }
   if(this.element.attributes.height && this.element.attributes.height.specified) {
      img_tag_height = this.element.attributes.height.value;
   }

   this.thumbnail_width =  original_thumbgen_width ||
                           img_tag_width ||
                           original_thumbgen_height ||
                           img_tag_height ||
                           50;

   this.thumbnail_height = original_thumbgen_height ||
                           img_tag_height ||
                           original_thumbgen_width ||
                           img_tag_width ||
                           50;
}

ThumbImage.prototype.ThumbImage_init = ThumbImage_init;

/** Used to handle any events and their following updates that affect the
 *  thumbnail image objects.  Right now it only handles changes in the product
 *  options choice boxes.
 */
function ThumbImage_update( object, l_event ) {
   if ( decodeURIComponent( this.replacement_att ) == object.name) {
      var uri;

      if ( object.replacement_image != "undefined" ) {
         if(object.replacement_image.match(/^http/)) {
            uri = object.replacement_image;
         } else {            
            this.sourcePath = object.replacement_image;
            uri = "/cgi-bin/ThumbGen.cgi?location=" + this.sourcePath;
            uri += ";width=" + this.thumbnail_width;
            uri += ";height=" + this.thumbnail_height;
            uri += ";type=" + this.thumbnail_type;
            if(this.fill) {
               uri += ";fill=" + this.thumbnail_fill;
            }
            uri += ";pos=" + this.thumbnail_pos;
         }
      } else {
         uri = this.original_uri;
      }

      this.element.src = uri;

      ThumbImage.onchange( this );
   }
}

ThumbImage.prototype.update = ThumbImage_update;

/** OnChange event function for the thumbnail image objects.
 */
function ThumbImage_onchange( object ) {
   var p_event = "ThumbOnChange";
   Observer.Update( object, p_event );
}

ThumbImage.onchange = ThumbImage_onchange;

/** OnMouserover event function for the thumbnail image objects.
 */
function ThumbImage_onmouseover( object ) {
   var p_event = "ThumbOnMouseover";
   Observer.Update( object, p_event );
}

ThumbImage.onmouseover = ThumbImage_onmouseover;


/** Used to initialize any events when the page loads and after all of the
 *  objects are created and setup.  Right now this function isn't doing
 *  anything, but because this object is a publisher this function is in here
 *  mostly as a formality and for possible use in the future.
 */
function ThumbImage_initialize_events( object ) {

}

ThumbImage.prototype.initialize_events = ThumbImage_initialize_events


/** Attaches the thumbnail objects to the listener and publisher arrays in the
 *  observer object.  Also, sets up the event functions for the objects.
 */
function ThumbImage_Attach( element ) {
   var object = new ThumbImage( element );
   var l_event = new Array( "ChoiceBoxOnChange" );

   Observer.PublisherRegister( object );

   for( var i = 0; i < l_event.length; i++ ) {
      Observer.ListenerRegister( object, l_event[i] );
   }

   element.onmouseover = function() {
      ThumbImage.onmouseover( object );
   }

   element.onchange = function() {
      ThumbImage.onchange( object );
   }
}

ThumbImage.Attach = ThumbImage_Attach;




// =============================================================================
// Price
// =============================================================================

/** This class contains the our price and list price objects, and allows them to
 *  be dynamically updated with product options' price adjustments.
 *
 *  @ctor
 *  Constructor.  The default constructor for Price class.  It calls
 * "Price_init" to initialize the elements for the Price objects.
 */
function Price( element ) {
   if ( arguments.length > 0 ) {
      this.Price_init( element );
   }
   else {
      //Being used as default constructor.
   }
}


/** Initializes the the elements for the  price objects.  The class name is not
 *  currently used.
 */
function Price_init( element ) {
   this.element        = element;
   this.id             = _get_class_id( element );
   this.name           = _get_class_name( element );
   this.original_price = this.get_original_price( element );
   this.adjustments    = new Array();
}

Price.prototype.Price_init = Price_init;


/** Looks at the html and returns the original price for the current price object.
 */
function Price_get_original_price( element ) {
   var price = {
      'amount' : 0,
      'currency' : "$"
   };
   var original_price = element.innerHTML;
   var reg = /([^\s0-9])?(\d+\.?\d+)/;
   var value = original_price.match( reg );

   if ( value != null ) {
      price.currency = value[1];
      price.amount = value[2];
   }

   return price;
}

Price.prototype.get_original_price = Price_get_original_price;


/** This function takes in the adjustments from the object that is passed in and
 *  dynamically updates the price displays for the price objects.  The
 *  "totalAdjustments" variable keeps a running total of the amount that will be
 *  added to the price.  This function calculates differently depending on
 *  wether the type of adjustment is a fixed monetary amount or a percent of the
 *  base price.
 */
function Price_update( object ) {
   this.adjustments[object.name] = object.value;

   var totalAdjustments = 0;

   for ( var k in this.adjustments ) {
      if ( this.adjustments[k].type == "fixed" ) {
         totalAdjustments = totalAdjustments + this.adjustments[k].amount;
         //Fixed amounts.
      }
      else {
         totalAdjustments = totalAdjustments + (
            this.original_price.amount * (this.adjustments[k].amount / 100)
         );
         //For percents.
      }
   }

   //This is a quick way to convert "original_price.amount from a string to a
   //number.                                                   |
   //                                                         \|/.
   var price = totalAdjustments + ( this.original_price.amount * 1 );

   if ( price < 0 ) {
      price = 0;
   }

   this.element.innerHTML = this.original_price.currency +
      _format_price( price );
}

Price.prototype.update = Price_update;


/** This function attaches the price to the observer's listener register.
 */
function Price_Attach( element ) {
   var object = new Price( element );
   var l_event = new Array( "ChoiceBoxOnChange" );

   for( var i = 0; i < l_event.length; i++ ) {
      Observer.ListenerRegister( object, l_event[i] );
   }
}

Price.Attach = Price_Attach;




// =============================================================================
// Savings
// =============================================================================

/** This class contains the savings object and allows it to dynamically update
 *  when there is a percentage product-option price increase on the list price
 *  and our price objects.
 *
 *  @ctor
 *  This is the default constructor for the Savings class.  It calls the
 *  "Savings_init" to initialize the elements for any savings objects.
 */
function Savings( element ) {
   if ( arguments.length > 0 ) {
      this.Savings_init( element );
   }
   else {
      //Being used as default constructor.
   }
}


/** Initializes any savings object.  The class name is not currently used.
 */
function Savings_init( element ) {
   this.element        = element;
   this.id             = _get_class_id( element );
   this.name           = _get_class_name( element );
   this.savings_amount = this.get_original_savings( element );
   this.adjustments    = new Array();
}

Savings.prototype.Savings_init = Savings_init;


/** This function extracts the savings amount from the original html and stores
 *  it as part of the object.  This is similar code to the
 *  "Price_get_original_price()" function in the previous class, but with doing
 *  the math right duplicating this code does save time and effort.
 */
function Savings_get_original_savings( element ) {
   var price = {
      'amount' : 0,
      'currency' : "$"
   };
   var original_savings = element.innerHTML;
   var reg = /([^\s0-9])?(\d+\.?\d+)/;
   var value = original_savings.match( reg );

   if ( value != null ) {
      price.currency = value[1];
      price.amount = value[2];
   }
   return price;
}

Savings.prototype.get_original_savings = Savings_get_original_savings;


/** This function takes the savings amount that is extracted from the
 *  html in the above function, and then does the "right math".  Instead of
 *  always calculating the list price and our price first then determining the
 *  savings amount, we calculate the savings amount between the original list
 *  and our prices, then we perform the same percentage calculations on the
 *  original savings amount and get the correct savings amount to display.
 *  The savings amount only changes when the list and our prices are increased
 *  or decreased by a percentage, fixed price changes don't have an effect.
 */
function Savings_update( object ) {
   this.adjustments[object.name] = object.value;

   var totalAdjustments = 0;

   for( var k in this.adjustments ) {
      if( this.adjustments[k].type == "percent" ) {
         totalAdjustments = totalAdjustments + (
            this.savings_amount.amount * ( this.adjustments[k].amount / 100 )
         );
      }
   }

   var savings = totalAdjustments + ( this.savings_amount.amount * 1 );

   if ( savings < 0 ) {
      savings = 0;
   }

   this.element.innerHTML = this.savings_amount.currency + _format_price( savings );
}

Savings.prototype.update = Savings_update;


/** This function attaches the savings to the observer's register.
 */
function Savings_Attach( element ) {
   var object = new Savings( element );
   var l_event = new Array( "ChoiceBoxOnChange" );

   for( var i = 0; i < l_event.length; i++ ) {
      Observer.ListenerRegister( object, l_event[i] );
   }
}

Savings.Attach = Savings_Attach;




// =============================================================================
// Choice Box
// =============================================================================

/** This class runs the product-options drop-down menus.  It creates an object
 *  for each drop-down menu and allows them to dynamically change the price,
 *  savings, thumbnail, and product image objects.
 *
 *  @ctor
 *  This is the default constructor for Choice Box objects.  It calls the
 *  "ChoiceBox_init" function to initialize the ChoiceBox elements.
 */
function ChoiceBox( element ) {
   if ( arguments.length > 0 ) {
      this.ChoiceBox_init( element );
   }
   else {
      //Being used as default constructor.
   }
}


/** Initializes the elements for the ChoiceBox objects.  Again, the class name
 *  isn't used right now.
 */
function ChoiceBox_init( element ) {
   this.element           = element;
   this.className         = _get_class_name( element );
   this.id                = _get_class_id( element );
   this.name              = element.name;
   this.value             = this.get_current_adjustment( element );
   this.replacement_image = this.get_replacement_image( element );
}

ChoiceBox.prototype.ChoiceBox_init = ChoiceBox_init;


/** Returns the adjustment "amount" and "type" inside of the value array.  The
 *  "type" refers to wether the adjustment is a percentage or fixed dollar
 *  amount and the "amount" is the actual numeric value (exemplorum gratia:
 *  "10.00%" would be 'amount = 10.00' and 'type = "percent"'; whereas "$10.00"
 *   would be 'amount = 10.00' and 'type = "fixed"').  "exemplorum gratia" is
 *   "e.g." except plural, meaning "some examples".
 */
function ChoiceBox_get_current_adjustment( element ) {
   var value = {
      'amount' : 0,
      'type' : "fixed"
   };

   var text = element.options[element.selectedIndex].text;
   var reg = /\(([+-][\S0-9]\d*\.\d+%?)\)$/;//Finds the section with the price.
   var regAmount = /([+-])[^\s0-9]?(\d+\.\d+)/;//Finds the actual price.
   var percent = /\%/;//Used to determine if it is a percent or fixed increase.
   var result = text.match( reg );

   if ( result != null ) {
      var section = result[1];

      var temp = section.match( percent );
      if ( temp != null ) {
         value.type = "percent";
      }
      else {
         value.type = "fixed";
      }

      temp = section.match( regAmount );
      if ( temp != null ) {
         if ( temp[1] == '-' ) {
            value.amount = temp[2] * -1;
            /** The negative one makes sure it is a negative amount and returned
             *  as a string.
             */
         }
         else {
            value.amount = temp[2] * 1;
            /** The one is to make sure it is returned as a number and not a
             *  string.
             */
         }
      }
   }

   return value;
}

ChoiceBox.prototype.get_current_adjustment = ChoiceBox_get_current_adjustment;


/** Looks at the style tag for the selected option to see if it has a uri for a
 *  replacement image, if so the uri is passed on to the appropriate thumbnail
 *  object which is then changed and then changes the product image object in
 *  turn.
 */
function ChoiceBox_get_replacement_image ( element ) {
   var result = "undefined";

   if (element.options[element.selectedIndex].style.backgroundImage) {
      var style = element.options[element.selectedIndex].style.backgroundImage;
      var temp = style.split(/[()]/);
      result = temp[1];

      if (result) {
         result = result.replace(/\"/g,""); // for opera
         result = result.replace(/http.+\/cgi-bin\/ez-catalog\//,""); // for opera
      }
   }
   return result;
}

ChoiceBox.prototype.get_replacement_image = ChoiceBox_get_replacement_image;


/** The choice box object OnChange function.  This function waits for changes on
 *  the drop-down menus to trigger and updates the value element and then sends
 *  the information to the Observer to update the the Listener objects.
 */
function ChoiceBox_onchange ( object ) {
   p_event = "ChoiceBoxOnChange";
   object.value = object.get_current_adjustment( object.element );
   object.replacement_image = object.get_replacement_image( object.element );
   Observer.Update( object, p_event );//Updates when changes occur.
}

ChoiceBox.onchange = ChoiceBox_onchange;


/** Used by the observer's UpdateAll function to make sure that all of the
 *  objects on the page are correlated when the page loads and after the objects
 *  have all been created and configured.
 */
function ChoiceBox_initialize_events( object ) {
   ChoiceBox_onchange( object );
}

ChoiceBox.prototype.initialize_events = ChoiceBox_initialize_events


/** Attaches the choice box objects to the observers publisher register.
 */
function ChoiceBox_Attach( element ) {
   var object = new ChoiceBox( element );

   Observer.PublisherRegister( object );

   element.onchange = function() {
      ChoiceBox.onchange( object );
   }

   element.onchange();
}

ChoiceBox.Attach = ChoiceBox_Attach;




// =============================================================================
// Observer
// =============================================================================

/** This class creates one object that manages the data transfer between the
 *  Publishers (choice box or boxes) and the Listeners (list price, our price,
 *  savings, thumbnail image, and product image objects.).
 *
 *  @ctor
 *  The default constructor for the Observer class. This object contains a
 *  register of all the listener objects and and another for the publisher
 *  objects.  Also, it calls "Observer_Init" to initialize the observer
 *  registers.  This is a Singleton object.
 */
function Observer() {
   this.Observer_init();
}


/** Initializes the Observer object.
 */
function Observer_init() {
   this.listener_registry  = {};
   this.publisher_registry = {};
}

Observer.prototype.Observer_init = Observer_init;


/** Creates the Observer object.
 */
function Observer_Instance() {
   if ( !Observer.INSTANCE ) {
      Observer.INSTANCE = new Observer();
   }

   return Observer.INSTANCE;
}

Observer.Instance = Observer_Instance;


/** This function passes information from the publisher objects to the listener
 * objects so that they can update accordingly.
 */
function Observer_Update( object, p_event ) {
   if (Observer.Instance().listener_registry[object.id] &&
       Observer.Instance().listener_registry[object.id][p_event]) {

      var leng;
      leng = Observer.Instance().listener_registry[object.id][p_event].length;

      for ( var i = 0; i < leng; i++ ) {
         Observer.Instance().listener_registry[object.id][p_event][i].update(
            object, p_event
         );
      }
   }
}

Observer.Update = Observer_Update;


/** This function takes in the objects, their's id's, and types of events that
 *  they're interested in, then sets them up in a multi-demensional associative
 *  array.
 */
function Observer_ListenerRegister( object, l_event ) {
   if ( !Observer.Instance().listener_registry[object.id] ) {
      Observer.Instance().listener_registry[object.id] = new Array();
   }

   if ( Observer.Instance().listener_registry[object.id][l_event] ) {
      Observer.Instance().listener_registry[object.id][l_event].push( object );
   }
   else {
      Observer.Instance().listener_registry[object.id][l_event] = [object];
   }
}

Observer.ListenerRegister = Observer_ListenerRegister;


/** This function sets up all of the publisher objects in to an array.
 */
function Observer_PublisherRegister( object ) {
   if ( Observer.Instance().publisher_registry[object.id] ) {
      Observer.Instance().publisher_registry[object.id].push( object );
   }
   else {
      Observer.Instance().publisher_registry[object.id] = [object];
   }
}

Observer.PublisherRegister = Observer_PublisherRegister;


/** This function calls all the initialize_events functions and updates
 *  everything when the page loads, but after all of the objects have been
 *  created and configured.
 */
function Observer_UpdateAll() {
   var leng;

   for ( var id in Observer.Instance().publisher_registry ){

      leng = Observer.Instance().publisher_registry[id].length;

      for ( var i = 0; i < leng; i++ ) {
         Observer.Instance().publisher_registry[id][i].initialize_events(
            Observer.Instance().publisher_registry[id][i]
         );
      }
   }
}

Observer.UpdateAll = Observer_UpdateAll;




/** Parses and returns the class id from the class name attribute.
 */
function _get_class_id( element ) {
   var id = element.className;
   var reg = /groupid[0-9]+/;
   var result = id.match( reg );
   return result;
}


/** Parses and returns the class name from the class name attribute.  This data
 *  is not really used later on.
 */
function _get_class_name( element ) {
  var name = element.className;
  var reg = /bCatalogOptions\w+/;
  var result = name.match( reg );
  return result;
}

/** Parses and returns the name of the product option, which has the replacement
 *  image information, from the class name attribute.
 */
function _get_att_name( element ) {
   var name = element.className;
   var reg = /att_\S+/;
   var result = name.match( reg );
   return result;
}


/** Formats the \p price ensuring it is rounded to two decimal places.
 *
 *  \tparam Number price
 *          The price to format.
 *
 *  \treturn String The formatted price.
 */
function _format_price( price ) {
   price = parseFloat( price );

   // Shift the decimal over two places so we can round to the 2nd decimal
   var shifted = price * 100;

   // Round the now shifted number. This will give us a whole number.
   var rounded = Math.round( shifted );

   // Convert the whole number back to a float now rounded to the 2nd
   // decimal
   price = rounded / 100;

   var formatted_price = price.toString();
   var decimal_pos = formatted_price.indexOf( "." );

   if ( decimal_pos == -1 ) {
      formatted_price += ".";
      decimal_pos = formatted_price.length - 1;
   }

   //                        |
   //                       \|/ calculates the current number of decimals
   var zeros_needed = 2 - ( formatted_price.length - decimal_pos - 1 );

   for ( var i = 0; i < zeros_needed; i++ )
      formatted_price += "0";

   return formatted_price;
}


/** Displays a popup window with the given title and the contents of body
 *  centered within a paragraph. The suid parameter is required if you want
 *  the page to be styled according to the account's global information
 *  settings.
 *
 *  \tparam string title the page title
 *  \tparam string body the page body
 *  \tparam string suid the member account id to get the global cfg from
 */
function popup( title, body, suid ) {
   h = screen.availHeight;
   w = screen.availWidth;

   var popW = 500;
   var popH = 230;

   var topPos = (h-popH)/2;
   var leftPos = (w-popW)/2;

   var url = '';
   var name = 'blah';

   mywindow = window.open('', '',
                          'dependent=no,status=yes,toolbar=no,' +
                          'location=no,resizable=yes,scrollbars=yes' +
                          ',width='   + popW +
                          ',height='  + popH +
                          ',screenX=' + leftPos +
                          ',screenY=' + topPos +
                          ',top='     + topPos +
                          ',left='    + leftPos);

   var doc =
      "<html><head>" +
      "<title>" + title + "</title>" +
      "<link rel='stylesheet' type='text/css' " +
        "href='/cgi-bin/ez-catalog/css_gen.cgi?SUID=" + suid + "' />" +
      "</head>" +
      "<body><center><h2>"  + title + "</h2></center>" +
      "<div align='left'>" +
      body +
      "</div>" +
      "<div align='center'><p>" +
      "<input type='button' value='Ok' name='close' " +
        "onclick='window.close()' />" +
      "</p></div>" +
      "</body></html>";

   mywindow.document.open();
   mywindow.document.write( doc );
   mywindow.document.close();
}
