
Date.customDateFormatRegex = /^(3[0-1]|0[1-9]|[1-2][0-9])\/(1[0-2]|0[1-9])\/([0-9]{4})$/;

/**
 * new Native Interface
 * Helps make sure a set of functions is implemented onto the target class
 *
 * @package    extensions
 * @author     Luis Merino <mail@luismerino.name>
 */

Element.Events.outerClick = {

	base: 'click',

	condition: function(e){
		e.stopPropagation();
		return false;
	},

	onAdd: function(fn){
		this.getDocument().addEvent('click', fn);
	},

	onRemove: function(fn){
		this.getDocument().removeEvent('click', fn);
	}

};

Element.Events.enter = {

	base: 'keydown',

	condition: function(e){
		return (e.key != 'enter') ? false : true;
	}

};

Element.Events.esc = {

	base: 'keydown',

	condition: function(e){
		return (e.key != 'esc') ? false : true;
	}

};

Element.Events.tab = {

	base: (Browser.Engine.gecko) ? 'keypress' : 'keydown',

	condition: function(e){
		return (e.key != 'tab') ? false : true;
	}

};

Element.Events.keydatehandler = {

	base: (Browser.Engine.gecko) ? 'keypress' : 'keydown',
	
	condition: function(e){
		return ['left','right','up','down'].some(function(key){ return e.key == key; });
	}
	
};

Element.Events.keynumber = {
	
	base: (Browser.Engine.gecko) ? 'keypress' : 'keydown',
	
	condition: function(e){
		if (e.key != 'tab' || e.alt) e.preventDefault();
		return (e.code > 47 && e.code < 60) ? true : false;
	}
	
};

Element.implement({
	
	hasProperty: function(attribute){
		return this.hasAttribute(attribute) || this.removeAttribute(attribute) ? true : false;
	},
	
	toQueryJSON: function(){
		var queryJSON = new Hash();
		this.getElements('input, select, textarea', true).each(function(el){
			if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file') return;
			var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
				return opt.value;
			}) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
			$splat(value).each(function(val){
				if (typeof val != 'undefined') queryJSON.include(el.name, val);
			});
		});
		return queryJSON.toJSON();
	}
	
});

function $E(selector){
	return document.getElement(selector);
};

function $F(element){
	return element.get('value') || element.get('text');
};

function $itself(){
	return function(){
		return this;
	}
};

this.nil = function(item){
	return (item != null && item != nil) ? item : null;
};

Object.extend({
	subset: function(object, keys, nuke){
		var results = {};
		for (var i = 0, l = keys.length; i < l; i++){
			var k = keys[i], value = object[k];
			results[k] = nil(value);
			if (nuke) delete object[k];
		}
		return results;
	},
	equals: function(obj){
		return (this == obj || JSON.encode(this) == JSON.encode(obj));
	}
});

Function.prototype.append = function(){
	for (var i = 0, l = arguments.length; i < l; i++) {
		$extend(this.prototype, arguments[i]);
	}
};

Function.prototype.overloadSetter = function(){
	var self = this;
	return function(a, b){
		if (typeof a != 'string'){
			for (var k in a) self.call(this, k, a[k]);
		} else {
			self.call(this, a, b);
		}
		return this;
	};
};
 
Function.prototype.overloadGetter = function(){
	var self = this;
	return function(a){
		var args, result;
		if (typeof a != 'string') args = a;
		else if (arguments.length > 1) args = arguments;
		if (args){
			result = {};
			for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]);
		} else {
			result = self.call(this, a);
		}
		return result;
	};
};

this.Storage = function(){

	var storage = {};

	this.store = function(property, value){
		storage[property] = value;
		return this;
	}.overloadSetter();
	
	this.retrieve = function(property, dflt){
		var prop = storage[property];
		if (dflt != undefined && prop == undefined) prop = storage[property] = dflt;
		return $pick(prop);
	}.overloadGetter();

	this.serialize = function(){
		return Object.subset(storage, arguments, true);
	}.overloadGetter();
	
	this.empty = function(){
		storage = {};
	};

	return this;

};

/*
---
Script: String.toDOM.js
Description: Convert a string into DOM nodes

Author: Yannick Croissant
...
*/

