/** 
 * 
 * @author Yury Ksenevich
 * @param sName trigger id
 * @param aArgs trigger arguments
 * @return Trigger new instance
 */
function Trigger(sName, aArgs)
{
    this.sName          = sName || '';
    this.aArgs          = aArgs || {};
    Trigger._aListeners = Trigger._aListeners || {};
    var aListeners      = Trigger._aListeners[sName] || [];

    for (var i = 0; i < aListeners.length; i++)
    {
        aListeners[i](this);
    }
}

Trigger.addListener = function(sName, oCallback)
{
    Trigger._aListeners        = Trigger._aListeners || {};
    Trigger._aListeners[sName] = Trigger._aListeners[sName] || [];
    Trigger._aListeners[sName].push(oCallback);
}

/**
 * @author Dmitry Karpenkov
 * @version 0.0.1.2
 * @class Select
 * @constructor
 * @param  {mixed} element The source HTMLSelect or ID HTMLSelect
 * @param  {Array} options The source HTMLSelect ID
 */
function Select(element, options) {
    var self = this;
    // source <select /> element
    
    if (typeof element === 'string') {
        this.select = document.getElementById(element);
    } else {
        this.select = element;
    }
    
    // hide the select field
    this.select.style.display = 'none';
    
    
    if (!this.select || this.select.tagName.toLowerCase() != 'select')
        return;
    this.options = this.select.options;
    // initialize options
    this._initializeOptions(options);
    
    // create and build div structure
    this.selectArea = document.createElement('div');
    var leftDiv = document.createElement('div');
    var rightDiv = document.createElement('div');
    this.textContainer = document.createElement('div');
    this.textContainer.id = this.select.id + 'Text';
    var text = document.createTextNode(this.emptyText);
    this.selectArea.id = this.select.id + 'SelectArea';
    this.selectArea.style.width = parseInt(this.width) + 'px';
    this.selectArea.style.height = parseInt(this.height) + 'px';
    addClass(this.selectArea, this.selectAreaStyle);
    addClass(leftDiv, this.selectAreaLeftStyle);
    addClass(rightDiv, this.selectAreaRightStyle);
    addClass(this.textContainer, this.selectAreaCenterStyle)
    this.textContainer.appendChild(text);
    this.selectArea.appendChild(leftDiv);
    this.selectArea.appendChild(rightDiv);
    this.selectArea.appendChild(this.textContainer);
    //insert select div
    this.select.parentNode.insertBefore(this.selectArea, this.select);
    // fix IE6 bug
    this.textContainer.style.width = this.width - 7 - getDimensions(leftDiv).width - getDimensions(rightDiv).width + 'px'
    //build & place options div
    this.optionsArea = document.createElement('ul');
    this.optionsArea.id = this.select.id + 'Options';
    this.optionsArea.style.width = parseInt(this.width)-2 + 'px';
    if (this.dropDownSize > 0 && this.options.length > this.dropDownSize) {
        this.optionsArea.style.height = this.dropDownSize * this.optionHeight + 'px';
    }
    this.optionsArea.className = this.optionsInvisibleStyle;
    //get select's options and add to options div
    
    var num = 0;
    var selectOptions = jQuery(this.select).children();
    for(var w = 0; w < selectOptions.length; w++) {
        if (selectOptions[w].nodeName === 'OPTGROUP') {
            var optionHolder = document.createElement('li');
            optionHolder.id = this.select.id + 'OptionText' + num;
            
            addClass(optionHolder, 'optgroup');
            
            var optionTxt = document.createTextNode(selectOptions[w].label);
            //optionHolder.position = w;
            optionHolder.appendChild(optionTxt);
            this.optionsArea.appendChild(optionHolder);
            
            var optGroupOptions = jQuery(selectOptions[w]).children();
            
            for (var i = 0; i < optGroupOptions.length; i++) {
                this.createOption(optGroupOptions[i], num, true);
                num++;
            }
        } else {
            this.createOption(selectOptions[w], num);
            num++;
        }
    }
    
    //insert options div
    this.selectArea.appendChild(this.optionsArea);
    
    this._initializeEventHandlers();
}

