/**
 * NS Interface elements
 * Contains specific classes that interact with the interface.
 *
 * @version   1.00.100616
 * @author    LBI Lost Boys
 */
NS(function($){

	var CLASS_ACTIVE = 'active';
	var CLASS_ERROR  = 'error';

	var PRIORITY_LOW    = 1;
	var PRIORITY_HIGH   = 3;

	var ATTR_REQUIRED = 'ns:required';
	var ATTR_COMPLETE = 'ns:complete';
	var ATTR_PATTERN  = 'ns:pattern';
	var ATTR_VALIDATE = 'ns:validate';

	var TYPE_TEXT   = 'text';
	var TYPE_PASS	= 'password';
	var TYPE_AREA   = 'textarea';
	var TYPE_SELECT = 'select-one';
	var TYPE_MULTI  = 'select-multiple';
	var TYPE_CHECK  = 'checkbox';
	var TYPE_RADIO  = 'radio';

	var PREF_VALIDATE_REQUIRED = 'validateRequired';
	var PREF_VALIDATE_SERVER = 'validateServer';

	var REG_WHITE = /^\s+$/;

	/**
	 * Interface namespace
	 */
	NS.Interface = {
		initialize: function() {

			// create custom click events on links with an existing hash
			// the event is only fired if the hash target is found on the current page
			NS.Dispatcher.createEvent('click:link:hash', function(e){
				var link = e.target;
				var hash = link.hash;
				var target = hash && $(hash)[0];
				if(target) {
					// store the target on the event
					e.setProperty('hashTarget', target);
					return link;
				}
				return null;
			});

			// static interface elements
			this.briefings		 = new Briefings();
			this.clickAndShow	 = new ClickAndShow();
			this.smoothScrolling = new SmoothScrolling();
			this.highlighter	 = new Highlighter();
			this.validator		 = new Validator();
			this.readspeaker	 = new Readspeaker();
			this.compatibility	 = new BrowserCompatibility();
			this.layoutMonitor   = new LayoutMonitor();
			this.drawers		 = new Drawers();
			//this.credentials	 = new Credentials();
			
			// ajax enabled interface elements
			this.remembered		= NS.AjaxWrapper(new RememberedInputs());
			this.external		= NS.AjaxWrapper(new ExternalLinks());
			this.modifications	= NS.AjaxWrapper(new DOMModifications());
			this.hints			= NS.AjaxWrapper(new InputHints());
			this.cardNumbers    = NS.AjaxWrapper(new CardNumbers());
		},

		displayError:function(input, toggle){
			var form = input.form;
			var error = this.getErrorElement(form);
			var origin = input.parentNode;
			var reg = /div|li/i;
			var $input = $(input);
			
			// find the closest field div
			var $targets = $input.closest('[class^=field]');

			// and also, any other closest div or li 
			while(origin && !reg.test(origin.nodeName)) {
				origin = origin.parentNode;
			}

			$targets = $(origin).add($targets);
			$targets.toggleClass(CLASS_ERROR, toggle);
			$input.toggleClass(CLASS_ERROR, toggle);
			
			if(toggle) {
				var lang = NS.getLanguage();
				var required = NS.getProperty(
					/nl/i.test(lang)? 'MSG_REQUIRED' : 'MSG_REQUIRED_EN'
				);
				error.html(required);
			}
		},
		
		displayErrorMessage:function(messages, form){
			var error = this.getErrorElement(form);
			if(messages) {
				error.html(messages); // .show() removed
			} else {
				error.html(''); // .hide() removed
			}
		},
		
		getErrorElement:function(form){
			return $('p.error', form).eq(0);
		},

		createIEFrame:function() {
			if(!/msie (5|6)/i.test(navigator.userAgent)) {
				return false;
			}

			var frame = $('iframe.cover-frm');
			if(frame.length === 0) {
				frame = $('<iframe src="/ns2009/static/blank.html" frameborder="0" class="cover-frm"></iframe>');
				$('#canvas').append(frame);
			}

			return frame;
		},

		/**
		 * "facades"
		 */
		scrollTo:function(target) {
			this.smoothScrolling.scrollTo(target);
		},

		highlight: function(target) {
			this.highlighter.highlight(target);
		}
	};

	/**
	 * DOMModifications adds or alters specific markup for styling purposes.
	 */
	function DOMModifications(){
		var br = $.browser;
		this.ielte8 = br.msie && (parseInt(br.version, 10) <= 8);
		this.ie9 = br.msie && (parseInt(br.version, 10) == 9);
		this.opera = br.opera;
		this.rounder = new NS.RoundedImages();
		
		this.parseNode(document);
	}
	
	DOMModifications.prototype = {
		parseNode:function(root) {
			var jRoot = $(root);
			this.stripeTables(jRoot);
			this.makeUnpastable(jRoot);
			this.roundImages(jRoot);
			
			if(this.ielte8) {
				this.roundCorners(jRoot);
				this.vmlifyCorners(jRoot);
				this.roundNavigation(jRoot);
				this.splitLists(jRoot);
			}

			if(this.opera || this.ie9) {
				this.splitLists(jRoot);
			}
		},

		roundCorners: function(root) {
			var box = '.box:not(.content):not(.title):not(.plain), ul.clicknshow > li, ul.drawers li.panel, li.multi, .errorpage .plain';
			var elements = root.filter(box).add(root.find(box));
			this.insertTopAndBottom(elements);
		},

		roundImages:function(root) {
			var images = root.find('img.rounded');
			this.rounder.replaceImages(images, {
				borderRadius: 8
			});
		},

		roundNavigation: function(root) {
			var menus = root.find('li .navigation-menu');
			this.insertTopAndBottom(menus);
		},

		insertTopAndBottom:function(elements) {
			elements.each(function(){
				var element = $(this);
				if(element.find('> span.top').length > 0) {
					return;
				}
				element.prepend($('<span class="top"></span>'));
				element.append($('<span class="bottom"></span>'));
			});
		},
		
		stripeTables: function(root) {
			var rows = root.find('tbody tr:odd');
			rows.addClass('odd');
		},
		
		makeUnpastable: function(root) {
			var unpastables = root.find('.unpastable');
			unpastables.bind('paste', function(e) { e.preventDefault(); });
		},
		
		splitLists: function(root) {
			var lists = root.find('ul.splitted, ol.splitted');
			lists.removeClass('splitted');
			lists.each(function(){
				var list = $(this);
				list.addClass('split');
				var items = list.find('li');
				var l = items.length -1;
				var pos = Math.ceil(items.length/2);
				var other = $(document.createElement(this.nodeName));
				
				for(var i=l; i>=pos; i--) {
					other.prepend(items[i]);
				}

				other.addClass(this.className);
				list.after(other);
				if(other.find('+ hr').length == 0) {
					other.after('<hr />');
				}
			});
		},

		vmlifyCorners:function(root) {
			var spans = $(root).find('span.top, span.bottom');
			var regTop = /top/;
			var regPX = /px/;
			var regBG = /#/;

			var getBackground = function(box) {
				var node = box.parentNode;
				while(node) {
					var color = node.currentStyle.backgroundColor;
					if(regBG.test(color)) {
						return color;
					}
					node = node.parentNode;
				}
			};

			var topL = 'm 0,10 c 0,10,0,0,10,0 l -10,-10 l 0,10 x';
			var topR = 'm 0,0 l 20,-10 l 10,10 c 10,10,10,0,0,0 x';
			var botL = 'm 0,0 c 0,0,0,10,10,10 l -10,20 l 0,0 x';
			var botR = 'm 10,0 l 20,20 l 0,10 c 0,10,10,10,10,0 x';

			var ctL = 'from="1,40" to="40,1" control1="0,15" control2="15,0"';
			var ctR = 'from="0,1" to="36,40" control1="15,0" control2="40,15"';
			var cbL = 'from="1,0" to="40,36" control1="0,15" control2="15,40"';
			var cbR = 'from="0,36" to="36,0" control1="15,40" control2="40,15"';

			spans.each(function() {
				var parent = this.parentNode;
				var w = this.offsetWidth;
				var ww = w -10;
				var css = this.parentNode.currentStyle;
				var top = regTop.test(this.className) ? true : false;
				var border = regPX.test(css.borderWidth)? true : false;
				var color = getBackground(parent);
				var stroke = css.borderColor;
				var weight = css.borderWidth;

				var pathL = top? topL : botL;
				var pathR = top? topR : botR;
				var curveL = top? ctL : cbL;
				var curveR = top? ctR : cbR;
				
				var box = [
					'<vml:group coordsize="40,40" coordorigin="0,0" style="display:block; width:10px;height:10px;position:absolute;left:0;top:0;">',
						'<vml:shape coordsize="10,10" style="width:100%;height:100%;" filled="true" fillColor="',color,'" stroked="false"  path="',pathL,'" />',
						(border? '<vml:curve '+curveL+' filled="false" strokeWeight="'+weight+'" strokeColor="'+stroke+'"  />' : ''),
					'</vml:group>',

					'<vml:group coordsize="40,40" coordorigin="0,0" style="display:block; width:10px;height:10px;position:absolute;left:',ww,'px;top:0;">',
						'<vml:shape coordsize="10,10" style="width:100%;height:100%;" filled="true" fillColor="',color,'" stroked="false" path="',pathR,'" />',
						(border? '<vml:curve '+curveR+' filled="false" strokeWeight="'+weight+'" strokeColor="'+stroke+'"  />' : ''),
					'</vml:group>'
				];

				this.insertAdjacentHTML('BeforeEnd', box.join(''));
			});
		}
	};

	/**
	 * Credentials 
	 * 
	 */
	function Credentials() {
		var opt = $('meta[name="show-credentials"]').attr('content');
		
		if(/(false|off|no)/i.test(opt)) {
			return;
		}
		
		var path = NS.getProperty('URL_CREDENTIALS');
		if(path) {
			var self = this;
			window.handleCredentials = function(json) {
				if(json && json.message) {
					self.displayStatus(json);
				}
			};
			
			$.ajax({
				url: path,
				dataType: 'jsonp',
				jsonpCallback: 'handleCredentials'
			});
		}
	}

	Credentials.prototype = {
		displayStatus:function(json) {
			var message = '<span>' + json.message + '</span> ';
			var logout = json.logout || '';
			var credentials = $(logout + message);
			
			var content = $('#content');
			var target = content.find('p.credentials');
			if(target.length) {
				target.eq(0).html(credentials);
			} else {
				var p = $('<p class="credentials"></p>');
				content.prepend(p);
				p.html(credentials);
			}
		}
	};


	/**
	 * Drawers controls toggleable elements, like the faq.
	 */
	function Drawers() {
		NS.relateLink(/drawer/, this.handleClick.bind(this) /*, null */);
	}

	Drawers.prototype = {
		handleClick:function(link, rel) {
			var item = $(link).closest('.drawer');
			var list = item.closest('.drawers');
			if (!list.hasClass('multiple')) {
				item.siblings('.drawer').removeClass('opened');
			}
			item.toggleClass('opened');

			NS.Dispatcher.fire('layoutchanged', list[0]);
			
			return true;
		}
	};

	/**
	 * InputHints displays the title attribute of text inputs as their initial value.
	 * Additionally, it autofocusses any input with class "autofocus" onload and after ajax
	 */
	function InputHints() {
		this.parseNode(document);
	}

	InputHints.prototype = {
		parseNode:function(root) {
			var $root = $(root);
			var inputs = $root.find('input:text[title], textarea[title]');
			
			inputs.bind('focus', function(e){ 
				if(this.value === this.title) {
					this.value = ''; 
					this.style.color = '';
				}
			});
			
			inputs.bind('blur',  function(e){ 
				if(this.value === '' || this.value === this.title) {
					this.style.color = '#7F7FB2';
					this.value = this.title; 
				}
			});

			inputs.triggerHandler('blur');

			$root.find('input.autofocus').focus();
		}
	};

	/**
	 * Briefings toggles the summary on T6 pages.
	 */
	function Briefings() {
		var lang = NS.getLanguage();
		var briefing = $('#content .box.summary');
		var text = /nl/i.test(lang)? 'Toon extra tekst' : 'Show additional text';
		
		var labelSwitches = [
			{ reg: /^Toon/, value: 'Verberg' },
			{ reg: /^Verberg/, value: 'Toon' },
			{ reg: /^Show/, value: 'Hide' },
			{ reg: /^Hide/, value: 'Show' }
		];

		function getSwitchedLabel(label) {
			var l = labelSwitches.length;
			for(var i=0; i<l; i++) {
				var item = labelSwitches[i];
				if(item.reg.test(label)) {
					return label.replace(item.reg, item.value);
				}
			}
		}
		
		if (briefing.hasClass('open')) {
			text = getSwitchedLabel(text);
		}
		
		var button = $('<a href="#" class="summary-trigger">' + text + '</a>');
		
		briefing.before(button);

		button.click(function(e){
			e.preventDefault();
			var label = getSwitchedLabel(button.text());
			briefing.toggleClass('open');
			button.html(label);

			NS.Dispatcher.fire('layoutchanged', briefing[0]);
		});
	}

	/**
	 * ClickAndShow is used on T5 pages to switch between hidden content sections.
	 * The "clicknshow" list links to the id's of these sections using a hash.
	 * Observes in high priority mode to precede other events that might rely on visibility
	 */
	function ClickAndShow() {
		var lists = $('ul.clicknshow');
		if(lists.length > 0) {
			NS.subscribe('click:link:hash', this.handleClick.bind(this), PRIORITY_HIGH);
			this.checkLocation();

			NS.relateLink(/reset-briefings/, this.reset.bind(this));  
		}
	}

	ClickAndShow.prototype = {
		checkLocation:function(){
			var hash = window.location.hash;
			if(hash) {
				var link = $('a[href$="'+hash+'"]')[0];
				var target = $(hash)[0];
				if(link && target) {
					this.activate(link, target);
				}
			}
		},

		handleClick:function(e) {
			var link = e.target;
			var target = e.hashTarget;

			if(!target) {
				return;
			}

			var $link = $(link);

			// activate a summary via a click inside the menu
			var list = $link.closest('.clicknshow')[0];
			if(list) {
				this.activate(link, target);
				return;
			}
			
			// activate a summary via a hash click outside of the menu
			var briefing = $link.closest('.briefings')[0];
			if(briefing) {
				var proxy = $('.clicknshow a[href$=' + link.hash + ']')[0];
				if(proxy) {
					this.activate(proxy, target);
				}
			}
		},

		activate:function(link, target) {
			try {
				var active = $(link).closest('ul').find('li.active');
				var current = $(active.find('a')[0].hash);
				active.removeClass(CLASS_ACTIVE);
				current.removeClass(CLASS_ACTIVE);
			} catch (e) { }
			$(link.parentNode).addClass(CLASS_ACTIVE);
			$(target).addClass(CLASS_ACTIVE);
			$("body").addClass('clickednshown'); //hides promos once a briefing is clicked
			
			NS.Dispatcher.fire('clickandshow', target);
			NS.Dispatcher.fire('layoutchanged', document.body);
		},

		reset:function(link, rel) {
			var targets = $('ul.clicknshow li').add('.briefings .default');
			targets.removeClass(CLASS_ACTIVE);
			$("body").removeClass('clickednshown');
			NS.Dispatcher.fire('layoutchanged', document.body);
			return true;
		}
	};

	/**
	 * External links or links that point to enclosures like pdf files, are provided
	 * with an icon marking them as external. A new window (or tab) is opened for
	 * these links via the link relation object.
	 */
	function ExternalLinks() {
	//	NS.relateLink(/external/, this.handleClick /*, null */);
		this.parseNode(document);
	}

	ExternalLinks.prototype = {
		handleClick: function(link, rel) {
			window.open(link.href);
			return true;
		},

		parseNode:function(root) {
			var iconHTML = '<img src="/ns2010/static/images/icons/external.png" class="icon" width="22" height="15" alt="" />';
			$('a[rel~=external], a[rel~=enclosure]', root).each(function(){
				var $link = $(this);
				$link.attr('target', '_blank');
				$link.append(iconHTML);
			});
		}
	};

	/**
	 * Smoothscrolling captures clicks on links that point to a specific hash (id) on
	 * the page, and scrolls to them over a short period. rather than jumping directly.
	 * Observes in low priority, to follow after any events that may change the layout.
	 */
	function SmoothScrolling() {
		NS.subscribe('click:link:hash', this.handleClick.bind(this), PRIORITY_LOW);
	}

	SmoothScrolling.prototype = {
		handleClick: function(e) {
			var link = e.target;
			
			if(/[a-z]+/i.test(link.rel)) {
				// link has a rel, implied functionality should take precedence.
				return;
			}

			var path = link.pathname;
			var page = location.href;
			if(path && page.indexOf(path) < 0) {
				// the hashtarget was found on this page, but the link actually points to another page.
				return;
			}

			// if here, we can cancel the click and initialize scrolling			
			e.preventDefault(); 
			
			this.hash = link.hash;
			this.scrollTo(e.hashTarget);
		},

		scrollTo:function(target) {
			if(!this.window) {
				this.window = $(window);
				this.animator = new NS.Animator(this.scroll, this.highlightTarget.bind(this));
			}
			
			var jTarget = $(target);
			if(jTarget.is(':visible')) {
				this.target = target;
				var currentY = this.window.scrollTop();
				var viewHeight = this.window.height();
				var targetY = $(target).offset().top;
				if(targetY < currentY || targetY > (currentY + viewHeight)) {
					this.animator.run(currentY, targetY, 600);
				} else {
					this.highlightTarget(false);
				}
			}
		},

		scroll:function(value) {
			window.scrollTo(0, value);
		},

		highlightTarget:function(doHash) {
			if(this.target == document.body) { 
				return; 
			}
			
			NS.Interface.highlight(this.target);
			var hash = this.hash || this.target.id;
			if(hash && doHash !== false) {
				window.location.hash = hash;
			}
		}
	};

	/**
	 * The Highlighter highlights elements on the page, for instance when the visibility
	 * is toggled from one element to another, or a smoothscroll animation has ended.
	 */
	function Highlighter() {
		// don't do anything untill needed
	}

	Highlighter.prototype = {
		highlight: function(target){
			if(!target || NS.Dispatcher.fire('highlight', target) === false) {
				return;
			}

			if(!this.pointer) {
				this.pointer = $('<div id="pointer"></div>');
				this.root = $('#canvas');
				this.animator = new NS.Animator(
					this.animate.bind(this),
					this.hide.bind(this)
				);
			}
			
			var jTarget = $(target);
			var offset = jTarget.offset();
			this.pointer.css({
				left: offset.left + 'px',
				top: offset.top + 'px',
				width: jTarget.outerWidth() + 'px',
				height: jTarget.outerHeight() + 'px',
				opacity: 0
			});

			this.root.append(this.pointer);
			this.animator.run(0, Math.PI, 1000);
		},

		animate:function(value) {
			this.pointer.css({ opacity: Math.sin(value) * 0.4 });
		},

		hide:function(e) {
			this.pointer.remove();
		}
	};

	/**
	 * Readspeaker reads either the full page, a selection of text, or the contents of an 
	 * element (id-) targeted by a link's hash. That link must have a rel="readspeaker".
	 */
	function Readspeaker() {
		this.exceptions = /^(tr|option)$/i;
		NS.relateLink(/readspeaker/, this.readElement.bind(this));
	}

	Readspeaker.prototype = {
		read:function(node) {
			var content = this.getSelection();
			
			if(!content) {
				content = this.parse(node).innerHTML;
			}
			
			this.form = document.getElementById('rs_form');
			if(!this.form) {
				this.form = this.createForm();
			}
			
			this.form.target = 'rs';
			this.form.rshtml.value = content;
			var rs = window.open("about:blank", "rs", "width=400, height=220");
			rs.focus();
			this.form.submit();
		},
		
		readElement:function(link, rel) {
			var target = $(link.hash || '#content')[0];
			if(target) {
				this.read(target);
			}
			return true;
		},
		
		createForm:function() {
			var form = $('<form method="post" action="http://asp.readspeaker.net/cgi-bin/nsrsone" target="rs" id="rs_form">' + 
					'<input type="hidden" name="rshtml" value="" />' + 
					'<input type="hidden" name="url" value="' + window.location.href + '" />' + 
					'<input type="hidden" name="customerid" value="1003740" />' + 
					'<input type="hidden" name="lang" value="nl" />' + 
				'</form>');
			
			$('body').append(form);
			return form[0];
		},

		getSelection:function() {
			return document.getSelection? document.getSelection() :
				window.getSelection? window.getSelection() :
				document.all? document.selection.createRange().text : false;
		},
			
		parse:function(node) {
			var buffer = document.createElement("div");
			this.parseNode(node, buffer);
			return buffer;
		},

		parseNode:function(node, buffer) {
			var l = node.childNodes.length, child, clone;
			for(var i=0; i<l; i++) {
				child = node.childNodes.item(i);
				if(child.nodeType === 1 && !child.offsetHeight && !child.offsetWidth && !this.exceptions.test(child.nodeName)) {
					continue;
				}

				clone = buffer.appendChild(child.cloneNode(false));
				this.parseNode(child, clone);
			}
		}
	};

	/**
	 * The validator observes submit and ajax submit events, and validates the form's 
	 * input before allowing it to be posted. Applications are queried for preferences.
	 */
	function Validator() {
		NS.Dispatcher.subscribe('submit', this.onsubmit.bind(this));
		NS.Dispatcher.subscribe('ajaxsubmit', this.onsubmit.bind(this));
	}

	Validator.prototype = {
		/**
		 * main onsubmit handler, queries apps for preferences and validates
		 */
		onsubmit:function(e) {
			var valid = true;
			var form = e.target;
			var app = NS.findApplication(form);

			var button = e.data.explicitTarget;
			var validate = $(button).attr(ATTR_VALIDATE);
			if(/false/i.test(validate)) {
				return;
			}

			if(app.prefers(PREF_VALIDATE_REQUIRED)) {
				valid &= this.validateRequired(form);
			}

			if(valid && app.prefers(PREF_VALIDATE_SERVER)) {
				// e.target is the form, the explicitTarget is the clicked submit button
				valid &= this.validateServer(form, e.data.explicitTarget);
			}

			if(!valid) {
				e.preventDefault();
				return false;
			}
		},

		validateRequired:function(form){
			var elements = form.elements || $('input,select,textarea', form), valid = true;
			var l = elements.length;

			for (var j=0; j<l; j++) {
				NS.Interface.displayError(elements[j], false);
			}
			
			for(var i=0; i<l; i++) {
				var input = elements[i];
				var hasValue = this.hasValue(input);
				
				if(this.isRequired(input)) {
					if(!hasValue) {
						NS.Interface.displayError(input, true);
					}
					valid &= hasValue;
				}

				var pattern = input.getAttribute(ATTR_PATTERN);
				if(pattern && hasValue) {
					var match = this.matches(input, pattern);
					NS.Interface.displayError(input, !match);
					valid &= match;
				}
			}
			
			if(valid) {
				NS.Interface.displayErrorMessage(false);
			}
			
			return valid;
		},

		isRequired:function(input) {
			var req = input.getAttribute(ATTR_REQUIRED);
			var required = /true/i.test(req);
			var optional = /false/i.test(req);
			var completed = input.getAttribute(ATTR_COMPLETE);
			var disabled = input.disabled;
			var visible = input.offsetHeight > 0;
			
			if(completed) { required = true; } // autocomplete implies required
			if(optional) { required = false; } // optional overrules implied required

			return (visible && !disabled && required);
		},

		hasValue:function(input) {
			if(!input.type) { return false; } // skip fieldsets
			
			var type = input.type.toLowerCase();
			switch (type) {
				case TYPE_TEXT: 
				case TYPE_PASS: 
				case TYPE_AREA:
					return (input.value && input.value !== input.title && !(REG_WHITE.test(input.value))) ? true : false;
				case TYPE_CHECK: 
				case TYPE_RADIO: 
					return this.checkedOne(input);
				case TYPE_SELECT: 
				case TYPE_MULTI: 
					return this.selectedOne(input);
			}
		},

		checkedOne:function(input) {
			var name = input.name, checks = name? input.form.elements[name] : [];
			checks = checks.length? checks : [input];
			for (var i=0; i<checks.length; i++) {
				if(checks[i].checked) { return true; }
			}	return false;
		},

		selectedOne:function(select) {
			return select.selectedIndex > 0; // the first option is considered a label, not a value
		},

		matches:function(input, pattern) {
			var reg = new RegExp("^"+pattern+"$", "i");
			return reg.test(input.value);
		},

		validateServer:function(form, explicitTarget) {
			var post = NS.getFormValues(form);
			var url = NS.getProperty('POST_VALIDATE', form);
			NS.XHR.sendAndLoad(post, url, function(xml){
				this.handleResponse(xml, form, explicitTarget);
			}.bind(this));
			
			// submitting is delayed, so return invalid here
			return false;
		},
		
		handleResponse:function(xml, form, explicitTarget) {
			var messages = [];
			$('error', xml).each(function(){
				var name = this.getAttribute('veld'),
					input = form.elements[name];

				if(input) {
					messages.push('- ' + this.firstChild.nodeValue);
					NS.Interface.displayError(input, true);
				}
			});

			if(messages.length > 0) {
				NS.Interface.displayErrorMessage(messages.join(' <br />'), form);
			} else {
				NS.Interface.displayErrorMessage(false);
				if(explicitTarget) {
					// if the original event had an explicitTarget input, click it
					$(explicitTarget).trigger('click');
				} else {
					// otherwise just submit the form
					form.submit();
				}
			}
		}
	};
	
	/**
	 * RememberedInputs sets a cookie for input fields that have their value stored
	 */
	function RememberedInputs() {
		NS.subscribe('submit', this.handleSubmit.bind(this));
		NS.subscribe('click:input', this.handleClick.bind(this));
		
		this.restoreValues(document);
	}
	
	RememberedInputs.prototype = {
		parseNode:function(node) {
			this.restoreValues(node);
		},

		handleSubmit:function(e) {
			var checks = $(e.target).find('input:checkbox');
			var l = checks.length;
			for(var i=0; i<l; i++) {
				var check = checks[i];
				if(check.getAttribute('ns:remember')) {
					this.storeValues(check);
				}
			}
		},

		handleClick:function(e){
			var input = e.target;
			
			// old way; store input value based on implied id reference
			var id = input.id;
			var reg = /remember-([^ ]+)/i;
			if(id && reg.test(id)) {
				var relatedInput = document.getElementById(reg.exec(id)[1]);
				if(relatedInput) {
					this.storeValue(relatedInput, input.checked);
				}
			}	
			
			// new way; store named inputs via custom attribute reference
			var remember = input.getAttribute('ns:remember');
			if(remember) {
				this.storeValues(input);
			}
		},

		storeValue:function(target, checked) {
			var name = 'remember-' + target.name;
			NS.setCookie(name, checked? target.value : "");
		},

		storeValues:function(input) {
			var form = input.form;
			var names = (input.name + ',' + input.getAttribute('ns:remember')).split(',');
			var l = names.length;
			for (var i=0; i<l; i++) {
				var target = form.elements[names[i]];
				if(target) {
					this.storeValue(target, input.checked);
				}
			}
		},
		
		getRememberedValues:function() {
			if(this.values) {
				return this.values;
			}

			// check the cookie for any remembered-xyz entries
			var value, cookie = document.cookie, remember = /remember-[^=]+=[^;\$]+/mig;
			var values = [];
			while((value = remember.exec(cookie))) {
				values.push(String(value));
			}

			this.values = values;
			return values;
		},

		restoreValues:function(root) {
			var values = this.getRememberedValues();
			var inputs = $(root || document).find('input');
			var l = values.length;
			var reg = /remember-/;

			for (var i=0; i<l; i++) {
				var splitted = values[i].split('=');
				var id = splitted[0];
				var name = id.replace(reg, '');
				var value = splitted[1];

				var input = inputs.filter('[name="'+name+'"]')[0];
				if(input && value) {
					switch (input.type.toLowerCase()) {
						case 'text': 
							if(!input.value) {
								input.value = decodeURIComponent(value); 
							}
						break;
						case 'checkbox':
						case 'radio':
							input.checked = true;
						break;
					}

					// backward compatibility
					var check = document.getElementById(id);
					if(check) {
						check.checked = true;
					}
				}
			}
		}
	};


	/**
	 * CardNumbers, autofocusses the next input if maxlength is reached.
	 * 
	 */
	function CardNumbers() {
		this.parseNode(document);
	}

	CardNumbers.prototype = {
		parseNode:function(root) {
			var cards = $(root).find('div.card-number');
			if(cards.length) {
				cards.bind('keyup', this.cardKeyUp.bind(this));
				cards.bind('keydown', this.cardKeyDown.bind(this));

				var first = cards.find('input:text').eq(0);
				first.focus();
				first.trigger('keyup');
			}
		},

		isKeyMod:function(e) {
			var code = e.keyCode;
			return e.ctrlKey | e.shiftKey | e.altKey | /^(8|9|16|17|18|46)$/.test(code);
		},
			
		cardKeyUp:function(e) {
			if(this.isKeyMod(e)) { return; }

			var target = e.target;
			if(/input/i.test(target.nodeName)) {
				var val = target.value;
				var max = target.getAttribute('maxlength');
				if(max) {
					max = parseInt(max, 10);
				}
				if(val && max && val.length >= max) {
					var next = $(target).next('input:text');
					if(next.length) {
						if(next.val().length) {
							next.select();
						} else {
							try {
								next.focus();
								next.trigger('keyup');
							} catch (e) {}
						}
					}
				}
			}
		},

		cardKeyDown:function(e) {
			if(this.isKeyMod(e)) { return; }

			var code = e.keyCode;
			var key = String.fromCharCode(code);
			var numpad = (code >= 96 && code <= 105)? true : false;
			if(!/[0-9]/.test(key) && !numpad && code !== 13) {
				e.preventDefault();
				return false;
			}
		}
	};
	
	/**
	 * BrowserCompatibility checks various browser specific quirks
	 */
	function BrowserCompatibility() {
		var br = $.browser;
		if(br.msie && (parseInt(br.version, 10) <= 8)) {
			this.cacheBackgrounds();
			this.checkDPI();
			this.insertVMLNamespace();
		}
	}
	
	BrowserCompatibility.prototype = {
		cacheBackgrounds:function() {
			// IE Anti-background flicker, conditional comment, runs in IE only
			/*@cc_on
				try { document.execCommand('BackgroundImageCache', false, true); } catch (e) {}
			@*/
		},
		
		checkDPI:function() {
			// MSIE DPI setting, only for IE7 or below, not for IE8 running as IE7,
			// because in IE8 the DPI is interpreted as a full page zoom factor, not as textzoom
			if(!document.documentMode && screen.deviceXDPI && (screen.deviceXDPI != 96)) {
				var ratio = (96/screen.deviceXDPI) * 100;
				document.body.style.fontSize = ratio + '%';
			}
		},

		insertVMLNamespace:function() {
			var NS_VML = "urn:schemas-microsoft-com:vml";
			this.importNS = '';
			if(!document.documentMode || document.documentMode < 8) {
				// IE6, 7 and 8 in compat mode
				document.namespaces.add("vml", NS_VML); 
				document.createStyleSheet().addRule('vml\\:*', "behavior: url(#default#VML);");
			} else if(document.documentMode && document.documentMode >= 8) {
				// IE8 in strict mode
				document.namespaces.add("vml", NS_VML, "#default#VML");
				this.importNS = '<?import namespace="vml" implementation="#default#VML" ?>'; 
			}
		}
	};

	/**
	 * Monitors the "layoutchanged" event to update various components. This event may be fired by 
	 * objects that (for instance) change classnames in order to toggle element visibility.
	 */
	function LayoutMonitor() {
		NS.subscribe('layoutchanged', this.layoutUpdated.bind(this));
	}

	LayoutMonitor.prototype = {
		layoutUpdated: function(e) {
			var root = $(e.target);

			// if object/embed elements become invisible, they need to stop playing.
			this.updateJWPlayer(root);

			// possible future layout checks
			// ...
		},

		updateJWPlayer:function(root) {
			var players = root.find('object,embed');
			players.each(function(){
				var player = this;
				var h = player.offsetHeight;
				if(!h || h === 0) { // hidden elements have 0 or no offsetheight
					try {
						player.sendEvent("PLAY", false);
					} catch (e) {
						NS.log('SendEvent failed or was not supported', player, e);
					}
				}
			});
		}
	};


	/**
	 * Bind to NS.initialize
	 */
	NS.subscribe('initialize', function(){
		NS.Interface.initialize();
	});

});
