(function($, undefined){

    var PLUGIN_NAME = 'tl_dropdown';
    var API = PLUGIN_NAME + 'API';
    var EVENT_ONCHANGE = 'change.' + PLUGIN_NAME;

    var defaults = {
        position: 'right',
        className: null,
        selected: null,
        items: null,
        showArrow: true,
        onRefresh: null,
        onCreateItem: null,
        onChange: null
    };

    var type = $.type;

    var sliceArgs = function(args, from) {
         return Array.prototype.slice.call(args, from || 0);
    };

    var ensureType = function(val, expectedType, defValue) {
        return type(val) === expectedType ? val : defValue;
    };

    var isNullOrUndefined = function(val) {
        return val === null || typeof(val) === 'undefined';
    };

    var setter = function(fn) {
        return function() {
            var args = arguments;
            return this.each(function() {
                fn.apply(this, args);
            });
        };
    };

    var each = setter;

    var getter = function(fn) {
        return function() {
            if (this.size() > 0) {
                return fn.apply(this[0], arguments);
            }
        };
    };

    var getter_setter = function(fn) {
        return function() {
            var args = arguments;
            if (args.length == 0) {
                if (this.size() > 0) {
                    return fn.apply(this[0], args);
                }
            } else {
                return this.each(function() {
                    fn.apply(this, args);
                });
            }
        };
    };

    var hasAPI = function(fn, noApiCallback) {
        return function() {
            if ($(this).data(API)) {
                return fn.apply(this, arguments);
            }
            return noApiCallback ? noApiCallback.call(this) : this;
        };
    };

    var apiCall = function(method) {
        return function() {
            var api = $(this).data(API);
            return api ? api[method].apply(this, arguments) : undefined;
        };
    };

    var prop = function(getter, setter) {
        return function() {
            var args = arguments;
            if (args.length == 0) {
                return getter && getter();
            } else {
                return setter && setter.apply(this, args);
            }
        };
    };

    var createDropDown = function(options) {
        var $this = $(this);
        if ($this.data(API)) {
            return;
        }

        var opts = $.extend({}, defaults, options);
        var items = opts.items = ensureType(opts.items, 'array', []);
        var onCreateItem = opts.onCreateItem = ensureType(opts.onCreateItem, 'function', null);
        var onRefresh = opts.onRefresh = ensureType(opts.onRefresh, 'function', null);

        var $cont, $currentCont, $currentInnerCont, $list, $sourceList, $arrowCont;
        var $items = $();
        var $selectedItem = null;
        var selectedValue = opts.selected;

        var init = function() {
            createContainer();
            createList();
            bindEvents();
            setupAPI();
            if (opts.onChange) {
                onChange(opts.onChange);
            }
        };

        var createContainer = function() {
            $sourceList = $this.find('ul');
            $this.empty().append(
                $cont = $('<div class="tl-dropdown"></div>').append(
                    $('<div class="tl-dropdown-inner"></div>').append(
                        $currentCont = $('<div class="tl-dropdown-current"></div>').append(
                            $currentInnerCont = $('<div class="tl-dropdown-current-inner"></div>'),
                            $arrowCont = $('<div class="tl-dropdown-arrow">▼</div>')
                        ),
                        $list = $('<ul class="tl-dropdown-list"></ul>')
                    )
                )
            );
            $cont.addClass(opts.position == 'right' ? 'tl-dropdown-right' : 'tl-dropdown-left').addClass(opts.className);
            if ($sourceList.length) {
                createList = cloneList;
            }
            $arrowCont.toggle(!!opts.showArrow);
        };

        var cloneList = function() {
            $list.empty();
            $items = $();
            items = [];
            $sourceList.find('li').each(function(index, item) {
                var $li = $(this);
                items[index] = item;
                var $item = $('<li/>', {'class': 'tl-dropdown-item'}).data('index', index).data('value', $li.data('value')).append($li.children().clone(true));
                $item.bind('click', function() {
                    var changed = $item != $selectedItem;
                    selectItem($item);
                    hide();
                    if (changed) {
                        notifyOnChange();
                    }
                });
                $list.append($item);
                $items = $items.add($item);
            });
            var _selectedValue = selectedValue;
            $selectedItem = selectedValue = null;
            selectItem($items.eq(0));
            selectItem(findItemByValue(_selectedValue));
        };

        var createList = function() {
            $list.empty();
            $items = $();
            $.each(items, function(index, item) {
                var $elem = null;
                var value = item;

                if (onCreateItem) {
                    var res = onCreateItem.call($this[0], index, item);
                    if (res && type(res) == 'array' && res.length == 2) {
                        value = res[0];
                        res = res[1];
                        if (res.jquery) {
                            $elem = res;
                        } else if (type(res) == 'string') {
                            item = {label: res, value: value};
                        }
                    }
                }

                if (!$elem) {
                    var label = null;
                    if (type(item) == 'object' && item.label) {
                        label = String(item.label);
                        if (!isNullOrUndefined(item.value)) {
                            value = String(item.value);
                        }
                    } else if (type(item) == 'string' || type(item) == 'number') {
                        label = String(item);
                    }
                    if (label) {
                        $elem = $('<span/>').text(label);
                        if (type(item.hint) == 'string') {
                            $elem.attr('title', item.hint);
                        }
                    }
                }

                if ($elem) {
                    var $item = $('<li/>', {'class': 'tl-dropdown-item'}).data('index', index).data('value', value).append($elem);
                    $item.bind('click', function() {
                        var changed = value != selectedValue;
                        selectItem($item);
                        hide();
                        if (changed) {
                            notifyOnChange();
                        }
                    });
                    $list.append($item);
                    $items = $items.add($item);
                }
            });
            var _selectedValue = selectedValue;
            $selectedItem = selectedValue = null;
            selectItem($items.eq(0));
            selectItem(findItemByValue(_selectedValue));
        };

        var findItemByValue = function(value) {
            var $item = $items.filter(function() {
                return $(this).data('value') == value;
            });
            return $item.length == 0 ? null : $item.first();
        };

        var selectItem = function($item) {
            if ($item && $item.length == 1) {
                if (onRefresh) {
                    var itemData = items[$item.data('index')];
                    onRefresh.call($this[0], $currentInnerCont, itemData, $item);
                } else {
                    $currentInnerCont.empty().append($item.children().clone(false));
                }
                $selectedItem = $item;
                selectedValue = $item.data('value');
            }
        };

        var isExpanded = function() {
            return $cont.is('.tl-dropdown-expanded');
        };

        var show = function() {
            $list.css('opacity', 0);
            $cont.addClass('tl-dropdown-expanded');
            var cwidth = $currentCont.outerWidth();
            var lwidth = $list.outerWidth();
            if (cwidth > lwidth) {
                $list.width(cwidth - (lwidth - $list.width()));
            }
            $list.animate({opacity: 1}, 200);
        };

        var hide = function() {
            $list.animate({opacity: 0}, 200, function() {
                $cont.removeClass('tl-dropdown-expanded');
            });
        };

        var toggle = function() {
            isExpanded() ? hide() : show();
        };

        var bindEvents = function() {
            $currentCont.bind('click', function() {
                toggle();
            });

            var focusTimeout = null;
            $cont.bind('mouseenter', function() {
                if (focusTimeout) {
                    clearTimeout(focusTimeout);
                    focusTimeout = null;
                }
            }).bind('mouseleave', function() {
                if (!focusTimeout) {
                    focusTimeout = setTimeout(function() {
                        focusTimeout = null;
                        hide();
                    }, 500);
                }
            });
        };

        var onChange = function(fn) {
            $cont.bind(EVENT_ONCHANGE, fn);
        };

        var notifyOnChange = function() {
            $cont.trigger(EVENT_ONCHANGE, [getSelectedValue(), getSelectedItem()]);
        };

        var getSelectedValue = function() {
            return $selectedItem ? $selectedItem.data('value') : null;
        };

        var setSelectedValue = function(value) {
            selectItem(findItemByValue(value));
        };

        var getSelectedItem = function() {
            return $selectedItem ? items[$selectedItem.data('index')] : null;
        };

        var setupAPI = function() {
            $this.data(API, {
                show: show,
                hide: hide,
                toggle: toggle,
                isExpanded: isExpanded,
                onChange: onChange,
                selectedValue: prop(getSelectedValue, setSelectedValue),
                selectedItem: prop(getSelectedItem, null)
            });
        };

        init();
    };

    var methods = {
        init : each(function(options) {
            createDropDown.call(this, options);
        }),
        show: each(apiCall('show')),
        hide: each(apiCall('hide')),
        toggle: each(apiCall('toggle')),
        isExpanded: getter(apiCall('isExpanded')),
        onChange: setter(apiCall('onChange')),
        value: getter_setter(apiCall('selectedValue')),
        selectedItem: getter_setter(apiCall('selectedItem'))
    };

    $.fn[PLUGIN_NAME] = function(method) {
        if (methods[method]) {
            return methods[method].apply(this, sliceArgs(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' +  method + ' does not exist on jQuery.' + PLUGIN_NAME);
        }
    };

    $.fn[PLUGIN_NAME].defaults = defaults;

})(jQuery);