Select.prototype = {
    /**
     * @description Initializes options.
     * @method _initializeOptions
     * @param {Array}	options	The options to initialize from.
     * @return {void}
     * @private
     */
    _initializeOptions: function(options) {
        // initialize CSS styles
        var options = options || {};
        this.selectAreaStyle = options.selectAreaStyle || 'select-area';
        this.selectAreaOpenedStyle = options.selectAreaOpenedStyle || 'select-area-opened';
        this.selectAreaLeftStyle = options.selectAreaLeftStyle || 'select-area-left';
        this.selectAreaRightStyle = options.selectAreaRightStyle || 'select-area-right';
        this.selectAreaCenterStyle = options.selectAreaCenterStyle || 'select-area-center';
        this.optionsVisibleStyle = options.optionsVisibleStyle || 'select-options-visible';
        this.optionsInvisibleStyle = options.optionsInvisibleStyle || 'select-options-invisible';
        this.optionSelectedStyle = options.optionSelectedStyle || 'select-option-selected';
        this.optionHoveredStyle = options.optionHoveredStyle || 'select-option-hovered';
        // initialize other options
        this.emptyText = options.emptyText || ' -- ' + ArboreusSource.tr.I18N_SELECT_TITLE + ' -- ';
        this.optionsSeparator = options.optionsSeparator || ',';
        this.optionsOverlap = options.optionsOverlap || 1;
        this.width = options.width || getDimensions(this.select).width;
        this.height = options.selectHeight || 20;
        this.optionHeight = parseInt(options.optionHeight) || 15;
        this.opened = false;
        this.hoveredIndex = -1;
        this.dropDownSize = parseInt(options.dropDownSize) || 0;
    },
    
    selectAreaClickHandler: function() {
        var self = this.self;
        self.toggle();
        if (self.opened) {
            if (!self.addedKeyDownHandler) {
                addEventHandler(self.selectArea, 'keydown', self.selectKeyDownHandler);
                self.addedKeyDownHandler = true;
            }
        } else {
            if (self.addedKeyDownHandler) {
                removeEventHandler(self.selectArea, 'keydown', self.selectKeyDownHandler);
                self.addedKeyDownHandler = false;
            }
        }
    },
    
    /**
     * @description Initializes select event handlers.
     * @method _initializeEventHandlers
     * @return {void}
     * @private
     */
    _initializeEventHandlers: function() {
        var self = this;
        var body = document.getElementsByTagName('body')[0];
        
        this.self = self;

        this.selectKeyDownHandler = function(e) {
            var e = e || window.event;
            self._handleKeyDownEvent(e);
        }
        
        var bodyClickHandler = function() {
            self.close();
            if (self.addedKeyDownHandler) {
                removeEventHandler(self.selectArea, 'keydown', this.selectKeyDownHandler);
                self.addedKeyDownHandler = false;
            }
        }
        
        var selectMouseOutHandler = function() {
            if (!self.addedbodyClickHandler) {
                addEventHandler(self.selectArea, 'click', bodyClickHandler);
                self.addedbodyClickHandler = true;
            }
        }
        
        var selectMouseOverHandler = function() {
            if (self.addedbodyClickHandler) {
                removeEventHandler(self.selectArea, 'click', bodyClickHandler);
                self.addedbodyClickHandler = false;
            }
        }
        
        var selectAreaClickHandler = function() {
            self.toggle();
            if (self.opened) {
                if (!self.addedKeyDownHandler) {
                    addEventHandler(self.selectArea, 'keydown', self.selectKeyDownHandler);
                    self.addedKeyDownHandler = true;
                }
            } else {
                if (self.addedKeyDownHandler) {
                    removeEventHandler(self.selectArea, 'keydown', self.selectKeyDownHandler);
                    self.addedKeyDownHandler = false;
                }
            }
        }
        
        this.selectArea.self = self;
        
        addEventHandler(this.selectArea, 'click', selectAreaClickHandler);
        addEventHandler(this.selectArea, 'mouseover', selectMouseOverHandler);
        addEventHandler(this.selectArea, 'mouseout', selectMouseOutHandler);
        addEventHandler(this.optionsArea, 'mouseover', selectMouseOverHandler);
        addEventHandler(this.optionsArea, 'mouseout', selectMouseOutHandler);

    },
    
    /**
     * @description Handles select key down events.
     * @method _handleKeyDownEvent
     * @param {Event}	e	The event to be handled from.
     * @return {void}
     * @private
     */
    _handleKeyDownEvent: function(e) {
        var keyCode = e.keyCode;
        switch (keyCode) {
            case 40: // down
                this.unhoverOption(this.hoveredIndex);
                this.hoveredIndex++;
                if (this.hoveredIndex >= this.options.length) 
                    this.hoveredIndex = 0;
                this.hoverOption(this.hoveredIndex);
                break;
            case 38: // up
                this.unhoverOption(this.hoveredIndex);
                this.hoveredIndex--;
                if (this.hoveredIndex < 0) 
                    this.hoveredIndex = this.options.length - 1;
                this.hoverOption(this.hoveredIndex);
                break;
            case 27: // escape
                this.close();
                break;
            case 32: // space
                this.selectOption(this.hoveredIndex);
                break;
            case 13: // enter
                if (!this.options[this.hoveredIndex].selected)
                    this.selectOption(this.hoveredIndex);
                this.close();
                break;
            default:
                break;
        }
    },
    
    createOption: function(optionEl, num, isGroupOption) {
        var self = this;

        var optionHolder = document.createElement('li');
        optionHolder.id = this.select.id + 'Option' + num;

        if (typeof optionEl.altClass !== 'undefined') {
            jQuery(optionHolder).addClass(optionEl.altClass);
        }

        if (optionEl.text.length == 0)
            optionEl.text = this.emptyText;
        var optionTxt = Pages.Renderer.revertEscapedHtmlRenderer(optionEl.text);
        
        optionHolder.position = num;
        optionHolder.onclick = function() {
            if (self.select.multiple) {
                self.selectOption(this.position);
            }
            else {
                self.selectOption(this.position);
            }
        }
        optionHolder.onmouseover = function() {
            self.unhoverOption(self.hoveredIndex);
            self.hoveredIndex = this.position;
            self.hoverOption(self.hoveredIndex);
        }
        optionHolder.onmouseout = function() {
            self.unhoverOption(this.position);
            self.hoveredIndex = -1;
        }

        if (optionEl.title.length != 0) {
            var imgEl = document.createElement('img');
            imgEl.src = optionEl.title;

            var divEl = document.createElement('div');
            divEl.appendChild(imgEl);

            optionHolder.appendChild(divEl);
        }

        optionHolder.innerHTML = optionTxt;
        this.optionsArea.appendChild(optionHolder);
        //check for pre-selected items
        if(optionEl.selected) {
            this.selectOption(num);
            addClass(optionHolder, this.optionSelectedStyle);
        }
        
        if (isGroupOption) {
            addClass(optionHolder, 'group-option');
        }
    },
    
    /**
     * @description Opens select.
     * @method open
     * @return {void}
     */
    open: function() {
        if (hasClass(this.optionsArea, this.optionsInvisibleStyle))
            replaceClass(this.optionsArea, this.optionsInvisibleStyle, this.optionsVisibleStyle);
        addClass(this.selectArea, this.selectAreaOpenedStyle);
        this.opened = true;
    },
    
    /**
     * @description Closes select.
     * @method close
     * @return {void}
     */
    close: function() {
        if (hasClass(this.optionsArea, this.optionsVisibleStyle))
            replaceClass(this.optionsArea, this.optionsVisibleStyle, this.optionsInvisibleStyle);
        removeClass(this.selectArea, this.selectAreaOpenedStyle);
        if(this.select.onchange && this.opened)
        {
            this.select.onchange();
        }
        this.opened = false;
    },
    
    /**
     * @description Toggle select.
     * @method toggle
     * @return {void}
     */
    toggle: function() {
        this.opened ? this.close() : this.open();
    },

    /**
     * @description Selects specified option.
     * @method selectOption
     * @param {int}	selectedIndex	The option index to be selected.
     * @return {void}
     */
    selectOption: function(selectedIndex) {
        if(typeof selectedIndex == "undefined") { return false; }
        
        new Trigger(this.select.id + '-change', {
            id    : this.select.id, 
            index : selectedIndex, 
            value : this.select.value
            });

        //feed selected option to the actual select field
        if (this.select.multiple) {
            var option = document.getElementById(this.select.id + 'Option' + selectedIndex);
            if (option) {
                this.options[selectedIndex].selected = !this.options[selectedIndex].selected;
                this.options[selectedIndex].selected ? addClass(option, this.optionSelectedStyle) : removeClass(option, this.optionSelectedStyle);
            }
            // modify selected option
            var text = '';
            for (var k = 0; k < this.options.length; k++) {
                if (this.options[k].selected)
                    text += this.options[k].text + this.optionsSeparator;
            }
            if (text.length > this.optionsSeparator.length)
                text = text.substring(0, text.length - this.optionsSeparator.length);
            else if (text.length == 0)
                text = this.emptyText;
            var newText = document.createElement('span');
            newText.innerHTML = Pages.Renderer.revertEscapedHtmlRenderer(text);

            if (this.options[selectedIndex].title.length != 0) {
                var imgEl = document.createElement('img');
                imgEl.src = this.options[selectedIndex].title;

                var divEl = document.createElement('div');
                divEl.appendChild(imgEl);

                jQuery(this.textContainer).html('');
                jQuery(this.textContainer).append(divEl);
                jQuery(this.textContainer).append(newText);
            } else {
                jQuery(this.textContainer).html('');
                jQuery(this.textContainer).append(newText);
            }
        }
        else {
            for (var k = 0; k < this.options.length; k++) {
                if (k == selectedIndex) {
                    this.options[k].selected = true;
                    var option = document.getElementById(this.select.id + 'Option' + k);
                    if (option)
                        addClass(option, this.optionSelectedStyle);
                }
                else {
                    this.options[k].selected = false;
                    var option = document.getElementById(this.select.id + 'Option' + k);
                    if (option)
                        removeClass(option, this.optionSelectedStyle);
                }
                //show selected option
                if(selectedIndex != -1)
                {
                    var newText = Pages.Renderer.revertEscapedHtmlRenderer(this.options[selectedIndex].text);

                    if (this.options[selectedIndex].title.length != 0) {
                        var imgEl = document.createElement('img');
                        imgEl.src = this.options[selectedIndex].title;

                        var divEl = document.createElement('div');
                        divEl.appendChild(imgEl);
                        
                        jQuery(this.textContainer).html('');
                        jQuery(this.textContainer).append(divEl);
                        jQuery(this.textContainer).append(newText);
                    } else {
                        jQuery(this.textContainer).html('');
                        jQuery(this.textContainer).append(newText);
                    }
                }
            }
        }
        new Trigger(this.select.id + '-after-change', {
            id    : this.select.id, 
            index : selectedIndex, 
            value : this.select.value
            });
    },
    
    /**
     * @description Hovers specified option.
     * @method hoverOption
     * @param {int}	hoveredIndex	The option index to be hovered.
     * @return {void}
     */
    hoverOption: function(hoveredIndex) {
        if (hoveredIndex >= 0 && hoveredIndex < this.options.length) {
            var hoveredOption = document.getElementById(this.select.id + 'Option' + hoveredIndex);
            addClass(hoveredOption, this.optionHoveredStyle);
        }
    },
    
    /**
     * @description Unhovers specified option.
     * @method unhoverOption
     * @param {int}	hoveredIndex	The option index to be unhovered.
     * @return {void}
     */
    unhoverOption: function(hoveredIndex) {
        if (hoveredIndex >= 0 && hoveredIndex < this.options.length) {
            var hoveredOption = document.getElementById(this.select.id + 'Option' + hoveredIndex);
            removeClass(hoveredOption, this.optionHoveredStyle);
        }
    }
}