String.implement({
	
	toDOM: function(){
		var wrapper =	this.test('^<the|^<tf|^<tb|^<colg|^<ca') && ['<table>', '</table>', 1] ||
						this.test('^<col') && ['<table><colgroup>', '</colgroup><tbody></tbody></table>',2] ||
						this.test('^<tr') && ['<table><tbody>', '</tbody></table>', 2] ||
						this.test('^<th|^<td') && ['<table><tbody><tr>', '</tr></tbody></table>', 3] ||
						this.test('^<li') && ['<ul>', '</ul>', 1] ||
						this.test('^<dt|^<dd') && ['<dl>', '</dl>', 1] ||
						this.test('^<le') && ['<fieldset>', '</fieldset>', 1] ||
						this.test('^<opt') && ['<select multiple="multiple">', '</select>', 1] ||
						['', '', 0];
		var el = new Element('div', {html: wrapper[0] + this + wrapper[1]}).getChildren();
		while(wrapper[2]--) el = el[0].getChildren();
		return el;
	}

});

/*
---
Script: Number.zeroise.js, Number.boundize.js, String.repeat.js, String.zeroise.js, String.boundize.js

Author: Luis Merino
...
*/

String.implement({
	'repeat': function(times){
		return new Array(times + 1).join(this);
	},
	'pad': function(length, character, direction){
		if (this.length >= length) return this;

		var result = this,
		direction = (direction || 'center'),
		until = length - this.length,
		character = (character.toString() || ' '),            
		additional = character.repeat(until).substr(0, until);

		switch (direction.toLowerCase())
		{
			case 'left':

			result = additional + result;
			break;

			case 'right':

			result = result + additional;
			break;

			default:

			var length = additional.length,
			left = Math.floor(length / 2),
			right = Math.ceil(length / 2);

			result = additional.substr(0, left) + this + additional.substr(0, right);
			break;
		}

		return result;
	},
	zeroise: function(length) {
		return '0'.repeat(length - this.length) + this;
	},
	times: function(fn, bind){
		Number(this).times(fn, bind);
	},
	toAsciiChars: function(){
		var chars = ['a','e','i','o','u','a','e','i','o','u','A','E','I','O','U','A','E','I','O','U','a','e','i','o','u','A','E','I','O','U'];
		var equiv = [/*lower*/'\u00E1','\u00E9','\u00ED','\u00F3','\u00FA','\u00E0','\u00E8','\u00EC','\u00F2','\u00F9',/*upper*/'\u00C1','\u00C9','\u00CD','\u00D3','\u00DA','\u00C0','\u00C8','\u00CC','\u00D2','\u00D9',/*umlauts*/'\u00E4','\u00EB','\u00EF','\u00F6','\u00FC','\u00C4','\u00CB','\u00CF','\u00D6','\u00DC'];

		return this.replace(/[\x80-\xff]/, function($1/*match*/, $2/*position*/, $3/*string*/){
			n = equiv.indexOf($1);
			return chars[n] || $1;
		});
	},
	encodeUTF8: function(){
	  return unescape(encodeURIComponent(this));
	},
	decodeUTF8: function(){
	  return decodeURIComponent(escape(this));
	}
});

Number.implement({
	zeroise: function(length) {
		return String(this).zeroise(length);
	},
	between: function(limits, include) {
		if (include) {
			a = limits[0] - 1;
			b = limits[1] + 1;
		}
		return (this > a && this < b) ? true : false;
	},
	percentOf: function(n) {
		return (n / 100) * this;
	}
});

Array.implement({
	call: function(name){
		var args = Array.slice(arguments, 1), results = [];
		for (var i = 0, j = this.length; i < j; i++){
			var item = this[i];
			results.push(item[name].apply(item, args));
		}
		return results;
	}
});
Native.genericize(Array, 'call', false);

/*
---
Script: Number.fib.js
Description: Fibonacci number implementation

Author: Luis Merino
...
*/

Number.implement('fib', function(){
	var n = Number(this);
	return n < 2 ? n : (n-1).fib() + (n-2).fib();
});

