function registerNamespace(ns)
{
    var parts = ns.split(".");
    var root = window;

    for(var i = 0; i < parts.length; i++)
    {
        if(!root[parts[i]])
        {
            root[parts[i]] = new Object();
        }

        root = root[parts[i]];
    }
}

registerNamespace('Associates.Util');

Associates.Util.getElementsByTagAndClass = function(tag, class_name, parent_element)
{
    if(!parent_element){ parent_element = document; }
    
    var tags = parent_element.getElementsByTagName(tag);
   
    var class_tags = new Array();

    var class_regex = new RegExp("\\b" + class_name + "\\b");
    for (var i = 0; i < tags.length; i++)
    {
        if(tags[i].className.match(class_regex))
        {
            class_tags.push(tags[i]);
        }
    }
    
    return class_tags;
}

Associates.Util.findAncestor = function(child, test)
{
    var test_func;

    if(typeof test == 'object')
    {
        test_func = function(obj){ return obj == test };
    }
    else if (typeof test == 'string')
    {
        test_func = function(obj){ return obj.nodeName == test };
    }
    else
    {
        test_func = test;
    }

    while(child && !test_func(child))
    {
        child = child.parentNode;
    }

    return child;
}

Associates.Util.getSelectedRadio = function(form, radioName)
{
    if (typeof form == 'string') { form = document.forms[form]; }

    for (var i = 0; i < form[radioName].length; i++)
    {
        if (form[radioName][i].checked)
        {
            return form[radioName][i].value;
        }
    }
    
    return undefined;
}

      
function AttachEventListener(object, event, handler, useCapture)
{
    if( !useCapture )
    {
        useCapture = false;
    }

    if( object.addEventListener )
    {
        object.addEventListener(event, handler, useCapture);
    }
    else
    {
        object.attachEvent('on' + event, handler);
    }
}


registerNamespace('Assoc.DOMEvent');
Assoc.DOMEvent.AddHandler = function(object, event, handler, context)
{
    var event_handler;
    if(context) 
        event_handler = function(event){ handler.call(context, event) };
    else
        event_handler = handler;

    AttachEventListener(object, event, event_handler);
}

function GetEventTarget(event)
{
    return (event.target)? event.target : event.srcElement;
}

//borrowed from quirksmode. Stops event bubbling.
function StopEventPropagation(event)
{
    if(!event) { event = window.event; }

    //MSIE
    event.cancelBubble = true;

    //w3c
    if(event.stopPropagation) { event.stopPropagation(); }
}

function StopEventDefault(event)
{
    if(!event) { event = window.event; }

    //MSIE
    event.returnValue = false;

    //w3c
    if(event.preventDefault) { event.preventDefault() };
}

//does both StopEventPropogation and StopEventDefault
function StopEvent(event)
{
    StopEventDefault(event);
    StopEventPropagation(event);
}

function mouseOverFromElement(event)
{
    return (event.relatedTarget)? event.relatedTarget : event.fromTarget;
}

function mouseOutToElement(event)
{
    return (event.relatedTarget)? event.relatedTarget : event.toTarget;
}

      /**
 * Copyright (c) 2007 Amazon.com, Inc.
 * All rights reserved.
 */

function AJSEventListener()
{
  this.events = new Object();
}
new AJSEventListener();

AJSEventListener.prototype.registerEvent = function(eventType, handler)
{
  if (!handler || !handler.handleEvent) return;

  if (this.events[eventType] == null) {
    this.events[eventType] = new Object();
    this.events[eventType].handlers = new Array();
  }
  this.events[eventType].handlers.push(handler);

  if(handler.initialize)
  {
      handler.initialize();
  }
}

AJSEventListener.prototype.handleEvent = function(event)
{
  if (event == null || event.type == null) return;

  if (this.events[event.type] != null)
  {
    for (var i = 0; i < this.events[event.type].handlers.length; i++)
    {
      this.events[event.type].handlers[i].handleEvent(event);
    }
  }
}

function AJSEventHandler(init, handle)
{
  this.initialize = init;
  this.handleEvent = handle;
}

function AJSEvent(type)
{
    this.type = type;
}

      function getY(obj)
{
    var y = obj.offsetTop;
    var p = obj.offsetParent;
    while (p)
    {
        y += p.offsetTop;
        p = p.offsetParent;
    }
    return y;
}