//Useful functions

/**
 * @description Returns element dimensions.
 * @method getDimensions
 * @param {HTMLElement}	el	The DOM elementement to get dimensions of.
 * @return {Array} HTMLElement offset.
 */
function getDimensions(el) {
    removeClass(el, 'hide');
    
    if (el.style.display != 'none' && el.style.display != null) // Safari bug
        return {width: el.offsetWidth, height: el.offsetHeight};
    // All *Width and *Height properties give 0 on els with display none,
    // so enable the el temporarily
    var els = el.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = el.clientWidth;
    var originalHeight = el.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};    
}

/**
 * @description Returns an HTMLElement offset.
 * @method getOffset
 * @param {HTMLElement}	el	The DOM element to get offset of.
 * @return {Array} HTMLElement offset.
 */
function getOffset(el) {
    var valueT = 0, valueL = 0;
    do {
      valueT += el.offsetTop  || 0;
      valueL += el.offsetLeft  || 0;
      el = el.offsetParent;
      if (el) {
        if (el.tagName.toLowerCase() == 'body')
            break;
        var pos = el.style.position;
        if (pos == 'relative' || pos == 'absolute')
            break;
      }
    } while (el);
    return {left: valueL, top: valueT};
}

/**
 * @description Determines whether an HTMLElement has the given className.
 * @method hasClass
 * @param {HTMLElement}	el		The DOM element to test.
 * @param {String}		className	The class name to search for.
 * @return {Boolean | Array} A boolean value or array of boolean values.
 */