String.implement({
	customDateToTime: function(){
		var date = this.match(Date.customDateFormatRegex);
		return (date) ? +new Date(date[3], --date[2], date[1]) : null;
	}
});

/*
Script: Fx.Tween.js
	Formerly Fx.Style, effect to transition any CSS property for an element.
 
License:
	MIT-style license.
*/
 
Fx.Shake = new Class({
 
	Extends: Fx.Tween,
 
	options: {
		times: 5
	},
 
	initialize: function(){
		this.parent.apply(this, arguments);
		if (this.options.property) {
			this.property = this.options.property;
			delete this.options.property;
		}
		this._start = this.start;
		this.start = this.shake;
		this.duration = this.options.duration;
		this.shakeChain = new Chain();
	},
 
	complete: function(){
		if (!this.shakeChain.$chain.length && this.stopTimer()) this.onComplete();
		else if (this.shakeChain.$chain.length) this.shakeChain.callChain();
		return this;
	},
 
	shake: function(property, distance, times){
		if (!this.check(arguments.callee, property, distance)) return this;
		var args = Array.link(arguments, {property: String.type, distance: $defined, times: $defined});
		property = this.property || args.property;
		times = args.times || this.options.times;
		this.stepDur = this.duration / (times + 1);
		this._getTransition = this.getTransition;
		this.origin = this.element.getStyle(property).toInt()||0;
		this.shakeChain.chain(
			function(){
				this.shakeChain.chain(
					function() {
							this.stopTimer();
							this.getTransition = this._getTransition;
							this.options.duration = this.stepDur / 2;
							//stage three, return to origin
							this._start(property, this.origin);
					}.bind(this)
				);
				this.getTransition = function(){
					return function(p){
						return (1 + Math.sin( times*p*Math.PI - Math.PI/2 )) / 2;
					}.bind(this);
				}.bind(this);
				this.stopTimer();
				this.options.duration = this.stepDur * times;
				//stage 2: offset to the other side using the shake transition
				this._start(property, this.origin - args.distance);
			}.bind(this)
		);
		this.options.duration = this.stepDur / 2;
		//stage 1: offset to one side
		return this._start(property, this.origin + args.distance);
	},
 
	onCancel: function(){
		this.parent();
		this.shakeChain.clearChain();
		return this;
	}
 
});
 
Element.Properties.shake = {
 
	set: function(options){
		var shake = this.retrieve('shake');
		if (shake) shake.cancel();
		return this.eliminate('shake').store('shake:options', $extend({link: 'cancel'}, options));
	},
 
	get: function(options){
		if (options || !this.retrieve('shake')){
			if (options || !this.retrieve('shake:options')) this.set('shake', options);
			this.store('shake', new Fx.Shake(this, this.retrieve('shake:options')));
		}
		return this.retrieve('shake');
	}
 
};
 
Element.implement({
 
	shake: function(property, distance, times, options){
		var args = Array.link(arguments, {property: String.type, distance: Number.type, times: Number.type, options: Object.type});
		if (args.options) this.set('shake', args.options);
		this.get('shake').start(args.property, args.distance, args.times);
		return this;
	}
 
});

/*
---
Script: MethodsInterface.js
Description: 'Class' Interface: Helps make sure a set of methods namespaces are implemented

License: MIT-style

Author:
- Luis Merino

Copyright:
- Luis Merino

Dependencies:
- MooTools-Core
...
*/

var MethodsInterface = new Class({
		
	initialize: function(name, methods){
		if (arguments.length != 2){
			throw new Error("Interface constructor called with " + arguments.length + " arguments, but expected exactly 2.");
		}
		this.name = name;
		this.methods = [];
		for (var i=0, len = methods.length; i < len; i++){
			if (typeof methods[i] !== 'string'){
				throw new Error("Interface constructor expects method names to be passed in as a string.");
			}
			this.methods.push(methods[i]);
		}
	}
	
});