function getX(obj)
{
    var x = obj.offsetLeft;
    var p = obj.offsetParent;
    while (p)
    {
        x += p.offsetLeft;
        p = p.offsetParent;
    }
    return x;
}

      
registerNamespace('Assoc');

/* Assoc.PopupDiv
 * provides an easy object API for floating content divs.
 * Also provides events to notify when the div has been opened or closed. */
Assoc.PopupDiv = function(element)
{
    this.content     = element;

    Assoc.DOMEvent.AddHandler(window, 'load', this.onLoad, this);
    
    // I wasn't absolutely positive that this was the right layer to throw the
    // open/close events as opposed to a popup div manager like HotspotPopupDiv
    // but it works for me for now, feel free to rearchitect.
    this.events = new AJSEventListener();
}

Assoc.PopupDiv.prototype.visible = function()
{
    return this.content.style.visibility != 'hidden';
}

/* takes an array specifying position attributes (left, top, right etc...)
 * moves the div to the end of the DOM tree, applies the position attributes,
 * and displays the div */
Assoc.PopupDiv.prototype.show = function(el_pos)
{
    //move the content div to the end of body
    this.content.parentNode.removeChild(this.content);
    document.body.appendChild(this.content);
    
    // funny things happen if you don't clear these out.
    this.content.style.left     = '';
    this.content.style.right    = '';
    this.content.style.top      = '';
    this.content.style.bottom   = '';
    
    //apply the position attributes
    if(el_pos)
    {
        var attrs = ['top', 'bottom', 'left', 'right'];
        for(var i = 0; i < attrs.length; i++)
        {
            if(el_pos[attrs[i]]) 
                this.content.style[attrs[i]] = el_pos[attrs[i]];
        }
    }
    
    //show it
    this.content.style.visibility = 'visible';

    this.events.handleEvent(
            new Assoc.PopupDiv.Event(this, Assoc.PopupDiv.SHOW_EVENT));
}

Assoc.PopupDiv.prototype.hide = function()
{
    this.content.style.visibility = 'hidden';
    this.events.handleEvent(
            new Assoc.PopupDiv.Event(this, Assoc.PopupDiv.HIDE_EVENT));
}

Assoc.PopupDiv.prototype.onLoad = function(event)
{
    // move the content div to the end of body so we don't mess up the display
    // when we flip the display style.
    this.content.parentNode.removeChild(this.content);
    document.body.appendChild(this.content);

    // we use visibility hidden rather than display none because when display
    // is 'none', positioning algorithms can't access the height/width
    // properties of the div.
    this.content.style.position   = 'absolute';
    this.content.style.visibility = 'hidden';
    this.content.style.display    = '';
}

Assoc.PopupDiv.HIDE_EVENT = 'hide';
Assoc.PopupDiv.SHOW_EVENT = 'show';

Assoc.PopupDiv.Event = function(popup, type)
{
    this.base = AJSEvent;
    this.base(type);
    this.popup = popup;
}
Assoc.PopupDiv.Event.prototype = new AJSEvent(LISTBOX_SELECTION_EVENT);

Assoc.PopupDiv.Positioner = function(){};
Assoc.PopupDiv.Positioner.prototype.position = function(
        content_el, rel_el){};

Assoc.PopupDiv.OpenSpacePositioner = function(offset_x, offset_y)
{
    this.offset_x = offset_x;
    this.offset_y = offset_y;
}

Assoc.PopupDiv.OpenSpacePositioner.prototype.position 
    = function(content_el, rel_el)
{
    var parent = document.body;
    
    var screen_middle_x = parent.clientWidth / 2;
    var screen_middle_y = parent.clientHeight / 2;

    var trigger_left    = getX(rel_el);
    var trigger_right   = trigger_left + rel_el.clientWidth;
    var trigger_top     = getY(rel_el);
    var trigger_bottom  = trigger_top + rel_el.clientHeight;
    
    //where's the most space? I prefer go_left && go_down if possible
    var go_left = false;
    go_left = (screen_middle_x >= parent.clientWidth - trigger_right);

    var go_down = false;
    go_down = (screen_middle_y <= parent.clientHeight - trigger_bottom);
    
    var pos = { top : '', bottom : '', left : '', right : ''};
    
    if(go_left)
    {
        var left = trigger_left - this.offset_x - content_el.clientWidth;
        if(left < 0)
            pos.left = '0px';
        else
            pos.left = left + 'px';
    }
    else
    {
        var left = trigger_right + this.offset_x;
        if(left + content_el.clientWidth > parent.clientWidth)
            pos.right = '0px';
        else
            pos.left = left + 'px';
    }
    
    if(go_down)
    {
        var top = trigger_bottom + this.offset_y;
        if(top + content_el.clientHeight > parent.clientHeight)
            pos.bottom = '0px';
        else
            pos.top = top + 'px';
    }
    else
    {
        var top = trigger_top - this.offset_y - content_el.clientHeight;
        if(top < 0)
            pos.top = '0px';
        else
            pos.top = top + 'px';
    }
    
    return pos;
}

      
registerNamespace('Assoc');

