/*jslint strict: true, browser: true, plusplus: true, devel: true, jquery: true, bitwise: true, maxerr: 50 */
/*global vQuery: true */
(function( global, jQuery ){
	"use strict";
	var vdp = function( element, options ){
		jQuery.extend( this, this.defaults, options );
		this.inputAllowTyping = this.defaults.isTouchDevice ? false : this.inputAllowTyping;

		var callback = function( element, options ) {
			var id = Math.random().toString( 36 ).substr( 2, 9 );

			this.options = options || {};
			this.element = jQuery( element );
			this.parent = jQuery( 'body' );

			if ( !this.layer ) {
				this.layer = jQuery( '<div class="vdp-layer" />' ).on( 'click.vdp', jQuery.proxy( this.event, this ) ).appendTo( 'body' );
				this.layer.attr( 'data-vdpLayer', id );

				if ( 'className' in options ) {
					this.layer.addClass( options.className );
				}

				if ( this.autoHide ) {
					jQuery( 'html' ).on( 'click.vdp', jQuery.proxy( this.hide, this ) );
				}

				var parent = this.element
					.parents()
					.filter(function(){
						return jQuery( this ).css( 'overflow-y' ) === 'scroll';
					})
					.eq( 0 );

				if ( parent.length ) {
					this.parent = parent;
				}

				jQuery( this.parent.is( 'body' ) ? global : this.parent ).on( 'scroll.vdp', jQuery.proxy( this.scroll, this ) );
			}

			this.input = this.element.is( 'input' ) ? this.element.prop( 'autocomplete', 'off' ) : jQuery( '<input />' );
			this.input
				.attr( 'data-vdpInput', id )
				.addClass( 'vdp-input' );
			this.display = this.element.is( 'input' ) ? 'popup' : 'inline';

			if ( this.dateRangeStartInput ) {
				this.dateRangeStartInput = jQuery( this.dateRangeStartInput );
			}

			if ( this.dateRangeEndInput ) {
				this.dateRangeEndInput = jQuery( this.dateRangeEndInput );
			}

			this.dateSet( this.date || this.input.attr( 'data-vdpDate' ) || this.input.val() );
			this.datesInit( true );
			this.eventsBind();

			if ( this.display === 'inline' ) {
				this.show();
			}
			else if ( this.display === 'popup' ) {
				this.input.on( 'keyup.vdp keydown.vdp change.vdp', jQuery.proxy( this.event, this ) );
			}

			this.callback( 'onReady' );
		};

		this.callback( 'onInit', arguments, callback );

		return this;
	};

	vdp.prototype = {
		defaults: {
			mode: 'single',
			display: 'popup',
			dateFormat: 'dd.mm.yyyy',
			date: null,
			dateGoto: new Date(),
			dateRangeStart: null,
			dateRangeStartInput: null,
			dateRangeEnd: null,
			dateRangeEndInput: null,
			dateRangeResetPosition: 'dateRangeEnd',
			dateRangeLock: true,
			dateRender: null,
			dateMin: null,
			dateMax: null,
			dateWeekStart: 1,
			monthsAmount: 1,
			monthsOthersShow: false,
			monthsOthersSelectable: false,
			monthShowHeader: true,
			monthDropdown: false,
			yearShow: false,
			yearDropdown: false,
			yearDropdownFrom: -25,
			yearDropdownTo: 25,
			autoHide: true,
			inputAllowTyping: false,
			isTouchDevice: ( window.navigator.MaxTouchPoints || window.navigator.msMaxTouchPoints || ( 'ontouchstart' in document.documentElement && screen.width < 768 ) ),
//			onInit: function(){ return true; },
//			onReady: function(){ },
//			onShow: function(){},
//			onShowDay: function(){},
//			onHide: function(){},
//			onMonthChange: function(){},
//			onDateSelect: function(){},
			i18n: {
				months: [ [ 'Jan', 'Januar' ], [ 'Feb', 'Februar' ], [ 'März', 'März' ], [ 'April', 'April' ], [ 'Mai', 'Mai' ], [ 'Juni', 'Juni' ], [ 'Juli', 'Juli' ], [ 'Aug', 'August' ], [ 'Sep', 'September' ], [ 'Okt', 'Oktober' ], [ 'Nov', 'November' ], [ 'Dez', 'Dezember' ] ],
				days: [ [ 'So', 'Sonntag' ], [ 'Mo', 'Montag' ], [ 'Di', 'Dienstag' ], [ 'Mi', 'Mittwoch' ], [ 'Do', 'Donnerstag' ], [ 'Fr', 'Freitag' ], [ 'Sa', 'Samstag' ] ]
			}
		},

		callback: function( name, args, func ) {
			args = args || [];
			if ( typeof func !== 'function' ) {
				func = function(){};
			}

			this.next = function( ){
				func.apply( this, arguments );
				this.next = function(){};
			};

			if ( !( name in this && typeof this[ name ] === 'function' ) ) {
				this.next.apply( this, args );
			}
			else if ( this[ name ].apply( this, args ) !== false ) {
				this.next.apply( this, args );
			}
		},

		scrollTop: null,
		scrollTimestamp: null,
		scroll: function() {
			if ( this.display === 'popup' && this.layer.is( '.vdp-visible' ) ) {
				this.layerMove( true );
			}
		},

		layerMove: function( refresh ){
			var scrollTop = jQuery( global ).scrollTop();
			var _refresh = typeof refresh !== 'undefined' ? refresh : true;
			if ( this.scrollTop === null ){
				this.scrollTop = scrollTop;
				_refresh = true;
			}

			if ( this.scrollTop < scrollTop && this.layer.css( 'top' ) !== 'auto' && !_refresh ) {
				this.scrollTop = scrollTop;
				return;
			}

			if ( this.scrollTop > scrollTop && this.layer.css( 'bottom' ) !== 'auto' && !_refresh ) {
				this.scrollTop = scrollTop;
				return;
			}

			this.scrollTop = scrollTop;

			var offset = this.input.offset();
			var offsetTop = offset.top - this.input.height();
			var globalHeight = jQuery( global ).height();
			var bodyHeight = jQuery( 'html' ).outerHeight();
			var bodyHeightOuter = jQuery( 'body' ).outerHeight();

			// if body has height: 0px cause they dont know what they are doing...
			if (!bodyHeightOuter) {
				bodyHeightOuter = globalHeight;
			}

			// check if html has property height: 100%; (or greater) then get document outerHeigth insteed.
			if ( bodyHeight >= globalHeight ) {
				bodyHeight = jQuery( global.document ).outerHeight();
			}

			var layer_css = {
				left: offset.left
			};

			if ( offsetTop - scrollTop > globalHeight - 270 ) {
				// if body has height: 100%; (or greater) don't use bottom.
				if( bodyHeightOuter >= globalHeight ) {
					layer_css.top = offset.top - this.input.outerHeight() - 290;
					layer_css.bottom = 'auto';
				}
				else {
					layer_css.bottom = ( bodyHeight - offsetTop ) + 'px';
					layer_css.top = 'auto';
				}
			}
			else {
				layer_css.top = offset.top + this.input.outerHeight() + 3;
				layer_css.bottom = 'auto';
			}

			this.layer
				.removeAttr( 'stlye' )
				.css( layer_css );
		},

		show: function() {
			this.render();

			var callback = function() {
				if ( this.display === 'popup' ) {
					this.layerMove( true );

					if ( this.inputAllowTyping && this.input.attr( 'placeholder' ) !== this.dateFormat ) {
						this.inputPlaceHolderOriginal = this.input.attr( 'placeholder' ) || '';
						this.input.attr( 'placeholder', this.dateFormat );
					}

					this.layer
						.addClass( 'vdp-visible' )
						.siblings( '.vdp-layer')
						.each(function(){
							var instance_id = jQuery( this ).attr( 'data-vdpLayer' );
							var instance = jQuery( '[data-vdpInput="' + instance_id + '"]' ).data( 'vdp' );

							if ( instance ) {
								instance.hide( true );
							}
						});
				}
			};

			this.callback( 'onShow', [], callback );
		},

		hide: function( autoHide, timeout ){
			if ( autoHide && !this.autoHide ) {
				return;
			}

			var _vdp = this;
			var callback = function() {
				this.layer.removeClass( 'vdp-visible' );
			};

			if ( this.inputAllowTyping && this.display === 'popup' ) {
				this.input.attr( 'placeholder', this.inputPlaceHolderOriginal );
			}

			if ( this.date ) {
				this.input.val( this.date.vFormat( this.dateFormat ) );
			}
			else {
				this.input.val( '' );
			}

			setTimeout( function(){
				_vdp.callback( 'onHide', [], callback );
			}, timeout );
		},

		refresh: function() {
			if ( arguments.length === 2 ) {
				if ( arguments[0] in this && typeof this[ arguments[0] ] === 'function' ) {
					this[ arguments[0] ].apply( this, [ arguments[1]] );
				}
				else {
					this[ arguments[0] ] = arguments[1];
				}
			}
			else {
				jQuery.extend( this, arguments[0] );
			}

			this.eventsBind();

			if ( this.display === 'inline' || this.layer.is( '.vdp-visible' ) ) {
				this.render();
			}
		},

		eventIgnore: false,

		event: function( event ){
			// ignore all non-native events
			if ('originalEvent' in event && event.originalEvent.isTrusted === false && !('%hammerhead%' in window)) {
				return;
			}

			if ( this.eventIgnore ) {
				this.eventIgnore = false;
				return;
			}

			var eventStop = true;
			var refresh = true;
			var target = jQuery( event.target );

			if ( !target.is( 'input,.vdp-listen' ) ) {
				target = target.parents( '.vdp-listen:first' );
			}

			this.callback( 'onEvent', [ event ] );

			if ( target.is( 'input' ) ) {
				var input_date;

				if ( !this.inputAllowTyping ) {
					if ( event.type === 'keyup' ) {
						switch( event.keyCode ) {
							case 8:// backspace
							case 46:// entf
								this.dateSet( false );
								return;
						}
					}
				}

				if ( event.type === 'click' ) {
					if ( this.display === 'popup' && this.layer.is( '.vdp-visible' ) ) {
						this.layerMove( true );
					}
					event.stopPropagation();
				}
				else if ( event.type === 'focus' && !this.layer.is( '.vdp-visible' ) ) {
					this.eventIgnore = !!event.keyCode;
//					this.callback( 'dateSet', [ target.val() ], this.show );
					input_date = target.val();

					if ( !this.date && input_date ) {
						this.dateSet( input_date );
					}

					this.show();
				}
				else if ( event.type === 'keydown' ) {
					eventStop = !this.inputAllowTyping;

					switch( event.keyCode ) {
						case 9:// tab
							this.hide();
							eventStop = false;
							break;

						case 13:// enter
						case 38:// arrow up
						case 40:// arrow done
							break;
					}
				}
				else if ( event.type === 'keyup' || event.type === 'change' ) {
					if ( this.inputAllowTyping ) {
						input_date = target.val();
						date = this.dateParse( input_date );

						if ( date && this.dateValid( date ) && date.vFormat( this.dateFormat ) ) {
							if ( this.date !== null && this.dateValid( this.date ) && date.vFormat() === this.date.vFormat() ) {
								refresh = false;
							}
							else {
								this.dateSet( date );
							}
						}
						else if ( !date && input_date == '' ) {
							this.dateUnset( );
						}
					}
					else {
						input_date = target.val();

						if ( !this.date && input_date ) {
							this.dateSet( input_date );
						}

					}
					if ( refresh ) {
						this.refresh();
					}
				}
			}
			else if ( target.is( '.vdp-date-reset' ) ) {
				this.dateRangeUnset();
				this.callback( 'onRangeReset', [], this.refresh );
			}
			else if ( target.is( '.vdp-date' ) ) {
				var date = target.data( 'date' );
				var callback = function() {
					this.refresh();

					if ( this.autoHide && this.display === 'popup' ) {
						if ( this.mode === 'single' ) {
							this.hide( true, 350 );
						}
						else if ( this.mode === 'range' ) {
							if ( this.dateRangeStart && this.dateRangeEnd ) {
								this.hide( true, 350 );
							}
						}
					}
				};
				this.dateSet( date, true );
				this.callback( 'onDateSelect', [ date ], callback );
			}
			if ( target.is( '.vdp-month-next' ) ) {
				this.dateRenderSet( this.dateRender.vFirst().vModify( undefined, 1 ) );
				this.callback( 'onMonthChange', [ this.dateStart.vClone(), this.dateEnd.vClone() ], this.render );
			}
			if ( target.is( '.vdp-month-prev' ) ) {
				this.dateRenderSet( this.dateRender.vFirst().vModify( undefined, -1 ) );
				this.callback( 'onMonthChange', [ this.dateStart.vClone(), this.dateEnd.vClone() ], this.render );
			}

			if ( eventStop ) {
				event.preventDefault();
				event.stopPropagation();
			}
		},

		eventsBind: function(){
			if ( this.display === 'inline' ) {
				this.element
					.off( 'click.vdp touchend.vdp tap.vdp' )
					.on( 'click.vdp touchend.vdp tap.vdp', jQuery.proxy( this.event, this ) );
			}
			else {
				if ( this.inputAllowTyping ) {
					this.input
						.prop( 'readonly', false )
						.off( 'keyup.vdp' )
						.on( 'keyup.vdp', jQuery.proxy( this.event, this ) );
				}
				else {
//					if ( this.isTouchDevice ) {
					this.input
						.prop( 'readonly', true )
						.off( 'keyup.vdp' );
//					}
				}

				this.input
					.off( 'click.vdp focus.vdp' )
					.on( 'click.vdp focus.vdp', jQuery.proxy( this.event, this ) );
			}
		},

		dateRenderSet: function( date ){
			date = this.dateParse( date );
			this.dateRender = date.vClone();
			this.dateStart = date.vClone().vFirst().vNoTime();
			this.dateEnd = date.vClone().vModify( 0, this.monthsAmount - 1 ).vLast().vNoTime();
		},

		render: function(){
			this.datesInit();

			if ( !this.dateRender.getTime() ) {
				this.dateRenderSet( this.dateGoto );
			}

			this.content = jQuery( '<div class="vdp-wrapper" />' );

			for( var i = 0; i < this.monthsAmount; i++ ) {
				this.content.append( this.renderMonth( i ) );
			}

			var callback = function() {
				if ( this.display === 'inline' ) {
					this.renderInline();
				}
				else {
					this.renderPopup();
				}

				this.callback( 'onRenderAfter' );
			};

			this.callback( 'onRenderBefore', [ this.dateStart.vClone(), this.dateEnd.vClone() ], callback );
		},

		renderInline: function(){
			var content_last = this.element.children( '.vdp-wrapper' );

			if ( content_last.length ) {
				content_last.replaceWith( this.content );
			}
			else {
				this.element.html( this.content );
			}
		},

		renderPopup: function(){
			this.layer.html( this.content );
		},

		renderMonth: function( month ){
			var dateDay = this.dateStart.vClone().vModify( 0, month );
			var dateEnd = dateDay.vLast();

			this.dateMonth = dateDay;

			this.contentMonth = jQuery( '<div class="vdp" />' );
			this.renderMonthHead();

			var weekDays = this.weekDaysGet();

			while( dateDay.getDay() !== weekDays[ 0 ] ) {
				dateDay = dateDay.vPrev();
			}

			while( dateEnd.getDay() !== weekDays[ weekDays.length - 1 ] ) {
				dateEnd = dateEnd.vNext();
			}

			this.contentMonthDays = jQuery( '<div class="vdp-row vdp-days"/>' );

			while( dateDay <= dateEnd ) {
				this.renderDay( dateDay );

				dateDay = dateDay.vNext();
			}

			this.contentMonth.append( this.contentMonthDays );

			return this.contentMonth;
		},

		renderMonthHead: function(){
			var htmlHead = jQuery( '<div class="vdp-row vdp-head"/>' );
			var i, l;

			if ( this.monthShowHeader ) {
				var select;
				var select_wrapper;
				var monthName = '';
				var optionDate;

				if ( this.isFirstMonth() ) {
					if ( !( this.dateMin instanceof Date ) || this.dateMin.vFirst() < this.dateMonth.vFirst() ) {
						htmlHead.append( '<span class="vdp-listen vdp-month-prev"><</span>' );
					}
				}

				if ( this.isLastMonth() || this.monthsAmount === 1 ) {
					if ( !( this.dateMax instanceof Date ) || this.dateMax.vFirst() > this.dateMonth.vFirst() ) {
						htmlHead.append( '<span class="vdp-listen vdp-month-next">></span>' );
					}
				}

				if ( this.monthDropdown ) {
					select_wrapper = jQuery( '<div class="vdp-head-month-wrapper" />').appendTo( htmlHead );
					select = jQuery( '<select class="vdp-head-month" />' )
						.on( 'change', jQuery.proxy( this.dateRenderSetFromDropdown, this ) )
						.appendTo( select_wrapper );
					var monthDiff = Math.abs( this.dateMonth.vDiff( this.dateStart, 'month' ) );

					for( i = 0, l = 12; i < l; i++ ) {
						optionDate = this.dateMonth.vClone();
						optionDate.setMonth( i - monthDiff );
						monthName = this.i18n.months[ i ];

						jQuery( '<option/>' )
							.val( i )
							.html( monthName[ 1 ] )
							.data( 'vdp-date', optionDate )
							.prop( 'selected', this.dateMonth.getMonth() === i )
							.appendTo( select );
					}
				}
				else {
					monthName = this.i18n.months[ this.dateMonth.getMonth() ];
					htmlHead.append( '<span class="vdp-head-month" title="' + monthName[1] + '">' + monthName[1] + '</span>' );
				}

				if ( this.yearShow ) {
					if ( this.yearDropdown ) {
						select_wrapper = jQuery( '<div class="vdp-head-year-wrapper" />').appendTo( htmlHead );
						select = jQuery( '<select class="vdp-head-year" />' )
							.on( 'change', jQuery.proxy( this.dateRenderSetFromDropdown, this ) )
							.appendTo( select_wrapper );

						var yearFrom = this.yearDropdownFrom.vFormat( 'yyyy' );
						var yearTo = this.yearDropdownTo.vFormat( 'yyyy' );

						for( ; yearFrom < yearTo; yearFrom++ ) {
							optionDate = this.dateMonth.vClone();
							optionDate.setFullYear( yearFrom );

							jQuery( '<option/>' )
								.val( yearFrom )
								.text( yearFrom )
								.data( 'vdp-date', optionDate )
								.prop( 'selected', this.dateMonth.getFullYear() === parseInt( yearFrom, 10 ) )
								.appendTo( select );
						}
					}
					else {
						htmlHead.append( '<span class="vdp-head-year"> ' + this.dateMonth.vFormat( 'yyyy' ) + '</span>' );
					}
				}

				this.contentMonth.append( jQuery( '<div class="vdp-row vdp-month" /> ' ).html( htmlHead ) );
			}

			htmlHead = '';
			var weekDays = this.weekDaysGet();

			for( i = 0, l = weekDays.length; i < l; i++ ) {
				var dayName = this.i18n.days[ weekDays[ i ] ];
				htmlHead += '<div class="vdp-cell" title="' + dayName[1] + '">' + dayName[0] + '</div>';
			}

			this.contentMonth.append( jQuery( '<div class="vdp-row vdp-daynames" /> ' ).html( htmlHead ) );
		},

		renderDay: function( dateDay ) {
			var day = jQuery( '<div class="vdp-cell" />' ).attr( 'data-date-iso', dateDay.vFormat( 'yyyy-mm-dd' ) );
			var daySelectable = true;

			if ( dateDay < this.dateMonth.vFirst() ) {
				daySelectable = false;
				day.addClass( 'vdp-date-before vdp-date-disabled' );
			}
			else if ( dateDay > this.dateMonth.vLast() ) {
				daySelectable = false;
				day.addClass( 'vdp-date-after vdp-date-disabled' );
			}

			if ( this.monthsOthersShow && this.monthsOthersSelectable && this.dateValid( dateDay ) ) {
				daySelectable = true;
			}

			if ( this.dateMin instanceof Date && dateDay < this.dateMin ) {
				daySelectable = false;
				day.addClass( 'vdp-date-disabled' );
			}
			else if ( this.dateMax instanceof Date && dateDay > this.dateMax ) {
				daySelectable = false;
				day.addClass( 'vdp-date-disabled' );
			}
			else if( this.options.dateRangeLock ) {
				if ( this.dateRangeStart instanceof Date && dateDay < this.dateRangeStart ) {
					daySelectable = false;
					day.addClass( 'vdp-date-disabled' );
				}
				else if ( this.dateRangeEnd instanceof Date && dateDay > this.dateRangeEnd ) {
					daySelectable = false;
					day.addClass( 'vdp-date-disabled' );
				}
			}

			if ( daySelectable ) {
				day
					.data( 'date', dateDay )
					.data( 'date-iso', dateDay.vFormat( 'yyyy-mm-dd' ) )
					.attr( 'title', dateDay.vFormat( this.dateFormat ) )
					.addClass( 'vdp-listen vdp-date' );

				if ( this.mode === 'single' ) {
					if ( dateDay.vIs( this.date ) ) {
						day.addClass( 'vdp-date-selected' );
					}
				}
				else if ( this.mode === 'range' ) {
					if ( dateDay.vIs( this.dateRangeStart ) || dateDay.vIs( this.dateRangeEnd ) ) {
						day.addClass( 'vdp-date-selected' );
					}
					if ( dateDay.vBetween( this.dateRangeStart, this.dateRangeEnd ) ) {
						day.addClass( 'vdp-date-range' );
					}
				}

				if ( this.dateRangeResetPosition ) {
					var resetElement = '<div class="vdp-date-reset vdp-listen"><span/></div>';

					if ( this.dateRangeResetPosition === 'date' && dateDay.vIs( this.date ) ) {
						day.append( resetElement );
					}
					else if ( this.dateRangeResetPosition === 'dateRangeStart' && dateDay.vIs( this.dateRangeStart ) ) {
						day.append( resetElement );
					}
					else if ( this.dateRangeResetPosition === 'dateRangeEnd' && dateDay.vIs( this.dateRangeStart ) && !this.dateRangeEnd ) {
						day.append( resetElement );
					}
					else if ( this.dateRangeResetPosition === 'dateRangeEnd' && dateDay.vIs( this.dateRangeEnd ) ) {
						day.append( resetElement );
					}
				}
			}

			var showDate = this.monthsOthersShow;
			if ( this.monthsAmount > 1 ) {
				showDate = this.monthsOthersShow && ( dateDay < this.dateStart || dateDay > this.dateEnd );
			}
			if ( dateDay.vIs( this.dateMonth, 'month' ) ) {
				showDate = true;
			}

			if ( !showDate ) {
				day.append( '<span>&nbsp;</span>' );
			}
			else {
				day.append( '<span>' + dateDay.getDate() + '</span>' );
			}

			var callback = function( date, day ) {
				day.appendTo( this.contentMonthDays );
			};

			if ( daySelectable ) {
				this.callback('onRenderDay', [dateDay.vClone(), day, daySelectable], callback);
			}
			else {
				day.appendTo( this.contentMonthDays );
			}
		},

		datesInit: function( initial ){
			this.dateGoto = this.dateParse( this.dateGoto );

			var date;

			if ( typeof this.yearDropdownFrom === 'number' ) {
				date = new Date();
				if ( this.yearDropdownFrom < 1000 ) {
					date.setFullYear( (new Date()).getFullYear() + this.yearDropdownFrom );
				}
				else {
					date.setFullYear( this.yearDropdownFrom );
				}
				this.yearDropdownFrom =  date;
			}

			if ( typeof this.yearDropdownTo === 'number' ) {
				date = new Date();
				if ( this.yearDropdownTo < 1000 ) {
					date.setFullYear( (new Date()).getFullYear() + this.yearDropdownTo + 1 );
				}
				else {
					date.setFullYear( this.yearDropdownTo );
				}
				this.yearDropdownTo =  date;
			}

			if ( this.dateMin === 0 ) {
				this.dateMin = (new Date()).vNoTime();

				if ( this.yearDropdownFrom && this.yearDropdownFrom.getFullYear() < this.dateMin.getFullYear() ) {
					this.yearDropdownFrom.setFullYear( this.dateMin.getFullYear() );
				}
			}

			if ( this.dateMax === 0 ) {
				this.dateMax = (new Date()).vNoTime();

				if ( this.yearDropdownTo && this.yearDropdownTo.getFullYear() > this.dateMax.getFullYear() ) {
					this.yearDropdownTo.setFullYear( this.dateMax.getFullYear() );
				}
			}

			if ( initial ) {
				if ( !this.dateValid( this.dateRender ) ) {
					if ( this.dateMin !== false ) {
						this.dateRenderSet( this.dateMin.vClone() );
						if( !this.dateValid( this.dateGoto ) ) {
							this.dateGoto = this.dateMin.vClone();
						}
					}
					else if ( this.dateMax !== false ) {
						this.dateRenderSet( this.dateMax.vClone() );
						if( !this.dateValid( this.dateGoto ) ) {
							this.dateGoto = this.dateMax.vClone();
						}
					}
				}
			}
		},

		dateSet: function( date, inputUpdate, callback ){
			date = this.dateParse( date );

			if ( date && this.dateValid( date ) ) {
				if ( this.mode === 'single' ) {
					this.date = date;
					this.dateRenderSet( this.date );

					var dateDisplay = this.date.vFormat( this.dateFormat );

					if ( inputUpdate && this.input.val() !== dateDisplay ) {
						this.input
							.val( dateDisplay )
							.trigger( 'change' );
					}

					this.input.attr( 'data-vdpDate', this.date.vFormat( 'yyyy-mm-dd' ) );
					this.callback( 'onDateSet', [ date.vClone() ], callback );
				}
				else {
					if ( this.dateRangeStart && this.dateRangeEnd ) {
						this.dateRangeUnset();
					}

					if ( !this.dateRangeStart ) {
						this.dateRangeStartSet( date );
						this.callback( 'onRangeStartSet', [ date.vClone() ], callback );
					}
					else {
						this.dateRangeEndSet( date );
						this.callback( 'onRangeEndSet', [ date.vClone() ], callback );
					}
				}
			}
			else {
				this.dateUnset();
			}
		},

		dateRangeStartSet: function( date ) {
			this.dateRangeStart = date;

			if ( this.dateRangeStartInput && this.dateRangeStartInput.is( 'input') ) {
				this.dateRangeStartInput.val( date.vFormat( this.dateFormat ) );
			}
		},

		dateRangeEndSet: function( date ) {
			this.dateRangeEnd = date;

			if ( this.dateRangeEndInput && this.dateRangeEndInput.is( 'input') ) {
				this.dateRangeEndInput.val( date.vFormat( this.dateFormat ) );
			}
		},

		dateMinSet: function( date ){
			if( this.dateMin !== null ) {
				this.dateMin = this.dateParse(date || (new Date()));
			}
			if ( this.dateMin && this.date && this.date < this.dateMin ) {
				this.dateSet( this.dateMin.vClone() );
			}
		},

		dateMaxSet: function( date ){
			this.dateMax = this.dateParse( date );

			if ( this.dateMax && this.date && this.date > this.dateMax ) {
				this.dateSet( this.dateMax );
			}
		},

		dateUnset: function(){
			this.dateRenderSet( this.dateParse( this.date ) || this.dateGoto );

			this.date = null;
			this.dateRangeUnset( );

			/*
			if ( !this.dateValid( this.dateRender, 'min' ) ) {
				this.dateRender = this.dateMin;
			}

			if ( !this.dateValid( this.dateRender, 'max' ) ) {
				this.dateRender = this.dateMax;
			}
			*/

			this.input.val( '' );
		},

		dateRangeUnset: function(){
			this.dateMinSet( this.options.dateMin );
			this.dateMaxSet( this.options.dateMax );

			this.dateRangeStartSet( null );
			this.dateRangeEndSet( null );
		},

		dateValid: function( date, type ){
			if ( type === 'min' ) {
				return !this.dateMin || date >= this.dateMin;
			}
			else if ( type === 'max' ) {
				return !this.dateMax || date <= this.dateMax;
			}

			return this.dateValid( date, 'min' ) && this.dateValid( date, 'max' );
		},

		dateRenderSetFromDropdown: function( event ){
			var date = jQuery( event.target ).children( ':selected' ).data( 'vdp-date' );
			this.dateRenderSet( new Date( date ) );
			this.callback( 'onMonthChange', [ this.dateStart.vClone(), this.dateEnd.vClone() ], this.render );

		},

		dateParse: function( date ) {
			if ( date instanceof Date ) {
				return date.vNoTime();
			}

			if ( typeof date !== 'string' ) {
				return false;
			}

			var isIso = date.match( /(\d{4})-(\d{2})-(\d{2})/ );
			var partMap = {};
			var dateParts = date.split( /\D/ );
			var patternParts = isIso ? [ 'yyyy', 'mm', 'dd' ] : this.dateFormat.split( /\W/ );
			var pad = function (val, len) {
				val = String(val);
				len = len || 2;
				while (val.length < len) {
					val = "0" + val;
				}
				return val;
			};

			if ( dateParts.length !== 3 ) {
				return false;
			}

			for( var i = 0, l = patternParts.length; i < l; i++ ) {
				var part = dateParts[ i ];

				if ( !parseInt( part, 10 ) ) {
					return false;
				}

				if ( patternParts[ i ] === 'yy' ) {
					patternParts[ i ] = 'yyyy';
				}

				if ( patternParts[ i ] === 'yyyy' ) {
					var today = new Date();

					if ( part.length === 3 ) {
						continue;
					}

					if ( String( part ).length !== 4 ) {
						if ( part - 10 < today.vFormat( 'yy' ) ) {
							part = 2000 + parseInt( part, 10 );
						}
						else {
							part = 1900 + parseInt( part, 10 );
						}
					}

					if ( part < 1900 || part > 3000 ) {
						part = '19' + String( part ).slice( -2 );
					}

				}
				else if ( String( part ).length < 2 ) {
					part = pad( part );
				}
				partMap[ patternParts[ i ] ] = part;
			}

			try {
				date = new Date( partMap.yyyy, partMap.mm - 1, partMap.dd );
			}
			catch (e) {
				date = null;
			}

			if ( !date || isNaN( date.getTime() ) ) {
				return false;
			}

			if ( date.vFormat( 'yyyy' ) < 1000   ) {
				return false;
			}

			return date.vNoTime();
		},

		isFirstMonth: function() {
			return this.dateMonth.vIs( this.dateStart, 'month' );
		},

		isLastMonth: function() {
			return this.dateMonth.vIs( this.dateEnd, 'month' );
		},

		weekDaysGet: function(){
			var days = [];

			for( var i = 0; i < 7; i++ ) {
				var day = this.dateWeekStart + i;
				days.push( day > 6 ? day - 7 : day );
			}

			return days;
		}
	};

	jQuery.fn.vdp = function () {
		var args = arguments || [];
		var result_return = false;

		if ( arguments.length && arguments[0] === 'get' ) {
			result_return = true;

			args = [];
			for( var i = 1, l = arguments.length; i < l; i++ ) {
				args[ i - 1 ] = arguments[ i ];
			}
		}

		var result = [];
		var that = this.each(function () {
			var instance = jQuery( this ).data( 'vdp' );

			if( jQuery.contains( document, this ) ) {
				if (typeof instance === 'undefined') {
					instance = new vdp(this, args[0]);
					jQuery(this).data('vdp', instance);
					result.push(instance);
				}
				else if (args.length === 1) {
					if (args[0] in instance) {
						if (typeof instance[args[0]] === 'function') {
							result.push(instance[args[0]].apply(instance));
						}
						else {
							result.push(instance[args[0]]);
						}
					}
					else {
						result.push(undefined);
					}
				}
				else {
					result.push(instance.refresh.apply(instance, args));
				}
			}
		});

		return result_return ? result : that;
	};

	Date.prototype.vClone = function( ) {
		var clone = new Date( );
		clone.setTime( this.getTime() );
		return clone;
	};

	Date.prototype.vNoTime = function( ) {
		this.setHours( 0, 0, 0, 0 );
		return this;
	};

	Date.prototype.vModify = function( year, month, day ) {
		year = year || 0;
		month = month || 0;
		day = day || 0;

		// fix last day of month calculation
		// if "this" is 2014-10-31 and we add 1 month, we get 2014-12-01 instead of 2014-11-30, cause november has only 30 days
		var clone = this.vClone();
		clone.setFullYear( this.getFullYear() + year );

		// fixes leap year calculation: 2016-02-29 plus one year is 2017-03-01 instead of 2017-02-28
		if ( this.getMonth() !== clone.getMonth() ) {
			clone.setDate( 0 );
		}

		if ( this.getDate() >= clone.vLast().getDate() ) {
			var date = new Date( clone.getFullYear(), clone.getMonth() + month, 1 ).vLast();

			if ( day ) {
				date.vModify( 0, 0, day );
			}

			return date;
		}

		return new Date( this.getFullYear() + year, this.getMonth() + month, this.getDate() + day );

		/*
		var a = new Date( 2014, 09, 31 );
		console.log('+ one month', a, a.vModify( 0, 1, 0 ) );

		var a = new Date( 2016, 01, 29 );
		console.log('+ one year', a, a.vModify( 1, 0, 0 ) );

		var a = new Date( 2017, 01, 28 );
		console.log('- one year', a, a.vModify( -1, 0, 0 ) );
		 */
	};

	Date.prototype.vFirst = function() {
		return new Date( this.getFullYear(), this.getMonth(), 1 );
	};

	Date.prototype.vLast = function() {
		return new Date( this.getFullYear(), this.getMonth() + 1, 0 );
	};

	Date.prototype.vPrev = function( ) {
		return new Date( this.getFullYear(), this.getMonth(), this.getDate() - 1 );
	};

	Date.prototype.vNext = function( ) {
		return new Date( this.getFullYear(), this.getMonth(), this.getDate() + 1 );
	};

	Date.prototype.vBetween = function( dateStart, dateEnd ) {
		if ( !( dateStart instanceof Date && dateEnd instanceof Date ) ) {
			return false;
		}

		return this.getTime() > dateStart.getTime() && this.getTime() < dateEnd.getTime();
	};

	Date.prototype.vIs = function( date, type ) {
		if ( type === 'month' ) {
			return this.getMonth() === date.getMonth();
		}
		else if ( date instanceof Date ) {
			return this.vFormat( 'yyyymmdd' ) === date.vFormat( 'yyyymmdd' );
		}

		return false;
	};

	Date.prototype.vDiff = function( date, type ) {
		var dateA = this.vClone();
		var dateB = date.vClone();

		dateA = dateA.getTime() < dateB.getTime() ? dateA : dateB;
		dateB = dateA.getTime() > dateB.getTime() ? dateA : dateB;

		switch ( type ) {
			case 'year':
				return dateA.getFullYear() - dateB.getFullYear();

			case 'month':
				var months = 0;

				if ( dateA.vFormat( 'yyyymm' ) !== dateB.vFormat( 'yyyymm' ) ) {
					while( dateA.getTime() < dateB.getTime() ) {
						months = months + 1;
						dateA.setMonth( dateA.getMonth() + 1 );
					}
				}

				return months;

			case 'week':
				return parseInt( ( dateA.getTime() - dateB.getTime() ) / ( 24 * 3600 * 1000 * 7 ), 10 );

			default:
				return parseInt( ( dateA.getTime() - dateB.getTime() ) / ( 24 * 3600 * 1000 ), 10 );
		}
	};

	/*
	 * Date Format 1.2.3
	 * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
	 * MIT license
	 *
	 * Includes enhancements by Scott Trenda <scott.trenda.net>
	 * and Kris Kowal <cixar.com/~kris.kowal/>
	 *
	 * Accepts a date, a mask, or a date and a mask.
	 * Returns a formatted version of the given date.
	 * The date defaults to the current date/time.
	 * The mask defaults to vDateFormat.masks.default.
	 */

	var vDateFormat = function () {
		var	token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
			timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
			timezoneClip = /[^-+\dA-Z]/g,
			pad = function (val, len) {
				val = String(val);
				len = len || 2;
				while (val.length < len) {
					val = "0" + val;
				}
				return val;
			};

		// Regexes and supporting functions are cached through closure
		return function (date, mask, utc) {
			var dF = vDateFormat;

			// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
			if (arguments.length === 1 && Object.prototype.toString.call(date) === "[object String]" && !/\d/.test(date)) {
				mask = date;
				date = undefined;
			}

			// Passing date through Date applies Date.parse, if necessary
			date = date ? new Date(date) : new Date();
			if (isNaN(date)) {
				throw SyntaxError("invalid date");
			}

			mask = String(dF.masks[mask] || mask || dF.masks["default"]);

			// Allow setting the utc argument via the mask
			if (mask.slice(0, 4) === "UTC:") {
				mask = mask.slice(4);
				utc = true;
			}

			var	_ = utc ? "getUTC" : "get",
				d = date[_ + "Date"](),
				D = date[_ + "Day"](),
				m = date[_ + "Month"](),
				y = date[_ + "FullYear"](),
				H = date[_ + "Hours"](),
				M = date[_ + "Minutes"](),
				s = date[_ + "Seconds"](),
				L = date[_ + "Milliseconds"](),
				o = utc ? 0 : date.getTimezoneOffset(),
				flags = {
					d:    d,
					dd:   pad(d),
					ddd:  dF.i18n.dayNames[D],
					dddd: dF.i18n.dayNames[D + 7],
					m:    m + 1,
					mm:   pad(m + 1),
					mmm:  dF.i18n.monthNames[m],
					mmmm: dF.i18n.monthNames[m + 12],
					yy:   String(y).slice(2),
					yyyy: y,
					h:    H % 12 || 12,
					hh:   pad(H % 12 || 12),
					H:    H,
					HH:   pad(H),
					M:    M,
					MM:   pad(M),
					s:    s,
					ss:   pad(s),
					l:    pad(L, 3),
					L:    pad(L > 99 ? Math.round(L / 10) : L),
					t:    H < 12 ? "a"  : "p",
					tt:   H < 12 ? "am" : "pm",
					T:    H < 12 ? "A"  : "P",
					TT:   H < 12 ? "AM" : "PM",
					Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
					o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
					S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 !== 10) * d % 10]
				};

			return mask.replace(token, function ($0) {
				return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
			});
		};
	}();

	vDateFormat.masks = {
		"default":      "ddd mmm dd yyyy HH:MM:ss",
		shortDate:      "m/d/yy",
		mediumDate:     "mmm d, yyyy",
		longDate:       "mmmm d, yyyy",
		fullDate:       "dddd, mmmm d, yyyy",
		shortTime:      "h:MM TT",
		mediumTime:     "h:MM:ss TT",
		longTime:       "h:MM:ss TT Z",
		isoDate:        "yyyy-mm-dd",
		isoTime:        "HH:MM:ss",
		isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
		isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
	};

// Internationalization strings
	vDateFormat.i18n = {
		dayNames: [
			"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
			"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
		],
		monthNames: [
			"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
			"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
		]
	};

	Date.prototype.vFormatSet = function( format ) {
		vDateFormat.masks["default"] = format;
	};

	Date.prototype.vFormat = function (mask, utc) {
		return vDateFormat(this, mask, utc);
	};
}( window, typeof vQuery !== 'undefined' ? vQuery : jQuery ));