MethodsInterface.ensureImplements = function(object){
	if (arguments.length < 1){
		throw new Error("Function Interface.ensureImplements called with " + arguments.length + " arguments, but expected at least 1.")
	}
	for (var i=1, len = arguments.length; i < len; i++){
		var iface = arguments[i];
		if (iface.constructor !== MethodsInterface){
			throw new Error("Function Interface.ensureImplements expects arguments two and above to be instances of Interface.");
		}
		for (var j=0, methodsLen = iface.methods.length; j < methodsLen; j++){
			var method = iface.methods[j];
			if (!object[method] || typeof object[method] !== 'function'){
				throw new Error("Function Interface.ensureImplements: object does not implement the " + iface.name + " interface. Method " + method + " was not found.");
			}
		}
	}
};

/*
---
Script: Interface.js
Description: 'Native' Interface: Helps make sure a set of key/value object is implemented onto the target class or object

License: MIT-style

Author:
- Luis Merino

Copyright:
- Luis Merino

Dependencies:
- MooTools-Core
...
*/

var Interface = new Native({
	
	name: 'Interface',
	
	protect: true,
	
	initialize: function(object){
		if (!object || $type(object) != 'object'){
			throw new Error("Interface constructor called with `" + arguments.length + "` arguments, but expected at least 1 to be an object with the implementation.");
		}
		var _implements = $unlink(object);
		for (var namespace in _implements){
			if ($type(_implements[namespace]) == undefined || $type(_implements[namespace]) == null){
				throw new Error("Interface constructor expects key namespaces to be passed in as a valid native type.");
			}
			if (_implements[namespace].prototype && _implements[namespace].prototype.$family){
				_implements[namespace] = _implements[namespace].prototype.$family.name;
			} else {
				_implements[namespace] = $type(_implements[namespace]);
			}
		}
		this.__implements = _implements;
		return this;
	}
	
});

(function(){
	// 'Loose' Type to allow checking against namespaces only for the key.
	var $Loose = this.$Loose = (function(){
		var F = Function;
		F.prototype.constructor = F.prototype = null;
		return new F;
	})();
	// Make Loose a Native type.
	new Native({name: 'Loose', initialize: $Loose, protect: true});
	
})();

Class.Mutators.Interfaces = function(items){
	var _implements = null;
	$splat(items).each(function(name){
		_implements = $merge(name.__implements, {});
	});
	return _implements;
};

Class.Mutators.initialize = function(initialize){
	var klass = this;
	return function(){
		$H(this.Interfaces).each(function(type, name){
			if (this[name] == undefined) throw new Error("Class does not implement the `" + name + "` namespace.");
			if (type === 'loose') return;
			if (type !== $type(this[name]))	throw new TypeError("Native type `" + type + "` does not match `" + name + "` namespace.");
		}, klass.prototype);
		$splat(this.Binds).each(function(name){
			var original = this[name];
			if (original) this[name] = original.bind(this);
		}, this);
		return initialize.apply(this, arguments);
	};
};

// NEEDS REVISION!
Hash.implement({
	
	keyOf: function(value){
		for (var key in this){
			if (this.hasOwnProperty(key)) {
				if ($type(value) == 'array') {
					if (Array.flatten(value).every(function(el, i){ return el === this[i]; }, this[key]))
						return key;
				}
			 	if (this[key] === value) return key;
			}
		}
		return null;
	}
	
});

/*
---
description: provides the Element.Events.addKeySequence function to create custom events for sequences of key presses.

license: MIT-style

authors:
- Ben Lenarts

requires:
  core/1.2.4: '*'
# actually:
# - core/1.2.4: Element.Event

provides: [Element.Events.addKeySequence]

...
*/

(function() {

function keycombo(event) {
  var mods = '';
  if (event.shift) mods += 'shift-';
  if (event.control) mods += 'control-';
  if (event.alt) mods += 'alt-';
  if (event.meta) mods += 'meta-';
  return mods + event.key;
}

Element.Events.addKeySequence = function(name, sequence, options) {
  options = options || {};
  var withModifiers = options.withModifiers !== false;

  var buffer = new Array(sequence.length);
  var target = sequence.toString();

  Element.Events[name] = {
    base: 'keyup',
    condition: function(event) {
      if (!event.key || event.key.charCodeAt(0) < 32) return false;
      buffer.shift();
      buffer.push(withModifiers ? keycombo(event) : event.key);
      return buffer.toString() == target;
    }
  }
}

})();