function hasClass(el, className) {
    var elClassName = el.className;
    return (elClassName.length > 0 && (elClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elClassName)));
}

/**
 * @description Adds a class name to a given el.
 * @method addClass         
 * @param {HTMLElement}	el		The DOM element to add the class to.
 * @param {String}		className	The class name to add to the class attribute.
 * @return {void}
 */
function addClass(el, className) {
    if (!hasClass(el, className))
        el.className += (el.className ? ' ' : '') + className;
}

/**
 * @description Removes a class name from a given el.
 * @method removeClass         
 * @param {HTMLElement}	el		The DOM element or to remove the class from.
 * @param {String}		className	The class name to remove from the class attribute.
 * @return {void}
 */
function removeClass(el, className) {
    el.className = el.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').replace(/^\s+/, '').replace(/\s+$/, '');
}

/**
 * @description Replaces a class name fom a given el.
 * @method removeClass         
 * @param {HTMLElement}	el			The DOM elementement to remove the class from.
 * @param {String}		oldClassName	The class name to remove from the class attribute.
 * @param {String}		newClassName	The class name to add to the class attribute.
 * @return {void}
 */
function replaceClass(el, oldClassName, newClassName) {
    removeClass(el, oldClassName);
    addClass(el, newClassName);
}

/**
 * @description Adds a DOM event directly without the caching, cleanup, scope adj, etc.
 * @method addEventHandler
 * @param {HTMLElement}	el		The DOM element to bind the handler to.
 * @param {String}		name		The name of event handler.
 * @param {function} 		handler	The callback to invoke.
 * @return {void}
 */
function addEventHandler(el, name, handler) {
    el.addEventListener ? el.addEventListener(name, handler, false) : el.attachEvent('on' + name, handler);
}

/**
 * @description Removes a DOM event.
 * @method removeEventHandler
 * @param {HTMLElement}	el		The DOM element to bind the handler to.
 * @param {String}		name		The name of event handler.
 * @param {function} 		handler	The callback to invoke.
 * @return {void}
 */
function removeEventHandler(el, name, handler) {
    el.removeEventListener ? el.removeEventListener(name, handler, false) : el.detachEvent('on' + name, handler);
}