Assoc.HotspotPopupDiv = function(content, config)
{
    if(content instanceof Assoc.PopupDiv)
    {
        this.popup = content;
    }
    else
    {
        this.popup   = new Assoc.PopupDiv(content);
    }

    // defaults 
    this.config = {
        trigger_on_click    : true,
        close_on_body_click : true,
        hover_open_delay    : 750,
        hover_close_delay   : 1000,
        positioner          : new Assoc.PopupDiv.OpenSpacePositioner(
                                    5,5)
    };
    
    // overwrite with passed in values
    for(var attr in config)
    {
        this.config[attr] = config[attr];
    }

    //setup content handlers
    if(this.config.hover_open_delay > -1 || this.config.hover_close_delay > -1)
    {
        Assoc.DOMEvent.AddHandler(
                this.popup.content, 'mouseover', this.onContentHover, this);
        Assoc.DOMEvent.AddHandler(
                this.popup.content, 'mouseout', this.onContentUnhover, this);
    }

    if(this.config.close_on_body_click)
    {
        Assoc.DOMEvent.AddHandler(
                window, 'click', this.onBodyClick, this);

        // This is tricky, see comments in onBodyClick to find out why
        Assoc.DOMEvent.AddHandler(

                this.popup.content, 'click', this.onContentClick, this);
    }
}

Assoc.HotspotPopupDiv.prototype.addTrigger = function(trigger_el)
{
    var me = this;
    if(this.config.trigger_on_click || this.config.close_on_body_click)
    {
        Assoc.DOMEvent.AddHandler(
                trigger_el, 'click', this.onTriggerClick, this);
    }

    if(this.config.hover_open_delay > -1 || this.config.hover_close_delay > -1)
    {
        Assoc.DOMEvent.AddHandler(
                trigger_el, 'mouseover', this.onTriggerHover, this);

        Assoc.DOMEvent.AddHandler(
                trigger_el, 'mouseout', this.onTriggerUnhover, this);
    }

    //we need to be able to distinguish this element
    trigger_el.hotspot_popup_trigger = true;
}

Assoc.HotspotPopupDiv.prototype.onTriggerClick = function(event)
{
    StopEventPropagation(event);
    
    // if !trigger_on_click then we're here because close_on_body_click and all
    // we want is the line above.
    if(!this.config.trigger_on_click) return;
    
    StopEventDefault(event);
    
    var trigger_el = Associates.Util.findAncestor(
            GetEventTarget(event), function(obj) {
                return obj.hotspot_popup_trigger == true 
            });
    
    if(this.popup.visible() && this.opening_trigger == trigger_el)
    {
        //close the thing.
        this.popup.hide();

        //clear any open timer.
        window.clearTimeout(this.open_timer);
    }
    else
    {
        //open
        this._positionAndShow(GetEventTarget(event));

        //clear any close timer
        window.clearTimeout(this.close_timer);
    }
}

Assoc.HotspotPopupDiv.prototype.onTriggerHover = function(event)
{
    // clear any close timer
    window.clearTimeout(this.close_timer);
    
    // do this even if it's already open so we reposition if we've switched
    // triggers
    if(this.config.hover_open_delay > -1)
        this._startOpenTimer(GetEventTarget(event));
}

Assoc.HotspotPopupDiv.prototype.onTriggerUnhover = function(event)
{
    //did we really leave the trigger el?
    var reltgt = mouseOutToElement(event);
    var still_here = Associates.Util.findAncestor(
            reltgt, function(obj) {
                return obj.hotspot_popup_trigger == true 
            });

    if(still_here) return;

    window.clearTimeout(this.open_timer);
    
    if(this.config.hover_close_delay > -1)
        this._startCloseTimer();
}

