String.implement({

	translate: function(from, to){
		from = Array.slice(from);
		return this.replace(new RegExp(from.map(String.escapeRegExp).join('|'), 'g'), function(chr){
			return to[from.indexOf(chr)];
		});
	}	

});

var Observer = new Class({
	
	Implements: [Options, Events],
	
	options: {
		interval: 100
	},

	initialize: function(element, property, options){
		this.setOptions(options);
		this.element = $(element);
		this.property = property;
		this.value = this.element.get(property);
		this.start();
	},
	
	check: function() {
		var value = this.element.get(this.property);
		if (value == this.value) return;
		this.value = value;
		this.fireEvent('onChange', [value]);
	},
	
	start: function() {
		this.stop();
		this.interval = this.check.periodical(this.options.interval, this);
	},
	
	stop: function() {
		$clear(this.interval);
	}
	
});

var InputMirror = new Class({
	
	Implements: Options,
	
	options: {
		// Property to which the output is written.
		property: 'value',
		map: $arguments(0),
		interval: 100
	},

	initialize: function(input, output, options){
		this.setOptions(options);
		this.input = $(input);
		this.output = $(output);
		this.setValues();
		this.check.periodical(this.options.interval, this);
	},
	
	setValues: function() {
		this.values = {
			input: this.input.get('value'),
			output: this.output.get(this.options.property)
		};
	},
	
	check: function() {
		var inputValue = this.input.get('value');
		if (inputValue == this.values.input || this.output.get(this.options.property) != this.values.output) return;
		
		this.output.set(this.options.property, this.options.map(inputValue));
		this.setValues();
	}
	
});

var InputMatcher = new Class({

	initialize: function(input, matcher){
		this.input = $(input);
		this.value = this.input.get('value');
		this.matcher = RegExp.type(matcher) ? function(string, old) {
			return string.match(matcher) ? false : old;
		} : matcher;
		this.action.periodical(100, this);
	},
	
	action: function() {
		var value = this.input.get('value');
		if (this.value == value) return;
		var filtered = this.matcher(value, this.value);
		if (filtered !== false) {
			this.input.set('value', filtered);
			this.value = filtered;
		} else this.value = value;
	}
	
});

var InputTags = new Class({

	initialize: function(input){
		this.input = $(input).addClass('input');
		this.wrapper = new Element('div', {'class': 'tags'}).wraps(this.input);
		new Observer(this.input, 'value', {
			onChange: (function(value){
				var newValue = value.replace(/\b\S+(?=\s)/g, this.makeTag.bind(this));
				if(value != newValue) this.input.set('value', newValue.clean());
			}).bind(this)
		});
		this.resizeInput();
	},
	
	makeTag: function(string) {
		var tag = new Element('div', {
			'class': 'tag'
		}).inject(this.input, 'before').adopt(
			new Element('div', {text: string})
		);
		var self = this;
		var tween = new Fx.Tween(tag, 'background-color', {link: 'cancel', duration: 300}).set('#3a3');
		var hideEffect = new Fx.Morph(tag, {
			onComplete: function(){
				tag.destroy();
				self.resizeInput();
			},
			duration: 500,
			transition: 'sine:in'
		});
		tag.addEvents({
			'click': function(){
				hideEffect.start({
					opacity: 0,
					'margin-top': 50
				});
			},

			mouseenter: function(){
				tween.start('#f33');
			},

			mouseleave: function(){
				tween.start('#3a3');
			}
		});
		this.resizeInput();
		return '';
	},
	
	resizeInput: function(){
		var width = 300;
		this.wrapper.getChildren('div').each(function(el){
			width -= el.getOffsetSize().x + el.getStyle('margin-right').toInt();
		});
		this.input.setStyle('width', width);
	}

});