// --- mosaic.widgets.dropdown ---
// version of june 17, 2011

// later

// todo: IE7: _now есть null или не является объектом
// todo: scrollbars
// todo: cache
// todo: тени под списком
// todo: не терять фокус при обновлении списка
// todo: $extend для настроек

(function($) {

    // --- PRE-PHASE ---

    // Check for existence all necessary namespaces.
    // Check for existence of namespace you're defining.

    if (!window.mosaic) { throw('global object [mosaic] not found'); }
    if (!window.mosaic.widgets) { throw('global object [mosaic.widgets] not found'); }

    if  (window.mosaic.widgets.dropdown) { return false; }

    // --- end of PRE-PHASE




    // --- PRIVATE GLOBAL FIELDS ---

    // These fields act on all widget instances on the page.
    // Any private field should start from "_" prefix.

        //var

            // end of PRIVATE GLOBAL FIELDS ---
            //;




    // --- PSEUDO-CONSTRUCTOR ---

    // This function creates context containing instance-specific
    // variables like idNamespace and any jQuery-elements inside widget.
    // Also it creates public object representing widget itself on the base
    // of "_proto" object. Then it create "context()" method returning
    // for this widget. Finally it returns created widget object itself.

    // idDropdown is point to HTML-instance of the widget on the page.
    // It should point to:
    //      - id/name of the central DOM element of widget (input field in case of inputs);
    //      - any DOM element inside widget;
    //      - any jQuery-object inside widget.

    window.mosaic.widgets.dropdown = function(idDropdown) {
        var
                _context        = {}
            ,   _constructor    = function() {  }
            ,   _self
            ;

        // saving id
        _context.idDropdown      = idDropdown;

        // search of container for widget
        // any jQuery-object fields should start from "$" prefix
        _context.$container     = _container(idDropdown); if (!_context.$container) { return null; }

        // init all fields here
        _context.$dropdown      = _context.$container.find('.' + _DROPDOWN_DROPDOWN_CLASSNAME);

        // create object using fake function
        _constructor.prototype  = _proto;
        _self                   = new _constructor();

        // create function returning context inside created object
        _self.context = function() {
            return _context;
        };

        return _self;
    };

    // --- end of PSEUDO-CONSTRUCTOR ---




    // --- SHORTCUT ---

    // Used to shortly point to widget inside private and public methods
    // without namespace prefixes.

    var
            dropdown = window.mosaic.widgets.dropdown

        // --- end of SHORTCUT ---
        ;




    // --- PUBLIC LOCAL METHODS ---

    // This is prototype of any instance of widget-object
    // with public methods.

    var _proto = {

            // Must be invoked only at first time!
            // This method applies settings, makes some initialization
            // and then sets up event handlers binded to this particular
            // widget by ".bind(this)". All event handlers are functions
            // from LOCAL EVENTS HANDLERS section.
            apply: function(settings) {
                _settings(this, settings);

                // any key-specific event handling
                $(document).keydown(function(e) {
                    // widget should be shown to proceed
                    if (_hidden(this)) { return undefined; }

                    // handling focusing items by arrow keys and
                    // selecting those by enter key
                    _handleFocusAndSelectByKeys(this, e);

                    _handleCancelByESC(this, e);

                    // prevent default browser reaction to pressing keys
                    return _preventDefaults(this, e);
                }.bind(this));

                // focus on item in dropdown box by mousemove
                this.context().$dropdown.mousemove(function(e) { _tryFocusByMouseMove(this, e); }.bind(this));

                // handle selecting item by mouse click
                this.context().$dropdown.click(function(e) { _trySelectByClick(this, e); }.bind(this));

                // handle exiting from widget through clicking out of it
                $(document).click(function(e) { _checkHideByClick(this, e); }.bind(this));

                return this;
            } // end of apply

            // shortcut to private "_settings"
        ,   settings: function(settings) {
                return _settings(this, settings);
            } // end of settings

            // show dropdown box if necessary
            // then filter items
        ,   call: function() {
                _call(this);
            } // end of call

            // shortcut to private "_cancel()" method
        ,   cancel: function() {
                _cancel(this);
            } // end of cancel

            // shortcut to private "_moveTo()" method
        ,   moveTo: function(offset) {
                _moveTo(this, offset);
            } // end of move_to

            // shortcut to private "_shown()" method
        ,   shown: function() {
                return _shown(this);
            } // end of shown

            // shortcut to private "_showing()" method
        ,   showing: function() {
                return _showing(this);
            } // end of showing

    };

    // --- end of PUBLIC LOCAL METHODS ---




    // --- PUBLIC GLOBAL METHODS ---

    // These methods are attached to pseudo-constructor like
    // it fields. In result, you can invoke those as "namespace.method()".

    // --- end of PUBLIC GLOBAL METHODS ---




    // --- PRIVATE CONSTS ---

    var
            // any string const used as key to inner data values
            // should start from "namespace-name"-key.
            _KEY_STATE_VISIBILITY               = 'dropdown-key-state-visibility'

        ,   _VALUE_STATE_VISIBILITY_HIDDEN      = 101

        ,   _VALUE_STATE_VISIBILITY_HIDING      = 102

        ,   _VALUE_STATE_VISIBILITY_SHOWING     = 103

        ,   _VALUE_STATE_VISIBILITY_SHOWN       = 104

        ,   _KEY_FOCUSED_ITEM                   = 'dropdown-key-field-$li-focused'

            // storing data in list items
        ,   _KEY_ITEM_DATA                      = 'dropdown-key-item-data'

            // key for storing settings
        ,   _KEY_SETTINGS                       = 'dropdown-key-settings'

            // empty jQuery object for internal purposes
        ,   _EMPTY_JQUERY                       = $()

            // bottom padding for container to embrace dropdown's shadow
        ,   _PADDING_FOR_SHADOW                 = 5

        ,   _DROPDOWN_CONTAINER_CLASSNAME       = 'mosaic-widgets-dropdown-container'

            // classname for dropdown
        ,   _DROPDOWN_DROPDOWN_CLASSNAME        = 'mosaic-widgets-dropdown-dropdown'

            // classname for items list
        ,   _DROPDOWN_LIST_CLASSNAME            = 'mosaic-widgets-dropdown-dropdown-items'

            // classname for list item
        ,   _DROPDOWN_ITEM_CLASSNAME            = 'mosaic-widgets-dropdown-dropdown-item'

            // classname for focused item
        ,   _DROPDOWN_FOCUSED_ITEM_CLASSNAME    = 'mosaic-widgets-dropdown-dropdown-item-chosen'

        ,   _SETTINGS_WIDTH                     = 'width'

        ,   _SETTINGS_VISIBLE_ITEMS_COUNT       = 'visibleItemsCount'

        ,   _SETTINGS_ANIMATION_TYPE_SLIDE      = 'slide'

        ,   _SETTINGS_ANIMATION_TYPE_FADE       = 'fade'

        // --- end of PRIVATE CONSTS ---
        ;




    // --- PRIVATE GLOBAL METHODS ---

    // These methods are invoked in global context of all widgets
    // with no touch to concrete instances.

    var
            // Find container for given namespace.
            // Finds object with id/name=idNamespace and then
            // its closest parent with selector = ".container-for-idNamespace-id-object".
            _container = function(idDropdown) {
                return inside.core.util.object(idDropdown, '.' + _DROPDOWN_CONTAINER_CLASSNAME);
            } // end _container

            // create HTML structure representing dropdown list.
        ,   _createListHTML = function(items) {
                var
                        html = ''
                    ;

                for (var i=0; i<items.length; i++) { html += _createItemHTML(items[i]); }
                html    =   '<ul class="' + _DROPDOWN_LIST_CLASSNAME + '">'
                        +       html
                        +   '</ul>';

                return html;
            } // _createListHTML

            // create HTML structure representing dropdown item.
        ,   _createItemHTML = function(item) {
                return  '<li class="' + _DROPDOWN_ITEM_CLASSNAME + '">'
                    +       '<a href="javascript:void(0);">'
                    +           item.label
                    +       '</a>'
                    +   '</li>';
            } // _createItemHTML

            // get data attached to list item
        ,   _dataOf = function($liItem) {
                return $liItem.data(_KEY_ITEM_DATA);
            } // end of _dataOf

            // get label attached to list item
        ,   _labelOf = function($liItem) {
                return $liItem.find('a').html();
            } // end of _labelOf

        // --- end of PRIVATE GLOBAL METHODS ---
        ;




    // --- PRIVATE LOCAL METHODS ---

    // Each of these methods exists as one istance,
    // but it gets "self" object as first parameter
    // which represents conrete instance of widget.
    // Since, each of these methods executes in local
    // context of widget

    var
            _call = function(self) {
                // first filter items then show dropdown
                inside.core.js.executeSafe(
                        _settings(self).filter
                    ,   function(items) {
                            if (_hidden(self)) { _show(self); }
                            _processItems(self, items);
                        }
                )(self);
            } // end of _process

        ,   _cancel = function(self) {
                inside.core.js.executeSafe(_settings(self).oncancel)(self);

                _hide(self);
            } // end of _cancel

        ,   _select = function(self, $liItem) {
                inside.core.js.executeSafe(_settings(self).onselect)(
                        self
                    ,   _labelOf($liItem)
                    ,   _dataOf($liItem)
                );

                _hide(self);
            } // end of _select

            // Handle data with items, render them and process other stuff.
            // "items" must be array of JSON-objects representing items.
            // If "items" is undefined then no rendering occures.
            // This method passed as param to "settings.filter()" inside "_call()" method
        ,   _processItems = function(self, items) {
                if ('undefined' != typeof items) {
                    self.context().$dropdown.html(_createListHTML(items));

                    // go through items and attach data
                    _$items(self).each(function(index) {
                        $(this).data(_KEY_ITEM_DATA, items[index].data);
                    });
                }

                // focus on the first item inside list
                _focus(self, _$items(self).eq(0));
            } // end of _processItems

        ,   _slideIn = function(self) {
                _stateVisibility(self, _VALUE_STATE_VISIBILITY_SHOWING);

                self.context().$container.css({
                        'display'   : 'block'
                    ,   'opacity'   : 1
                }).css({
                        'height'    :   self.context().$dropdown.outerHeight()
                                    +   _PADDING_FOR_SHADOW
                                    +   'px'
                });

                self.context().$dropdown.css({
                        'marginTop' : -self.context().$dropdown.outerHeight() - 1 + 'px'
                });

                self.context().$dropdown.animate(
                        { 'marginTop': 0 }
                    ,   mosaic.settings().animationDelay
                    ,   'swing'
                    ,   function() {
                            _stateVisibility(self, _VALUE_STATE_VISIBILITY_SHOWN);
                        }
                );
            } // end of _slideIn

        ,   _slideOut = function(self) {
                _stateVisibility(self, _VALUE_STATE_VISIBILITY_HIDING);

                self.context().$dropdown.animate(
                        { 'marginTop': -self.context().$container.outerHeight() + 'px' }
                    ,   mosaic.settings().animationDelay
                    ,   'swing'
                    ,   function() {
                            _stateVisibility(self, _VALUE_STATE_VISIBILITY_HIDDEN);
                            self.context().$container.css({
                                    'display'   : 'none'
                                ,   'height'    : 0
                            });
                        }
                );
            } // end of _slideOut

        ,   _fadeIn = function(self) {
                _stateVisibility(self, _VALUE_STATE_VISIBILITY_SHOWING);

                self.context().$container.css({
                        'display'   : 'block'
                    ,   'width'     : 0
                    ,   'height'    : 0
                    ,   'opacity'   : 0
                });

                self.context().$dropdown.css({
                        'marginTop' : 0
                });

                self.context().$container.animate(
                        {
                                'width'     :   _settings(self).width
                            ,   'height'    :   self.context().$dropdown.outerHeight()
                                            +   _PADDING_FOR_SHADOW
                                            +   'px'
                            ,   'opacity' : 1
                        }
                    ,   mosaic.settings().animationDelay
                    ,   'swing'
                    ,   function() {
                            _stateVisibility(self, _VALUE_STATE_VISIBILITY_SHOWN);
                        }
                );
            } // end of _fadeIn

        ,   _fadeOut = function(self) {
                _stateVisibility(self, _VALUE_STATE_VISIBILITY_HIDING);

                self.context().$container.animate(
                        {
                                'width'   : 0
                            ,   'height'  : 0
                            ,   'opacity' : 0
                        }
                    ,   mosaic.settings().animationDelay
                    ,   'swing'
                    ,   function() {
                            _stateVisibility(self, _VALUE_STATE_VISIBILITY_HIDDEN);
                            self.context().$container.css({
                                    'display'   : 'none'
                            });
                        }
                );
            } // end of _fadeOut

        ,   _show = function(self) {
                var
                        offset = _settings(self).getOffset
                    ;
                if (offset) { _moveTo(self, offset(self)); }

                if (_SETTINGS_ANIMATION_TYPE_FADE == _settings(self).animationType) {
                    _fadeIn(self);
                } else {
                    _slideIn(self);
                }
            } // end of _show

        ,   _hide = function(self) {
                _focused(self, null);

                if (_SETTINGS_ANIMATION_TYPE_FADE == _settings(self).animationType) {
                    _fadeOut(self);
                } else {
                    _slideOut(self);
                }
            } // end of _hide

            // move dropdown window to offset specified relative to the document body
        ,   _moveTo = function(self, offset) {
                var
                        $now    = self.context().$container.parent()
                    ;

                while (!inside.core.util.is_positioned($now)) { $now = $now.parent(); }
                offset.left     -= $now.offset().left;
                offset.top      -= $now.offset().top;
                self.context().$container.css({
                        'left'  : offset.left + 'px'
                    ,   'top'   : offset.top + 'px'
                });
            } // end of _moveTo

        ,   _state = function(self, name, value) {
                if (undefined == value) { return self.context().$container.data(name); }
                    else { self.context().$container.data(name, value); }
            } // end of _state

        ,   _stateVisibility = function(self, stateValue) {
                return _state(self, _KEY_STATE_VISIBILITY, stateValue);
            } // end of _stateVisibility

            // should always return jQuery-object, possibly 0-size
        ,   _stateFocused = function(self, stateValue) {
                // if focused is not set then try to find it and set by jQuery
                if (undefined == stateValue && undefined == _state(self, _KEY_FOCUSED_ITEM)) {
                    _state(
                            self
                        ,   _KEY_FOCUSED_ITEM
                        ,   self.context().$dropdown.find('.' + _DROPDOWN_FOCUSED_ITEM_CLASSNAME)
                    );
                }

                return _state(self, _KEY_FOCUSED_ITEM, stateValue);
            } // end of _stateFocused

        ,   _hidden = function(self) {
                var
                        state = _stateVisibility(self)
                    ;

                return null == state || _VALUE_STATE_VISIBILITY_HIDDEN == state;
            } // end of _hidden

        ,   _hiding = function(self) {
                return _VALUE_STATE_VISIBILITY_HIDING == _stateVisibility(self);
            } // end of _hiding

        ,   _showing = function(self) {
                return _VALUE_STATE_VISIBILITY_SHOWING == _stateVisibility(self);
            } // end of _showing

        ,   _shown = function(self) {
                return _VALUE_STATE_VISIBILITY_SHOWN == _stateVisibility(self);
            } // end of _shown

        ,   _stateSettings = function(self, settingsValue) {
                return _state(self, _KEY_SETTINGS, settingsValue);
            } // end of _stateSettings

        ,   _focused = function(self, $liFocused) {
                if (undefined != $liFocused) {
                    // mark current focused element as unfocused
                    _stateFocused(self).removeClass(_DROPDOWN_FOCUSED_ITEM_CLASSNAME);

                    // clear or set focus-item
                    if (null == $liFocused) {
                        _stateFocused(self, _EMPTY_JQUERY);
                    } else {
                        _stateFocused(self, $liFocused);
                    }

                    // mark new focused element as focused
                    _stateFocused(self).addClass(_DROPDOWN_FOCUSED_ITEM_CLASSNAME);
                }

                return _stateFocused(self);
            } // end of _focused

        ,   _focus = function(self, $liItem) {
                if (!$liItem.size()) { return null; }

                // set focus to
                _focused(self, $liItem);

                if ($liItem.position().top < 0) {
                    self.context().$dropdown.get(0).scrollTop += $liItem.position().top;
                }
                if ($liItem.position().top >= self.context().$dropdown.height()) {
                    self.context().$dropdown.get(0).scrollTop += $liItem.outerHeight();
                }
                if (1 == self.context().$dropdown.get(0).scrollTop % self.context().$dropdown.height()) {
                    self.context().$dropdown.get(0).scrollTop -= 1;
                }

                inside.core.js.executeSafe(_settings(self).onfocus)(self, $liItem);

                return $liItem;
            } // end of _focus

        ,   _focusNext = function(self) {
                _focus(self, _focused(self).next());
            } // end of _focusNext

        ,   _focusPrev = function(self) {
                _focus(self, _focused(self).prev());
            } // end of _focusPrev

        ,   _focusPageUp = function(self) {
                for (var i=0; i<_pageSize(self); i++) {
                    _focus(self, _focused(self).prev());
                }
            } // end of _focusNext

        ,   _focusPageDown = function(self) {
                for (var i=0; i<_pageSize(self); i++) {
                    _focus(self, _focused(self).next());
                }
            } // end of _focusPrev

        ,   _pageSize = function(self) {
                return Math.ceil(self.context().$dropdown.height() / parseInt(self.context().$dropdown.css('lineHeight')));
            } // end of _pageSize

        ,   _settings = function(self, settings) {
                var
                        _settings = _stateSettings(self)
                    ;
                _settings = 'undefined' != typeof _settings ? _settings : {};

                if (settings) {
                    for (var key in settings) {
                        _settings[key] = settings[key];

                        if (_SETTINGS_WIDTH == key) {
                            _width(self, settings[key]);
                        } else if (_SETTINGS_VISIBLE_ITEMS_COUNT == key) {
                            _visible(self, settings[key]);
                        }
                    }

                    _stateSettings(self, _settings);
                }

                return _settings;
            } // end of _settings

        ,   _width = function(self, value) {
                self.context().$container.css({
                        'width': value
                });
            } // end of _width

            // sets amount of visible items in the dropdown list
        ,   _visible = function(self, value) {
                self.context().$dropdown.css({
                        'height'    :   value * parseInt(self.context().$dropdown.css('lineHeight'))
                                    +   'px'
                });
            } // end of _visible

        ,   _$items = function(self) {
                return self.context().$dropdown.find('li');
            } // end of _$items

        // --- end of PRIVATE LOCAL METHODS ---
        ;




    // --- LOCAL EVENT HANDLERS ---

    // any event handling attached to particular widget
    // should be processed in routines below.
    // Apply() method just sets pointers to these handlers.

    var
            // select item in dropdown box by mouse click
            _trySelectByClick = function(self, e) {
                var
                        $item = $(e.target).closest('.' + _DROPDOWN_ITEM_CLASSNAME)
                    ;
                if ($item.size()) {
                    _select(self, $item);
                }
            } // _trySelectByClick

            // check exiting from dropdown box through clicking outside
        ,   _checkHideByClick = function(self, e) {
                if (_shown(self, self.context()) && null == _container(e.target)) {
                    var
                            preventHideOnClick = _settings(self).preventHideOnClick
                        ;
                    if (!preventHideOnClick || !preventHideOnClick(self, e)) {
                        _cancel(self);
                    }
                }
            } // _checkHideByClick

            // focus on item inside dropdown box by mouse move
        ,   _tryFocusByMouseMove = function(self, e) {
                if (self.context().$dropdown.get(0) == e.target) { return undefined; }

                _focus(
                        self
                    ,   $(e.target).closest('.' + _DROPDOWN_ITEM_CLASSNAME)
                );
            } // _tryFocusByMouseMove

            // handle moving focus by arrow keys
            // and selecting item by enter.
            // Widget should be shown to proceed. This is checked inside
            // outer event handler.
        ,   _handleFocusAndSelectByKeys = function(self, e) {
                // should be focused before
                if (!_focused(self).size()) { return undefined; }

                if (inside.code.KEY_UP == e.which) {
                    _focusPrev(self);
                } else if (inside.code.KEY_DOWN == e.which) {
                    _focusNext(self);
                } else if (inside.code.KEY_PAGE_UP == e.which) {
                    _focusPageUp(self);
                } else if (inside.code.KEY_PAGE_DOWN == e.which) {
                    _focusPageDown(self);
                } else if (inside.code.KEY_ENTER == e.which) {
                    _select(self, _focused(self));
                }
            } // _handleFocusAndSelectByKeys

            // Widget should be shown to proceed. This is checked inside
            // outer event handler.
        ,   _handleCancelByESC = function(self, e) {
                if (inside.code.KEY_ESC == e.which) {
                    _cancel(self);
                }
            } // _handleCancelByESC

            // Prevents default browser behavior when user presses keys.
            // Widget should be shown to proceed. This is checked inside
            // outer event handler.
        ,   _preventDefaults = function(self, e) {
                if (inside.code.KEY_UP == e.which) {
                    e.preventDefault(); return false;
                } else if (inside.code.KEY_DOWN == e.which) {
                    e.preventDefault(); return false;
                } else if (inside.code.KEY_ENTER == e.which) {
                    e.preventDefault(); return false;
                } else if (inside.code.KEY_ESC == e.which) {
                    e.preventDefault(); return false;
                }

                return undefined;
            }

        // --- end of LOCAL EVENT HANDLERS ---
        ;




    // --- GLOBAL EVENT HANDLERS ---

    $(document).click(function(e) {
        // place your code for event handling like this
    });

    // --- end of GLOBAL EVENT HANDLERS ---

})(jQuery);