Assoc.HotspotPopupDiv.prototype.onContentHover = function(event)
{
    // just stop any close timeout
    window.clearTimeout(this.close_timer);
}

Assoc.HotspotPopupDiv.prototype.onContentUnhover = function(event)
{
    if(!this.popup.visible) return;

    //did we really leave the content?
    var reltgt = mouseOutToElement(event);
    var still_here = Associates.Util.findAncestor(
            reltgt, this.popup.content);

    if(still_here) return;

    window.clearTimeout(this.open_timer);

    if(this.config.hover_close_delay > -1)
        this._startCloseTimer();
}

Assoc.HotspotPopupDiv.prototype.onContentClick = function(event)
{
    if(!this.popup.visible()) return;
    StopEventPropagation(event);
}

Assoc.HotspotPopupDiv.prototype.onBodyClick = function(event)
{
    // We have click handlers on both the triggers and the content that stop
    // event propagation. So if we get a click here, we know that the click
    // wasn't on any child element of either of them, therefore we can close the
    // element.
    if(!this.popup.visible()) return;
    
    this.popup.hide();
}

Assoc.HotspotPopupDiv.prototype._positionAndShow = function(opening_target)
{
    var trigger_el = Associates.Util.findAncestor(
            opening_target, function(obj) {
                return obj.hotspot_popup_trigger == true 
            });
    
    // opening_trigger is always the last trigger that opened the content
    this.opening_trigger = trigger_el;
    
    var css = this.config.positioner.position(this.popup.content, trigger_el);
 
    this.popup.show(css);
}

Assoc.HotspotPopupDiv.prototype._startOpenTimer = function(opening_target)
{
    var me = this;
    this.open_timer = window.setTimeout(
            function() {
                me._positionAndShow(opening_target) 
                }, 
            this.config.hover_open_delay );
}

Assoc.HotspotPopupDiv.prototype._startCloseTimer = function()
{
    var me = this;
    this.close_timer = window.setTimeout(
            function() {
                me.popup.hide() 
                },
            this.config.hover_close_delay  );
}

      
var LISTBOX_SELECTION_EVENT = 'listbox_selection';

function DynamicListbox(
    idPrefix,
    elementValues,
    selectedHtmls,
    openOnHover,
    selectedIndex)
{
    this.element_values = elementValues; 
    this.id_prefix      = idPrefix;

    //the element that contains the list elements
    var list_element = document.getElementById(idPrefix + '_list');
    
    // the element that holds the selected value and pops the list open and
    // closed
    var selector_element = document.getElementById(idPrefix + '_selector');
    this.div_manager = new Assoc.HotspotPopupDiv(
            list_element,
            { 
                positioner       : new DynamicListbox.Positioner(),
                hover_open_delay : openOnHover ? 500 : -1,
                hover_close_delay : openOnHover ? 1000 : -1
            });
    this.div_manager.addTrigger(selector_element);
    
    //needed by closeListboxes
    selector_element.listbox_object = this;

    // register for the open event. We'll use it as notification to close all
    // other dynamic-listboxes on the page.
    var me = this;
    this.div_manager.popup.events.registerEvent(
            Assoc.PopupDiv.SHOW_EVENT,
            { handleEvent : function(event) { me.closeListboxes(me) } }
            );

    //save the select box content area
    this.selector_element_content = document.getElementById(
        idPrefix + '_selector_content');
    
    //attach attributes and handlers to the elements in the list.
    var elements = Associates.Util.getElementsByTagAndClass(
            'div', 'dynamic_listbox_element', list_element);

    for(var i = 0; i < elements.length; i++)
    {
        //get the element index from its ID
        var match_vars = elements[i].id.match(/_element_(\d+)$/);
        var element_index;
        if(match_vars)
        {
            element_index = match_vars[1]; 
        }
        else
        {
            //this really shouldn't happen
            continue; 
        }

        //let the element itself hold on it's own attributes.
        elements[i].listbox_selected_html = (selectedHtmls[element_index])?
            selectedHtmls[element_index] : "";
        elements[i].listbox_value         = elementValues[element_index]; 

        //some handlers.
        Assoc.DOMEvent.AddHandler(
                elements[i], 'click', this.onElementClick, this);
        Assoc.DOMEvent.AddHandler(
                elements[i], 'click', this.onElementClick, this);
        Assoc.DOMEvent.AddHandler(
                elements[i], 'mouseover', this.onElementMouseOver, this);
        Assoc.DOMEvent.AddHandler(
                elements[i], 'mouseout', this.onElementMouseOut, this);

        //is this the selected element? (do we have a selected element?)
        if(element_index == selectedIndex)
        {
            this._setSelectedElement(elements[i], true);
        }
    }
    
    //Create an event handler
    this.eventDispatcher = new AJSEventListener();
    
    // make sure the div is closed when the user hits the back button.
    Assoc.DOMEvent.AddHandler(window, 'load',
            function(event) { this.div_manager.popup.hide(); },
            this);
}

