//UUID
function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
        .toString(16)
        .substring(1);
}

function guid() {
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

// Outer HTML
(function ($) {
    $.fn.outerHtml = function () {
        if (this.length == 0) return false;
        var elem = this[0], name = elem.tagName.toLowerCase();
        if (elem.outerHTML) return elem.outerHTML;
        var attrs = $.map(elem.attributes, function (i) { return i.name + '="' + i.value + '"'; });
        return "<" + name + (attrs.length > 0 ? " " + attrs.join(" ") : "") + ">" + elem.innerHTML + "</" + name + ">";
    };
})(jQuery);

// Bind twitter typeahead
ko.bindingHandlers.typeahead = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);
        var allBindings = allBindingsAccessor();
        var value = valueAccessor();
        var typeaheadArr = ko.utils.unwrapObservable(value.source);

        /* TYPEAHEAD PUBLIC CLASS DEFINITION
        * ================================= */

        var Typeahead = function (element, options) {
            this.$element = $(element)
            this.options = $.extend({}, $.fn.typeahead.defaults, options)
            this.matcher = this.options.matcher || this.matcher
            this.sorter = this.options.sorter || this.sorter
            this.highlighter = this.options.highlighter || this.highlighter
            this.updater = this.options.updater || this.updater
            this.source = this.options.source
            this.$menu = $(this.options.menu)
            this.shown = false
            this.listen()
        }

        Typeahead.prototype = {

            constructor: Typeahead

        , select: function () {
            var val = this.$menu.find('.active').attr('data-value')
            this.$element
              .val(this.updater(val))
              .change()
            return this.hide()
        }

        , updater: function (item) {
            return item
        }

        , show: function () {
            var pos = $.extend({}, this.$element.position(), {
                height: this.$element[0].offsetHeight
            })

            this.$menu
              .insertAfter(this.$element)
              .css({
                  top: pos.top + pos.height
              , left: pos.left
              })
              .show()

            this.shown = true
            return this
        }

        , hide: function () {
            this.$menu.hide()
            this.shown = false
            return this
        }

        , lookup: function (event) {
            var items

            this.query = this.$element.val()

            if (!this.query || this.query.length < this.options.minLength) {
                return this.shown ? this.hide() : this
            }

            items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source

            return items ? this.process(items) : this
        }

        , process: function (items) {
            var that = this

            items = $.grep(items, function (item) {
                return that.matcher(item)
            })

            items = this.sorter(items)

            if (!items.length) {
                return this.shown ? this.hide() : this
            }

            return this.render(items.slice(0, this.options.items)).show()
        }

        , matcher: function (item) {
            return ~item.toLowerCase().indexOf(this.query.toLowerCase())
        }

        , sorter: function (items) {
            var beginswith = []
              , caseSensitive = []
              , caseInsensitive = []
              , item

            while (item = items.shift()) {
                if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
                else if (~item.indexOf(this.query)) caseSensitive.push(item)
                else caseInsensitive.push(item)
            }

            return beginswith.concat(caseSensitive, caseInsensitive)
        }

        , highlighter: function (item) {
            var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
            return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
                return '<strong>' + match + '</strong>'
            })
        }

        , render: function (items) {
            var that = this

            items = $(items).map(function (i, item) {
                i = $(that.options.item).attr('data-value', item)
                i.find('a').html(that.highlighter(item))
                return i[0]
            })

            items.first().addClass('active')
            this.$menu.html(items)
            return this
        }

        , next: function (event) {
            var active = this.$menu.find('.active').removeClass('active')
              , next = active.next()

            if (!next.length) {
                next = $(this.$menu.find('li')[0])
            }

            next.addClass('active')
        }

        , prev: function (event) {
            var active = this.$menu.find('.active').removeClass('active')
              , prev = active.prev()

            if (!prev.length) {
                prev = this.$menu.find('li').last()
            }

            prev.addClass('active')
        }

        , listen: function () {
            this.$element
              .on('blur', $.proxy(this.blur, this))
              .on('keypress', $.proxy(this.keypress, this))
              .on('keyup', $.proxy(this.keyup, this))

            if (this.eventSupported('keydown')) {
                this.$element.on('keydown', $.proxy(this.keydown, this))
            }

            this.$menu
              .on('click', $.proxy(this.click, this))
              .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
        }

        , eventSupported: function (eventName) {
            var isSupported = eventName in this.$element
            if (!isSupported) {
                this.$element.setAttribute(eventName, 'return;')
                isSupported = typeof this.$element[eventName] === 'function'
            }
            return isSupported
        }

        , move: function (e) {
            if (!this.shown) return

            switch (e.keyCode) {
                case 9: // tab
                case 13: // enter
                case 27: // escape
                    e.preventDefault()
                    break

                case 38: // up arrow
                    e.preventDefault()
                    this.prev()
                    break

                case 40: // down arrow
                    e.preventDefault()
                    this.next()
                    break
            }

            e.stopPropagation()
        }

        , keydown: function (e) {
            this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40, 38, 9, 13, 27])
            this.move(e)
        }

        , keypress: function (e) {
            if (this.suppressKeyPressRepeat) return
            this.move(e)
        }

        , keyup: function (e) {
            switch (e.keyCode) {
                case 40: // down arrow
                case 38: // up arrow
                case 16: // shift
                case 17: // ctrl
                case 18: // alt
                    break

                case 9: // tab
                case 13: // enter
                    if (!this.shown) return
                    this.select()
                    break

                case 27: // escape
                    if (!this.shown) return
                    this.hide()
                    break

                default:
                    this.lookup()
            }

            e.stopPropagation()
            e.preventDefault()
        }

        , blur: function (e) {
            var that = this
            setTimeout(function () { that.hide() }, 150)
        }

        , click: function (e) {
            e.stopPropagation()
            e.preventDefault()
            this.select()
        }

        , mouseenter: function (e) {
            this.$menu.find('.active').removeClass('active')
            $(e.currentTarget).addClass('active')
        }

        }


        /* TYPEAHEAD PLUGIN DEFINITION
        * =========================== */

        var old = $.fn.typeahead

        $.fn.typeahead = function (option) {
            return this.each(function () {
                var $this = $(this)
                  , data = $this.data('typeahead')
                  , options = typeof option == 'object' && option
                if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
                if (typeof option == 'string') data[option]()
            })
        }

        $.fn.typeahead.defaults = {
            source: []
        , items: 8
        , menu: '<ul class="typeahead dropdown-menu"></ul>'
        , item: '<li><a href="#"></a></li>'
        , minLength: 1
        }

        $.fn.typeahead.Constructor = Typeahead


        /* TYPEAHEAD NO CONFLICT
         * =================== */

        $.fn.typeahead.noConflict = function () {
            $.fn.typeahead = old
            return this
        }


        /* TYPEAHEAD DATA-API
         * ================== */

        $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
            var $this = $(this)
            if ($this.data('typeahead')) return
            e.preventDefault()
            $this.typeahead($this.data())
        })

        $element.attr("autocomplete", "off")
				.typeahead({
				    'source': typeaheadArr,
				    'minLength': allBindings.minLength,
				    'items': allBindings.items,
				    'updater': allBindings.updater,
				    'matcher': value.matcher,
				    'highlighter': value.highlighter,
				    'updater': value.updater,
				    'sorter': value.sorter
				});
    }
};

