/*
 Script: TextboxList.js
 Displays a textbox as a combination of boxes an inputs (eg: facebook tokenizer)

 Authors:
 Guillermo Rauch

 Note:
 TextboxList is not priceless for commercial use. See <http://devthought.com/projects/jquery/textboxlist/>.
 Purchase to remove this message.
 */

jQuery(function ($) {

	$.TextboxList = function (element, _options) {

		var original, container, list, current, focused = false, index = [], blurtimer, events = {};
		var options = $.extend(true, {
			prefix: 'textboxlist',
			max: null,
			unique: false,
			uniqueInsensitive: true,
			endEditableBit: true,
			startEditableBit: true,
			hideEditableBits: true,
			inBetweenEditableBits: true,
			keys: {previous: 37, next: 39},
			bitsOptions: {editable: {}, box: {}},
			plugins: {},
			// tip: you can change encode/decode with JSON.stringify and JSON.parse
			encode: function (o) {
				return $.grep($.map(o, function (v) {
					v = (chk(v[0]) ? v[0] : v[1]);
					return chk(v) ? v.toString().replace(/,/, '') : null;
				}),function (o) {
					return o != undefined;
				}).join(',');
			},
			decode: function (o) {
				return o.split(',');
			}
		}, _options);

		element = $(element);

		var self = this;
		var init = function () {
			original = element.css('display', 'none').attr('autocomplete', 'off').focus(focusLast);
			container = $('<div class="' + options.prefix + '" />')
				.insertAfter(element)
				.click(function (e) {
					if ((e.target == list.get(0) || e.target == container.get(0)) && (!focused || (current && current.toElement().get(0) != list.find(':last-child').get(0)))) focusLast();
				});
			list = $('<ul class="' + options.prefix + '-bits" />').appendTo(container);
			for (var name in options.plugins) enablePlugin(name, options.plugins[name]);
			afterInit();
		};

		var enablePlugin = function (name, options) {
			self.plugins[name] = new $.TextboxList[camelCase(capitalize(name))](self, options);
		};

		var afterInit = function () {
			if (options.endEditableBit) create('editable', null, {tabIndex: original.tabIndex}).inject(list);
			addEvent('bitAdd', update, true);
			addEvent('bitRemove', update, true);
			$(document).click(function (e) {
				if (!focused) return;
				if (e.target.className.indexOf(options.prefix) != -1) {
					if (e.target == $(container).get(0)) return;
					var parent = $(e.target).parents('div.' + options.prefix);
					if (parent.get(0) == container.get(0)) return;
				}
				blur();
			}).keydown(function (ev) {
					if (!focused || !current) return;
					var caret = current.is('editable') ? current.getCaret() : null;
					var value = current.getValue()[1];
					var special = !!$.map(['shift', 'alt', 'meta', 'ctrl'],function (e) {
						return ev[e];
					}).length;
					var custom = special || (current.is('editable') && current.isSelected());
					var evStop = function () {
						ev.stopPropagation();
						ev.preventDefault();
					};
					switch (ev.which) {
						case 8:
							if (current.is('box')) {
								evStop();
								return current.remove();
							}
						case options.keys.previous:
							if (current.is('box') || ((caret == 0 || !value.length) && !custom)) {
								evStop();
								focusRelative('prev');
							}
							break;
						case 46:
							if (current.is('box')) {
								evStop();
								return current.remove();
							}
						case options.keys.next:
							if (current.is('box') || (caret == value.length && !custom)) {
								evStop();
								focusRelative('next');
							}
					}
				});
			setValues(options.decode(original.val()));
		};

		var create = function (klass, value, opt) {
			if (klass == 'box') {
				if (chk(options.max) && list.children('.' + options.prefix + '-bit-box').length + 1 > options.max) return false;
				if (options.unique && $.inArray(uniqueValue(value), index) != -1) return false;
			}
			return new $.TextboxListBit(klass, value, self, $.extend(true, options.bitsOptions[klass], opt));
		};

		var uniqueValue = function (value) {
			return chk(value[0]) ? value[0] : (options.uniqueInsensitive ? value[1].toLowerCase() : value[1]);
		}

		var add = function (plain, id, html, afterEl) {
			var b = create('box', [id, plain, html]);
			if (b) {
				if (!afterEl || !afterEl.length) afterEl = list.find('.' + options.prefix + '-bit-box').filter(':last');
				b.inject(afterEl.length ? afterEl : list, afterEl.length ? 'after' : 'top');
			}
			return self;
		};

		var focusRelative = function (dir, to) {
			var el = getBit(to && $(to).length ? to : current).toElement();
			var b = getBit(el[dir]());
			if (b) b.focus();
			return self;
		};

		var focusLast = function () {
			var lastElement = list.children().filter(':last');
			if (lastElement) getBit(lastElement).focus();
			return self;
		};

		var blur = function () {
			if (!focused) return self;
			if (current) current.blur();
			focused = false;
			return fireEvent('blur');
		};

		var getBit = function (obj) {
			return (obj.type && (obj.type == 'editable' || obj.type == 'box')) ? obj : $(obj).data('textboxlist:bit');
		};

		var getValues = function () {
			var values = [];
			list.children().each(function () {
				var bit = getBit(this);
				if (!bit.is('editable')) values.push(bit.getValue());
			});
			return values;
		};

		var setValues = function (values) {
			if (!values) return;
			$.each(values, function (i, v) {
				if (v) add.apply(self, $.isArray(v) ? [v[1], v[0], v[2]] : [v]);
			});
		};

		var update = function () {
			original.val(options.encode(getValues()));
		};

		var addEvent = function (type, fn) {
			if (events[type] == undefined) events[type] = [];
			var exists = false;
			$.each(events[type], function (f) {
				if (f === fn) {
					exists = true;
					return;
				}
				;
			});
			if (!exists) events[type].push(fn);
			return self;
		};

		var fireEvent = function (type, args, delay) {
			if (!events || !events[type]) return self;
			$.each(events[type], function (i, fn) {
				(function () {
					args = (args != undefined) ? splat(args) : Array.prototype.slice.call(arguments);
					var returns = function () {
						return fn.apply(self || null, args);
					};
					if (delay) return setTimeout(returns, delay);
					return returns();
				})();
			});
			return self;
		};

		var removeEvent = function (type, fn) {
			if (events[type]) {
				for (var i = events[type].length; i--; i) {
					if (events[type][i] === fn) events[type].splice(i, 1);
				}
			}
			return self;
		};

		var isDuplicate = function (v) {
			return $.inArray(uniqueValue(v), index);
		};

		this.onFocus = function (bit) {
			if (current) current.blur();
			clearTimeout(blurtimer);
			current = bit;
			container.addClass(options.prefix + '-focus');
			if (!focused) {
				focused = true;
				fireEvent('focus', bit);
			}
		};

		this.onAdd = function (bit) {
			if (options.unique && bit.is('box')) index.push(uniqueValue(bit.getValue()));
			if (bit.is('box')) {
				var prior = getBit(bit.toElement().prev());
				if ((prior && prior.is('box') && options.inBetweenEditableBits) || (!prior && options.startEditableBit)) {
					var priorEl = prior && prior.toElement().length ? prior.toElement() : false;
					var b = create('editable').inject(priorEl || list, priorEl ? 'after' : 'top');
					if (options.hideEditableBits) b.hide();
				}
			}
		};

		this.onRemove = function (bit) {
			if (!focused) return;
			if (options.unique && bit.is('box')) {
				var i = isDuplicate(bit.getValue());
				if (i != -1) index = index.splice(i + 1, 1);
			}
			var prior = getBit(bit.toElement().prev());
			if (prior && prior.is('editable')) prior.remove();
			focusRelative('next', bit);
		};

		this.onBlur = function (bit, all) {
			current = null;
			container.removeClass(options.prefix + '-focus');
			blurtimer = setTimeout(blur, all ? 0 : 200);
		};

		this.setOptions = function (opt) {
			options = $.extend(true, options, opt);
		};

		this.getOptions = function () {
			return options;
		};

		this.getContainer = function () {
			return container;
		};

		this.isDuplicate = isDuplicate;
		this.addEvent = addEvent;
		this.removeEvent = removeEvent;
		this.fireEvent = fireEvent;
		this.create = create;
		this.add = add;
		this.getValues = getValues;
		this.plugins = [];
		init();
	};

	$.TextboxListBit = function (type, value, textboxlist, _options) {

		var element, bit, prefix, typeprefix, close, hidden, focused = false, name = capitalize(type);
		var options = $.extend(true, type == 'box' ? {
			deleteButton: true
		} : {
			tabIndex: null,
			growing: true,
			growingOptions: {},
			stopEnter: true,
			addOnBlur: false,
			addKeys: [13]
		}, _options);

		this.type = type;
		this.value = value;

		var self = this;
		var init = function () {
			prefix = textboxlist.getOptions().prefix + '-bit';
			typeprefix = prefix + '-' + type;
			bit = $('<li />').addClass(prefix).addClass(typeprefix)
				.data('textboxlist:bit', self)
				.hover(function () {
					bit.addClass(prefix + '-hover').addClass(typeprefix + '-hover');
				}, function () {
					bit.removeClass(prefix + '-hover').removeClass(typeprefix + '-hover');
				});
			if (type == 'box') {
				bit.html(chk(self.value[2]) ? self.value[2] : self.value[1]).click(focus);
				if (options.deleteButton) {
					bit.addClass(typeprefix + '-deletable');
					close = $('<a href="#" class="' + typeprefix + '-deletebutton" />').click(remove).appendTo(bit);
				}
				bit.children().click(function (e) {
					e.stopPropagation();
					e.preventDefault();
				});
			} else {
				element = $('<input type="text" class="' + typeprefix + '-input" autocomplete="off" />').val(self.value ? self.value[1] : '').appendTo(bit);
				if (chk(options.tabIndex)) element.tabIndex = options.tabIndex;
				if (options.growing) new $.GrowingInput(element, options.growingOptions);
				element.focus(function () {
					focus(true);
				}).blur(function () {
						blur(true);
						if (options.addOnBlur) toBox();
					});
				if (options.addKeys || options.stopEnter) {
					element.keydown(function (ev) {
						if (!focused) return;
						var evStop = function () {
							ev.stopPropagation();
							ev.preventDefault();
						};
						if (options.stopEnter && ev.which === 13) evStop();
						if ($.inArray(ev.which, splat(options.addKeys)) != -1) {
							evStop();
							toBox();
						}
					});
				}
			}
		};

		var inject = function (el, where) {
			switch (where || 'bottom') {
				case 'top':
					bit.prependTo(el);
					break;
				case 'bottom':
					bit.appendTo(el);
					break;
				case 'before':
					bit.insertBefore(el);
					break;
				case 'after':
					bit.insertAfter(el);
					break;
			}
			textboxlist.onAdd(self);
			return fireBitEvent('add');
		};

		var focus = function (noReal) {
			if (focused) return self;
			show();
			focused = true;
			textboxlist.onFocus(self);
			bit.addClass(prefix + '-focus').addClass(prefix + '-' + type + '-focus');
			fireBitEvent('focus');
			if (type == 'editable' && !noReal) element.focus();
			return self;
		};

		var blur = function (noReal) {
			if (!focused) return self;
			focused = false;
			textboxlist.onBlur(self);
			bit.removeClass(prefix + '-focus').removeClass(prefix + '-' + type + '-focus');
			fireBitEvent('blur');
			if (type == 'editable') {
				if (!noReal) element.blur();
				if (hidden && !element.val().length) hide();
			}
			return self;
		};

		var remove = function () {
			blur();
			textboxlist.onRemove(self);
			bit.remove();
			return fireBitEvent('remove');
		};

		var show = function () {
			bit.css('display', 'block');
			return self;
		};

		var hide = function () {
			bit.css('display', 'none');
			hidden = true;
			return self;
		};

		var fireBitEvent = function (type) {
			type = capitalize(type);
			textboxlist.fireEvent('bit' + type, self).fireEvent('bit' + name + type, self);
			return self;
		};

		this.is = function (t) {
			return type == t;
		};

		this.setValue = function (v) {
			if (type == 'editable') {
				element.val(chk(v[0]) ? v[0] : v[1]);
				if (options.growing) element.data('growing').resize();
			} else value = v;
			return self;
		};

		this.getValue = function () {
			return type == 'editable' ? [null, element.val(), null] : value;
		};

		if (type == 'editable') {
			this.getCaret = function () {
				var el = element.get(0);
				if (el.createTextRange) {
					var r = document.selection.createRange().duplicate();
					r.moveEnd('character', el.value.length);
					if (r.text === '') return el.value.length;
					return el.value.lastIndexOf(r.text);
				} else return el.selectionStart;
			};

			this.getCaretEnd = function () {
				var el = element.get(0);
				if (el.createTextRange) {
					var r = document.selection.createRange().duplicate();
					r.moveStart('character', -el.value.length);
					return r.text.length;
				} else return el.selectionEnd;
			};

			this.isSelected = function () {
				return focused && (self.getCaret() !== self.getCaretEnd());
			};

			var toBox = function () {
				var value = self.getValue();
				var b = textboxlist.create('box', value);
				if (b) {
					b.inject(bit, 'before');
					self.setValue([null, '', null]);
					return b;
				}
				return null;
			};

			this.toBox = toBox;
		}

		this.toElement = function () {
			return bit;
		};

		this.focus = focus;
		this.blur = blur;
		this.remove = remove;
		this.inject = inject;
		this.show = show;
		this.hide = hide;
		this.fireBitEvent = fireBitEvent;
		init();
	};

	var chk = function (v) {
		return !!(v || v === 0);
	};
	var splat = function (a) {
		return $.isArray(a) ? a : [a];
	};
	var camelCase = function (str) {
		return str.replace(/-\D/g, function (match) {
			return match.charAt(1).toUpperCase();
		});
	};
	var capitalize = function (str) {
		return str.replace(/\b[a-z]/g, function (A) {
			return A.toUpperCase();
		});
	};

	$.fn.extend({

		textboxlist: function (options) {
			return this.each(function () {
				new $.TextboxList(this, options);
			});
		}

	});

});