DynamicListbox.prototype.getSelectedValue = function()
{
    return this.selected_element.listbox_value;
}

DynamicListbox.prototype.setSelectedValue = function(value)
{
    for(var i = 0; i < this.element_values.length; i++)
    {
        if(this.element_values[i] == value)
        {
            var element = document.getElementById(
                    this.id_prefix + '_element_' + i);
            
            this._setSelectedElement(element, true);

            break;
        }
    }
}

DynamicListbox.prototype.addSelectionEventHandler = function(handler)
{
    this.eventDispatcher.registerEvent(LISTBOX_SELECTION_EVENT, handler);
}

DynamicListbox.prototype._setSelectedElement = function(element, dontFireEvent)
{
    var fire_event; //is this a change?
    if(this.selected_element)
    {
        if(this.selected_element != element)
        {
            fire_event = true;
        }
        
        this.selected_element.className = 'dynamic_listbox_element';
    }
    else
    {
        fire_event = true; 
    }

    element.className = 'dynamic_listbox_element element_selected';
    this.selected_element = element;
    
    this.selector_element_content.innerHTML 
            = this.selected_element.listbox_selected_html;

    if(!dontFireEvent && fire_event)
    {
        var selection_event = new DynamicListboxSelectionEvent(this);
        this.eventDispatcher.handleEvent(selection_event);
    }
}

DynamicListbox.prototype.onElementClick = function(event)
{
    StopEventPropagation(event);
    
    //close the list first, since setSelectedElement throws an event, we 
    //don't want the users handler to block the closing.
    this.div_manager.popup.hide();
    this._setSelectedElement(this._getListElementTarget(event));
}

DynamicListbox.prototype._getListElementTarget = function(event)
{
    //gets you the list element you attached the handler to.
    return Associates.Util.findAncestor(
            GetEventTarget(event),
            function(obj) { 
                return obj.className.match(/\bdynamic_listbox_element\b/) 
                }
            );
}

DynamicListbox.prototype.onElementMouseOver = function(event)
{
    var target = this._getListElementTarget(event);
    
    if(target != this.selected_element)
    {
        target.className = 'dynamic_listbox_element element_mouseover';
    }

}

DynamicListbox.prototype.onElementMouseOut = function(event)
{
    var target = this._getListElementTarget(event);

    if(target != this.selected_element)
    {
        target.className = 'dynamic_listbox_element';
    }
}

//close all listboxes, optionally, specify one that shouldn't be closed.
DynamicListbox.prototype.closeListboxes = function(dont_close)
{
    var divs = Associates.Util.getElementsByTagAndClass(
            'div', 'dynamic_listbox_selector');
    for(var i = 0; i < divs.length; i++)
    {
        if(divs[i].listbox_object != dont_close)
        {
            divs[i].listbox_object.div_manager.popup.hide();
        }
    }
}

/* A positioner for dynamic listboxes */
DynamicListbox.Positioner = function(){};
DynamicListbox.Positioner.prototype.position = function(list_el, selector_el)
{
    var pos = new Object();
    
    var left     = getX(selector_el);
    var right    = left + list_el.offsetWidth;
    
    if(right > document.body.clientWidth)
        pos['right'] = '0px';
    else
        pos['left'] = left + 'px';

    pos['top'] = getY(selector_el) + selector_el.offsetHeight + 'px';    

    return pos;
}

/*
* This is an event type that is thrown when the user makes a selection in
* a DynamicListbox
*/
function DynamicListboxSelectionEvent(listbox_object)
{
    this.listbox_object = listbox_object;
}
DynamicListboxSelectionEvent.prototype = new AJSEvent(LISTBOX_SELECTION_EVENT);