// Bind Twitter Progress
ko.bindingHandlers.progress = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);

        var bar = $('<div/>', {
            'class': 'bar',
            'data-bind': 'style: { width:' + valueAccessor() + ' }'
        });

        $element.attr('id', guid())
			.addClass('progress progress-info')
			.append(bar);

        ko.applyBindingsToDescendants(viewModel, $element[0]);
    }
}

// Bind Twitter Alert
ko.bindingHandlers.alert = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element = $(element);
        var alertInfo = ko.utils.unwrapObservable(valueAccessor());

        var dismissBtn = $('<button/>', {
            'type': 'button',
            'class': 'close',
            'data-dismiss': 'alert'
        }).html('&times;');

        var alertMessage = $('<p/>').html(alertInfo.message);

        $element.addClass('alert alert-' + alertInfo.priority)
    			.append(dismissBtn)
    			.append(alertMessage);
    }
};

// Bind Twitter Tooltip
ko.bindingHandlers.tooltip = {
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var $element, options, tooltip;
        options = ko.utils.unwrapObservable(valueAccessor());
        $element = $(element);
        tooltip = $element.data('tooltip');
        if (tooltip) {
            $.extend(tooltip.options, options);
        } else {
            $element.tooltip(options);
        }
    }
};

// Bind Twitter Popover
ko.bindingHandlers.popover = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // read popover options 
        var popoverBindingValues = ko.utils.unwrapObservable(valueAccessor());

        // set popover title 
        var popoverTitle = popoverBindingValues.title;

        // set popover template id
        var tmplId = popoverBindingValues.template;

        // set popover trigger
        var trigger = 'click';

        if (popoverBindingValues.trigger) {
            trigger = popoverBindingValues.trigger;
        }

        // update triggers
        if (trigger === 'hover') {
            trigger = 'mouseenter mouseleave';
        } else if (trigger === 'focus') {
            trigger = 'focus blur';
        }

        // set popover placement
        var placement = popoverBindingValues.placement;

        // get template html
        var tmplHtml = $('#' + tmplId).html();

        // create unique identifier to bind to
        var uuid = guid();
        var domId = "ko-bs-popover-" + uuid;

        // create correct binding context
        var childBindingContext = bindingContext.createChildContext(viewModel);

        // create DOM object to use for popover content
        var tmplDom = $('<div/>', {
            "class": "ko-popover",
            "id": domId
        }).html(tmplHtml);

        // set content options
        options = {
            content: $(tmplDom[0]).outerHtml(),
            title: popoverTitle
        };

        if (placement) {
            options.placement = placement;
        }

        if (popoverBindingValues.container) {
            options.container = popoverBindingValues.container;
        }

        // Need to copy this, otherwise all the popups end up with the value of the last item
        var popoverOptions = $.extend({}, ko.bindingHandlers.popover.options, options);

        // bind popover to element click
        $(element).bind(trigger, function () {
            var popoverAction = 'show';
            var popoverTriggerEl = $(this);

            // popovers that hover should be toggled on hover
            // not stay there on mouseout
            if (trigger !== 'click') {
                popoverAction = 'toggle';
            }

            // show/toggle popover
            popoverTriggerEl.popover(popoverOptions).popover(popoverAction);

            // hide other popovers and bind knockout to the popover elements
            var popoverInnerEl = $('#' + domId);
            $('.ko-popover').not(popoverInnerEl).parents('.popover').remove();

            // if the popover is visible bind the view model to our dom ID
            if ($('#' + domId).is(':visible')) {

                ko.applyBindingsToDescendants(childBindingContext, $('#' + domId)[0]);

                /* Since bootstrap calculates popover position before template is filled,
                * a smaller popover height is used and it appears moved down relative to the trigger element.
                * So we have to fix the position after the bind
                *  */

                var triggerElementPosition = $(element).offset().top;
                var triggerElementHeight = $(element).outerHeight();

                var popover = $(popoverInnerEl).parents('.popover');
                var popoverHeight = popover.outerHeight();
                var arrowSize = 10;

                switch (popoverOptions.placement) {
                    case 'left':
                    case 'right':
                        popover.offset({ top: triggerElementPosition - popoverHeight / 2 + triggerElementHeight / 2 });
                        break;
                    case 'top':
                        popover.offset({ top: triggerElementPosition - popoverHeight - arrowSize });
                        break;
                    case 'bottom':
                        popover.offset({ top: triggerElementPosition + triggerElementHeight + arrowSize });
                }
            }

            // bind close button to remove popover
            $(document).on('click', '[data-dismiss="popover"]', function (e) {
                popoverTriggerEl.popover('hide');
            });
        });

        // Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
        return { controlsDescendantBindings: true };
    },
    options: {
        placement: "right",
        title: "",
        html: true,
        content: "",
        trigger: "manual"
    }
};
