; (function (window, $, undefined) {
	; (function () {
		var VERSION = '2.2.3',
			pluginName = 'datepicker',
			autoInitSelector = '.datepicker-here',
			$body, $datepickersContainer,
			containerBuilt = false,
			baseTemplate = '' +
				'<div class="datepicker">' +
				'<i class="datepicker--pointer"></i>' +
				'<nav class="datepicker--nav"></nav>' +
				'<div class="datepicker--content"></div>' +
				'</div>',
			defaults = {
				classes: '',
				inline: false,
				language: 'en',
				startDate: new Date(),
				firstDay: '',
				weekends: [6, 0],
				dateFormat: '',
				altField: '',
				altFieldDateFormat: '@',
				toggleSelected: true,
				keyboardNav: true,

				position: 'bottom left',
				offset: 12,

				view: 'days',
				minView: 'days',

				showOtherMonths: true,
				selectOtherMonths: true,
				moveToOtherMonthsOnSelect: true,

				showOtherYears: true,
				selectOtherYears: true,
				moveToOtherYearsOnSelect: true,

				minDate: '',
				maxDate: '',
				disableNavWhenOutOfRange: true,

				multipleDates: false, // Boolean or Number
				multipleDatesSeparator: ',',
				range: false,

				todayButton: false,
				clearButton: false,

				showEvent: 'focus',
				autoClose: false,

				// navigation
				monthsField: 'monthsShort',
				prevHtml: '<svg viewBox="0 0 320 512"><path fill="currentColor" d="M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z"/></svg>',
				nextHtml: '<svg viewBox="0 0 320 512"><path fill="currentColor" d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"/></svg>',
				navTitles: {
					days: 'MM, <i>yy</i>',
					months: 'yy',
					years: 'yy1 - yy2'
				},

				// timepicker
				timepicker: false,
				onlyTimepicker: false,
				dateTimeSeparator: ' ',
				timeFormat: '',
				minHours: 0,
				maxHours: 24,
				minMinutes: 0,
				maxMinutes: 59,
				hoursStep: 1,
				minutesStep: 1,

				// events
				onSelect: '',
				onShow: '',
				onHide: '',
				onChangeMonth: '',
				onChangeYear: '',
				onChangeDecade: '',
				onChangeView: '',
				onRenderCell: ''
			},
			hotKeys = {
				'ctrlRight': [17, 39],
				'ctrlUp': [17, 38],
				'ctrlLeft': [17, 37],
				'ctrlDown': [17, 40],
				'shiftRight': [16, 39],
				'shiftUp': [16, 38],
				'shiftLeft': [16, 37],
				'shiftDown': [16, 40],
				'altUp': [18, 38],
				'altRight': [18, 39],
				'altLeft': [18, 37],
				'altDown': [18, 40],
				'ctrlShiftUp': [16, 17, 38]
			},
			datepicker;

		var Datepicker = function (el, options) {
			this.el = el;
			this.$el = $(el);

			this.opts = $.extend(true, {}, defaults, options, this.$el.data());

			if ($body == undefined) {
				$body = $('body');
			}

			if (!this.opts.startDate) {
				this.opts.startDate = new Date();
			}

			if (this.el.nodeName == 'INPUT') {
				this.elIsInput = true;
			}

			if (this.opts.altField) {
				this.$altField = typeof this.opts.altField == 'string' ? $(this.opts.altField) : this.opts.altField;
			}

			this.inited = false;
			this.visible = false;
			this.silent = false; // Need to prevent unnecessary rendering

			this.currentDate = this.opts.startDate;
			this.currentView = this.opts.view;
			this._createShortCuts();
			this.selectedDates = [];
			this.views = {};
			this.keys = [];
			this.minRange = '';
			this.maxRange = '';
			this._prevOnSelectValue = '';

			this.init()
		};

		datepicker = Datepicker;

		datepicker.prototype = {
			VERSION: VERSION,
			viewIndexes: ['days', 'months', 'years'],

			init: function () {
				if (!containerBuilt && !this.opts.inline && this.elIsInput) {
					this._buildDatepickersContainer();
				}
				this._buildBaseHtml();
				this._defineLocale(this.opts.language);
				this._syncWithMinMaxDates();

				if (this.elIsInput) {
					if (!this.opts.inline) {
						// Set extra classes for proper transitions
						this._setPositionClasses(this.opts.position);
						this._bindEvents()
					}
					if (this.opts.keyboardNav && !this.opts.onlyTimepicker) {
						this._bindKeyboardEvents();
					}
					this.$datepicker.on('mousedown', this._onMouseDownDatepicker.bind(this));
					this.$datepicker.on('mouseup', this._onMouseUpDatepicker.bind(this));
				}

				if (this.opts.classes) {
					this.$datepicker.addClass(this.opts.classes)
				}

				if (this.opts.timepicker) {
					this.timepicker = new $.fn.airDatepicker.Timepicker(this, this.opts);
					this._bindTimepickerEvents();
				}

				if (this.opts.onlyTimepicker) {
					this.$datepicker.addClass('-only-timepicker-');
				}

				this.views[this.currentView] = new $.fn.airDatepicker.Body(this, this.currentView, this.opts);
				this.views[this.currentView].show();
				this.nav = new $.fn.airDatepicker.Navigation(this, this.opts);
				this.view = this.currentView;

				this.$el.on('clickCell.adp', this._onClickCell.bind(this));
				this.$datepicker.on('mouseenter', '.datepicker--cell', this._onMouseEnterCell.bind(this));
				this.$datepicker.on('mouseleave', '.datepicker--cell', this._onMouseLeaveCell.bind(this));

				this.inited = true;
			},

			_createShortCuts: function () {
				this.minDate = this.opts.minDate ? this.opts.minDate : new Date(-8639999913600000);
				this.maxDate = this.opts.maxDate ? this.opts.maxDate : new Date(8639999913600000);
			},

			_bindEvents: function () {
				this.$el.on(this.opts.showEvent + '.adp', this._onShowEvent.bind(this));
				this.$el.on('mouseup.adp', this._onMouseUpEl.bind(this));
				this.$el.on('blur.adp', this._onBlur.bind(this));
				this.$el.on('keyup.adp', this._onKeyUpGeneral.bind(this));
				$(window).on('resize.adp', this._onResize.bind(this));
				$('body').on('mouseup.adp', this._onMouseUpBody.bind(this));
			},

			_bindKeyboardEvents: function () {
				this.$el.on('keydown.adp', this._onKeyDown.bind(this));
				this.$el.on('keyup.adp', this._onKeyUp.bind(this));
				this.$el.on('hotKey.adp', this._onHotKey.bind(this));
			},

			_bindTimepickerEvents: function () {
				this.$el.on('timeChange.adp', this._onTimeChange.bind(this));
			},

			isWeekend: function (day) {
				return this.opts.weekends.indexOf(day) !== -1;
			},

			_defineLocale: function (lang) {
				if (typeof lang == 'string') {
					this.loc = $.fn.airDatepicker.language[lang];
					if (!this.loc) {
						console.warn('Can\'t find language "' + lang + '" in Datepicker.language, will use "en" instead');
						this.loc = $.extend(true, {}, $.fn.airDatepicker.language.en)
					}

					this.loc = $.extend(true, {}, $.fn.airDatepicker.language.en, $.fn.airDatepicker.language[lang])
				} else {
					this.loc = $.extend(true, {}, $.fn.airDatepicker.language.en, lang)
				}

				if (this.opts.dateFormat) {
					this.loc.dateFormat = this.opts.dateFormat
				}

				if (this.opts.timeFormat) {
					this.loc.timeFormat = this.opts.timeFormat
				}

				if (this.opts.firstDay !== '') {
					this.loc.firstDay = this.opts.firstDay
				}

				if (this.opts.timepicker) {
					this.loc.dateFormat = [this.loc.dateFormat, this.loc.timeFormat].join(this.opts.dateTimeSeparator);
				}

				if (this.opts.onlyTimepicker) {
					this.loc.dateFormat = this.loc.timeFormat;
				}

				var boundary = this._getWordBoundaryRegExp;
				if (this.loc.timeFormat.match(boundary('aa')) ||
					this.loc.timeFormat.match(boundary('AA'))
				) {
					this.ampm = true;
				}
			},

			_buildDatepickersContainer: function () {
				containerBuilt = true;
				$body.append('<div class="datepickers-container" id="datepickers-container"></div>');
				$datepickersContainer = $('#datepickers-container');
			},

			_buildBaseHtml: function () {
				var $appendTarget,
					$inline = $('<div class="datepicker-inline">');

				if (this.el.nodeName == 'INPUT') {
					if (!this.opts.inline) {
						$appendTarget = $datepickersContainer;
					} else {
						$appendTarget = $inline.insertAfter(this.$el)
					}
				} else {
					$appendTarget = $inline.appendTo(this.$el)
				}

				this.$datepicker = $(baseTemplate).appendTo($appendTarget);
				this.$content = $('.datepicker--content', this.$datepicker);
				this.$nav = $('.datepicker--nav', this.$datepicker);
			},

			_triggerOnChange: function () {
				if (!this.selectedDates.length) {
					// Prevent from triggering multiple onSelect callback with same argument (empty string) in IE10-11
					if (this._prevOnSelectValue === '') return;
					this._prevOnSelectValue = '';
					return this.opts.onSelect('', '', this);
				}

				var selectedDates = this.selectedDates,
					parsedSelected = datepicker.getParsedDate(selectedDates[0]),
					formattedDates,
					_this = this,
					dates = new Date(
						parsedSelected.year,
						parsedSelected.month,
						parsedSelected.date,
						parsedSelected.hours,
						parsedSelected.minutes
					);

				formattedDates = selectedDates.map(function (date) {
					return _this.formatDate(_this.loc.dateFormat, date)
				}).join(this.opts.multipleDatesSeparator);

				// Create new dates array, to separate it from original selectedDates
				if (this.opts.multipleDates || this.opts.range) {
					dates = selectedDates.map(function (date) {
						var parsedDate = datepicker.getParsedDate(date);
						return new Date(
							parsedDate.year,
							parsedDate.month,
							parsedDate.date,
							parsedDate.hours,
							parsedDate.minutes
						);
					})
				}

				this._prevOnSelectValue = formattedDates;
				this.opts.onSelect(formattedDates, dates, this);
			},

			next: function () {
				var d = this.parsedDate,
					o = this.opts;
				switch (this.view) {
					case 'days':
						this.date = new Date(d.year, d.month + 1, 1);
						if (o.onChangeMonth) o.onChangeMonth(this.parsedDate.month, this.parsedDate.year);
						break;
					case 'months':
						this.date = new Date(d.year + 1, d.month, 1);
						if (o.onChangeYear) o.onChangeYear(this.parsedDate.year);
						break;
					case 'years':
						this.date = new Date(d.year + 10, 0, 1);
						if (o.onChangeDecade) o.onChangeDecade(this.curDecade);
						break;
				}
			},

			prev: function () {
				var d = this.parsedDate,
					o = this.opts;
				switch (this.view) {
					case 'days':
						this.date = new Date(d.year, d.month - 1, 1);
						if (o.onChangeMonth) o.onChangeMonth(this.parsedDate.month, this.parsedDate.year);
						break;
					case 'months':
						this.date = new Date(d.year - 1, d.month, 1);
						if (o.onChangeYear) o.onChangeYear(this.parsedDate.year);
						break;
					case 'years':
						this.date = new Date(d.year - 10, 0, 1);
						if (o.onChangeDecade) o.onChangeDecade(this.curDecade);
						break;
				}
			},

			formatDate: function (string, date) {
				date = date || this.date;
				var result = string,
					boundary = this._getWordBoundaryRegExp,
					locale = this.loc,
					leadingZero = datepicker.getLeadingZeroNum,
					decade = datepicker.getDecade(date),
					d = datepicker.getParsedDate(date),
					fullHours = d.fullHours,
					hours = d.hours,
					ampm = string.match(boundary('aa')) || string.match(boundary('AA')),
					dayPeriod = 'am',
					replacer = this._replacer,
					validHours;

				if (this.opts.timepicker && this.timepicker && ampm) {
					validHours = this.timepicker._getValidHoursFromDate(date, ampm);
					fullHours = leadingZero(validHours.hours);
					hours = validHours.hours;
					dayPeriod = validHours.dayPeriod;
				}

				switch (true) {
					case /@/.test(result):
						result = result.replace(/@/, date.getTime());
					case /aa/.test(result):
						result = replacer(result, boundary('aa'), dayPeriod);
					case /AA/.test(result):
						result = replacer(result, boundary('AA'), dayPeriod.toUpperCase());
					case /dd/.test(result):
						result = replacer(result, boundary('dd'), d.fullDate);
					case /d/.test(result):
						result = replacer(result, boundary('d'), d.date);
					case /DD/.test(result):
						result = replacer(result, boundary('DD'), locale.days[d.day]);
					case /D/.test(result):
						result = replacer(result, boundary('D'), locale.daysShort[d.day]);
					case /mm/.test(result):
						result = replacer(result, boundary('mm'), d.fullMonth);
					case /m/.test(result):
						result = replacer(result, boundary('m'), d.month + 1);
					case /MM/.test(result):
						result = replacer(result, boundary('MM'), this.loc.months[d.month]);
					case /M/.test(result):
						result = replacer(result, boundary('M'), locale.monthsShort[d.month]);
					case /ii/.test(result):
						result = replacer(result, boundary('ii'), d.fullMinutes);
					case /i/.test(result):
						result = replacer(result, boundary('i'), d.minutes);
					case /hh/.test(result):
						result = replacer(result, boundary('hh'), fullHours);
					case /h/.test(result):
						result = replacer(result, boundary('h'), hours);
					case /yy/.test(result):
						result = replacer(result, boundary('yy'), d.year);
					case /yy1/.test(result):
						result = replacer(result, boundary('yy1'), decade[0]);
					case /yy2/.test(result):
						result = replacer(result, boundary('yy2'), decade[1]);
					case /y/.test(result):
						result = replacer(result, boundary('y'), d.year.toString().slice(-2));
				}

				return result;
			},

			_replacer: function (str, reg, data) {
				return str.replace(reg, function (match, p1, p2, p3) {
					return p1 + data + p3;
				})
			},

			_getWordBoundaryRegExp: function (sign) {
				var symbols = '\\s|\\.|-|/|\\\\|,|\\$|\\!|\\?|:|;';

				return new RegExp('(^|>|' + symbols + ')(' + sign + ')($|<|' + symbols + ')', 'g');
			},


			selectDate: function (date) {
				var _this = this,
					opts = _this.opts,
					d = _this.parsedDate,
					selectedDates = _this.selectedDates,
					len = selectedDates.length,
					newDate = '';

				if (Array.isArray(date)) {
					date.forEach(function (d) {
						_this.selectDate(d)
					});
					return;
				}

				if (!(date instanceof Date)) return;

				this.lastSelectedDate = date;

				// Set new time values from Date
				if (this.timepicker) {
					this.timepicker._setTime(date);
				}

				// On this step timepicker will set valid values in it's instance
				_this._trigger('selectDate', date);

				// Set correct time values after timepicker's validation
				// Prevent from setting hours or minutes which values are lesser then `min` value or
				// greater then `max` value
				if (this.timepicker) {
					date.setHours(this.timepicker.hours);
					date.setMinutes(this.timepicker.minutes)
				}

				if (_this.view == 'days') {
					if (date.getMonth() != d.month && opts.moveToOtherMonthsOnSelect) {
						newDate = new Date(date.getFullYear(), date.getMonth(), 1);
					}
				}

				if (_this.view == 'years') {
					if (date.getFullYear() != d.year && opts.moveToOtherYearsOnSelect) {
						newDate = new Date(date.getFullYear(), 0, 1);
					}
				}

				if (newDate) {
					_this.silent = true;
					_this.date = newDate;
					_this.silent = false;
					_this.nav._render()
				}

				if (opts.multipleDates && !opts.range) { // Set priority to range functionality
					if (len === opts.multipleDates) return;
					if (!_this._isSelected(date)) {
						_this.selectedDates.push(date);
					}
				} else if (opts.range) {
					if (len == 2) {
						_this.selectedDates = [date];
						_this.minRange = date;
						_this.maxRange = '';
					} else if (len == 1) {
						_this.selectedDates.push(date);
						if (!_this.maxRange) {
							_this.maxRange = date;
						} else {
							_this.minRange = date;
						}
						// Swap dates if they were selected via dp.selectDate() and second date was smaller then first
						if (datepicker.bigger(_this.maxRange, _this.minRange)) {
							_this.maxRange = _this.minRange;
							_this.minRange = date;
						}
						_this.selectedDates = [_this.minRange, _this.maxRange]

					} else {
						_this.selectedDates = [date];
						_this.minRange = date;
					}
				} else {
					_this.selectedDates = [date];
				}

				_this._setInputValue();

				if (opts.onSelect) {
					_this._triggerOnChange();
				}

				if (opts.autoClose && !this.timepickerIsActive) {
					if (!opts.multipleDates && !opts.range) {
						_this.hide();
					} else if (opts.range && _this.selectedDates.length == 2) {
						_this.hide();
					}
				}

				_this.views[this.currentView]._render()
			},

			removeDate: function (date) {
				var selected = this.selectedDates,
					_this = this;

				if (!(date instanceof Date)) return;

				return selected.some(function (curDate, i) {
					if (datepicker.isSame(curDate, date)) {
						selected.splice(i, 1);

						if (!_this.selectedDates.length) {
							_this.minRange = '';
							_this.maxRange = '';
							_this.lastSelectedDate = '';
						} else {
							_this.lastSelectedDate = _this.selectedDates[_this.selectedDates.length - 1];
						}

						_this.views[_this.currentView]._render();
						_this._setInputValue();

						if (_this.opts.onSelect) {
							_this._triggerOnChange();
						}

						return true
					}
				})
			},

			today: function () {
				this.silent = true;
				this.view = this.opts.minView;
				this.silent = false;
				this.date = new Date();

				if (this.opts.todayButton instanceof Date) {
					this.selectDate(this.opts.todayButton)
				}
			},

			clear: function () {
				this.selectedDates = [];
				this.minRange = '';
				this.maxRange = '';
				this.views[this.currentView]._render();
				this._setInputValue();
				if (this.opts.onSelect) {
					this._triggerOnChange()
				}
			},

			/**
			 * Updates datepicker options
			 * @param {String|Object} param - parameter's name to update. If object then it will extend current options
			 * @param {String|Number|Object} [value] - new param value
			 */
			update: function (param, value) {
				var len = arguments.length,
					lastSelectedDate = this.lastSelectedDate;

				if (len == 2) {
					this.opts[param] = value;
				} else if (len == 1 && typeof param == 'object') {
					this.opts = $.extend(true, this.opts, param)
				}

				this._createShortCuts();
				this._syncWithMinMaxDates();
				this._defineLocale(this.opts.language);
				this.nav._addButtonsIfNeed();
				if (!this.opts.onlyTimepicker) this.nav._render();
				this.views[this.currentView]._render();

				if (this.elIsInput && !this.opts.inline) {
					this._setPositionClasses(this.opts.position);
					if (this.visible) {
						this.setPosition(this.opts.position)
					}
				}

				if (this.opts.classes) {
					this.$datepicker.addClass(this.opts.classes)
				}

				if (this.opts.onlyTimepicker) {
					this.$datepicker.addClass('-only-timepicker-');
				}

				if (this.opts.timepicker) {
					if (lastSelectedDate) this.timepicker._handleDate(lastSelectedDate);
					this.timepicker._updateRanges();
					this.timepicker._updateCurrentTime();
					// Change hours and minutes if it's values have been changed through min/max hours/minutes
					if (lastSelectedDate) {
						lastSelectedDate.setHours(this.timepicker.hours);
						lastSelectedDate.setMinutes(this.timepicker.minutes);
					}
				}

				this._setInputValue();

				return this;
			},

			_syncWithMinMaxDates: function () {
				var curTime = this.date.getTime();
				this.silent = true;
				if (this.minTime > curTime) {
					this.date = this.minDate;
				}

				if (this.maxTime < curTime) {
					this.date = this.maxDate;
				}
				this.silent = false;
			},

			_isSelected: function (checkDate, cellType) {
				var res = false;
				this.selectedDates.some(function (date) {
					if (datepicker.isSame(date, checkDate, cellType)) {
						res = date;
						return true;
					}
				});
				return res;
			},

			_setInputValue: function () {
				var _this = this,
					opts = _this.opts,
					format = _this.loc.dateFormat,
					altFormat = opts.altFieldDateFormat,
					value = _this.selectedDates.map(function (date) {
						return _this.formatDate(format, date)
					}),
					altValues;

				if (opts.altField && _this.$altField.length) {
					altValues = this.selectedDates.map(function (date) {
						return _this.formatDate(altFormat, date)
					});
					altValues = altValues.join(this.opts.multipleDatesSeparator);
					this.$altField.val(altValues);
				}

				value = value.join(this.opts.multipleDatesSeparator);

				this.$el.val(value)
			},

			/**
			 * Check if date is between minDate and maxDate
			 * @param date {object} - date object
			 * @param type {string} - cell type
			 * @returns {boolean}
			 * @private
			 */
			_isInRange: function (date, type) {
				var time = date.getTime(),
					d = datepicker.getParsedDate(date),
					min = datepicker.getParsedDate(this.minDate),
					max = datepicker.getParsedDate(this.maxDate),
					dMinTime = new Date(d.year, d.month, min.date).getTime(),
					dMaxTime = new Date(d.year, d.month, max.date).getTime(),
					types = {
						day: time >= this.minTime && time <= this.maxTime,
						month: dMinTime >= this.minTime && dMaxTime <= this.maxTime,
						year: d.year >= min.year && d.year <= max.year
					};
				return type ? types[type] : types.day
			},

			_getDimensions: function ($el) {
				var offset = $el.offset();

				return {
					width: $el.outerWidth(),
					height: $el.outerHeight(),
					left: offset.left,
					top: offset.top
				}
			},

			_getDateFromCell: function (cell) {
				var curDate = this.parsedDate,
					year = cell.data('year') || curDate.year,
					month = cell.data('month') == undefined ? curDate.month : cell.data('month'),
					date = cell.data('date') || 1;

				return new Date(year, month, date);
			},

			_setPositionClasses: function (pos) {
				pos = pos.split(' ');
				var main = pos[0],
					sec = pos[1],
					classes = 'datepicker -' + main + '-' + sec + '- -from-' + main + '-';

				if (this.visible) classes += ' active';

				this.$datepicker
					.removeAttr('class')
					.addClass(classes);
			},

			setPosition: function (position) {
				position = position || this.opts.position;

				var dims = this._getDimensions(this.$el),
					selfDims = this._getDimensions(this.$datepicker),
					pos = position.split(' '),
					top, left,
					offset = this.opts.offset,
					main = pos[0],
					secondary = pos[1];

				switch (main) {
					case 'top':
						top = dims.top - selfDims.height - offset;
						break;
					case 'right':
						left = dims.left + dims.width + offset;
						break;
					case 'bottom':
						top = dims.top + dims.height + offset;
						break;
					case 'left':
						left = dims.left - selfDims.width - offset;
						break;
				}

				switch (secondary) {
					case 'top':
						top = dims.top;
						break;
					case 'right':
						left = dims.left + dims.width - selfDims.width;
						break;
					case 'bottom':
						top = dims.top + dims.height - selfDims.height;
						break;
					case 'left':
						left = dims.left;
						break;
					case 'center':
						if (/left|right/.test(main)) {
							top = dims.top + dims.height / 2 - selfDims.height / 2;
						} else {
							left = dims.left + dims.width / 2 - selfDims.width / 2;
						}
				}

				this.$datepicker
					.css({
						left: left,
						top: top
					})
			},

			show: function () {
				var onShow = this.opts.onShow;

				this.setPosition(this.opts.position);
				this.$datepicker.addClass('active');
				this.visible = true;

				if (onShow) {
					this._bindVisionEvents(onShow)
				}
			},

			hide: function () {
				var onHide = this.opts.onHide;

				this.$datepicker
					.removeClass('active')
					.css({
						left: '-100000px'
					});

				this.focused = '';
				this.keys = [];

				this.inFocus = false;
				this.visible = false;
				this.$el.blur();

				if (onHide) {
					this._bindVisionEvents(onHide)
				}
			},

			down: function (date) {
				this._changeView(date, 'down');
			},

			up: function (date) {
				this._changeView(date, 'up');
			},

			_bindVisionEvents: function (event) {
				this.$datepicker.off('transitionend.dp');
				event(this, false);
				this.$datepicker.one('transitionend.dp', event.bind(this, this, true))
			},

			_changeView: function (date, dir) {
				date = date || this.focused || this.date;

				var nextView = dir == 'up' ? this.viewIndex + 1 : this.viewIndex - 1;
				if (nextView > 2) nextView = 2;
				if (nextView < 0) nextView = 0;

				this.silent = true;
				this.date = new Date(date.getFullYear(), date.getMonth(), 1);
				this.silent = false;
				this.view = this.viewIndexes[nextView];

			},

			_handleHotKey: function (key) {
				var date = datepicker.getParsedDate(this._getFocusedDate()),
					focusedParsed,
					o = this.opts,
					newDate,
					totalDaysInNextMonth,
					monthChanged = false,
					yearChanged = false,
					decadeChanged = false,
					y = date.year,
					m = date.month,
					d = date.date;

				switch (key) {
					case 'ctrlRight':
					case 'ctrlUp':
						m += 1;
						monthChanged = true;
						break;
					case 'ctrlLeft':
					case 'ctrlDown':
						m -= 1;
						monthChanged = true;
						break;
					case 'shiftRight':
					case 'shiftUp':
						yearChanged = true;
						y += 1;
						break;
					case 'shiftLeft':
					case 'shiftDown':
						yearChanged = true;
						y -= 1;
						break;
					case 'altRight':
					case 'altUp':
						decadeChanged = true;
						y += 10;
						break;
					case 'altLeft':
					case 'altDown':
						decadeChanged = true;
						y -= 10;
						break;
					case 'ctrlShiftUp':
						this.up();
						break;
				}

				totalDaysInNextMonth = datepicker.getDaysCount(new Date(y, m));
				newDate = new Date(y, m, d);

				// If next month has less days than current, set date to total days in that month
				if (totalDaysInNextMonth < d) d = totalDaysInNextMonth;

				// Check if newDate is in valid range
				if (newDate.getTime() < this.minTime) {
					newDate = this.minDate;
				} else if (newDate.getTime() > this.maxTime) {
					newDate = this.maxDate;
				}

				this.focused = newDate;

				focusedParsed = datepicker.getParsedDate(newDate);
				if (monthChanged && o.onChangeMonth) {
					o.onChangeMonth(focusedParsed.month, focusedParsed.year)
				}
				if (yearChanged && o.onChangeYear) {
					o.onChangeYear(focusedParsed.year)
				}
				if (decadeChanged && o.onChangeDecade) {
					o.onChangeDecade(this.curDecade)
				}
			},

			_registerKey: function (key) {
				var exists = this.keys.some(function (curKey) {
					return curKey == key;
				});

				if (!exists) {
					this.keys.push(key)
				}
			},

			_unRegisterKey: function (key) {
				var index = this.keys.indexOf(key);

				this.keys.splice(index, 1);
			},

			_isHotKeyPressed: function () {
				var currentHotKey,
					found = false,
					_this = this,
					pressedKeys = this.keys.sort();

				for (var hotKey in hotKeys) {
					currentHotKey = hotKeys[hotKey];
					if (pressedKeys.length != currentHotKey.length) continue;

					if (currentHotKey.every(function (key, i) { return key == pressedKeys[i] })) {
						_this._trigger('hotKey', hotKey);
						found = true;
					}
				}

				return found;
			},

			_trigger: function (event, args) {
				this.$el.trigger(event, args)
			},

			_focusNextCell: function (keyCode, type) {
				type = type || this.cellType;

				var date = datepicker.getParsedDate(this._getFocusedDate()),
					y = date.year,
					m = date.month,
					d = date.date;

				if (this._isHotKeyPressed()) {
					return;
				}

				switch (keyCode) {
					case 37: // left
						type == 'day' ? (d -= 1) : '';
						type == 'month' ? (m -= 1) : '';
						type == 'year' ? (y -= 1) : '';
						break;
					case 38: // up
						type == 'day' ? (d -= 7) : '';
						type == 'month' ? (m -= 3) : '';
						type == 'year' ? (y -= 4) : '';
						break;
					case 39: // right
						type == 'day' ? (d += 1) : '';
						type == 'month' ? (m += 1) : '';
						type == 'year' ? (y += 1) : '';
						break;
					case 40: // down
						type == 'day' ? (d += 7) : '';
						type == 'month' ? (m += 3) : '';
						type == 'year' ? (y += 4) : '';
						break;
				}

				var nd = new Date(y, m, d);
				if (nd.getTime() < this.minTime) {
					nd = this.minDate;
				} else if (nd.getTime() > this.maxTime) {
					nd = this.maxDate;
				}

				this.focused = nd;

			},

			_getFocusedDate: function () {
				var focused = this.focused || this.selectedDates[this.selectedDates.length - 1],
					d = this.parsedDate;

				if (!focused) {
					switch (this.view) {
						case 'days':
							focused = new Date(d.year, d.month, new Date().getDate());
							break;
						case 'months':
							focused = new Date(d.year, d.month, 1);
							break;
						case 'years':
							focused = new Date(d.year, 0, 1);
							break;
					}
				}

				return focused;
			},

			_getCell: function (date, type) {
				type = type || this.cellType;

				var d = datepicker.getParsedDate(date),
					selector = '.datepicker--cell[data-year="' + d.year + '"]',
					$cell;

				switch (type) {
					case 'month':
						selector = '[data-month="' + d.month + '"]';
						break;
					case 'day':
						selector += '[data-month="' + d.month + '"][data-date="' + d.date + '"]';
						break;
				}
				$cell = this.views[this.currentView].$el.find(selector);

				return $cell.length ? $cell : $('');
			},

			destroy: function () {
				var _this = this;
				_this.$el
					.off('.adp')
					.data('datepicker', '');

				_this.selectedDates = [];
				_this.focused = '';
				_this.views = {};
				_this.keys = [];
				_this.minRange = '';
				_this.maxRange = '';

				if (_this.opts.inline || !_this.elIsInput) {
					_this.$datepicker.closest('.datepicker-inline').remove();
				} else {
					_this.$datepicker.remove();
				}
			},

			_handleAlreadySelectedDates: function (alreadySelected, selectedDate) {
				if (this.opts.range) {
					if (!this.opts.toggleSelected) {
						// Add possibility to select same date when range is true
						if (this.selectedDates.length != 2) {
							this._trigger('clickCell', selectedDate);
						}
					} else {
						this.removeDate(selectedDate);
					}
				} else if (this.opts.toggleSelected) {
					this.removeDate(selectedDate);
				}

				// Change last selected date to be able to change time when clicking on this cell
				if (!this.opts.toggleSelected) {
					this.lastSelectedDate = alreadySelected;
					if (this.opts.timepicker) {
						this.timepicker._setTime(alreadySelected);
						this.timepicker.update();
					}
				}
			},

			_onShowEvent: function (e) {
				if (!this.visible) {
					this.show();
				}
			},

			_onBlur: function () {
				if (!this.inFocus && this.visible) {
					this.hide();
				}
			},

			_onMouseDownDatepicker: function (e) {
				this.inFocus = true;
			},

			_onMouseUpDatepicker: function (e) {
				this.inFocus = false;
				e.originalEvent.inFocus = true;
				if (!e.originalEvent.timepickerFocus) this.$el.focus();
			},

			_onKeyUpGeneral: function (e) {
				var val = this.$el.val();

				if (!val) {
					this.clear();
				}
			},

			_onResize: function () {
				if (this.visible) {
					this.setPosition();
				}
			},

			_onMouseUpBody: function (e) {
				if (e.originalEvent.inFocus) return;

				if (this.visible && !this.inFocus) {
					this.hide();
				}
			},

			_onMouseUpEl: function (e) {
				e.originalEvent.inFocus = true;
				setTimeout(this._onKeyUpGeneral.bind(this), 4);
			},

			_onKeyDown: function (e) {
				var code = e.which;
				this._registerKey(code);

				// Arrows
				if (code >= 37 && code <= 40) {
					e.preventDefault();
					this._focusNextCell(code);
				}

				// Enter
				if (code == 13) {
					if (this.focused) {
						if (this._getCell(this.focused).hasClass('-disabled-')) return;
						if (this.view != this.opts.minView) {
							this.down()
						} else {
							var alreadySelected = this._isSelected(this.focused, this.cellType);

							if (!alreadySelected) {
								if (this.timepicker) {
									this.focused.setHours(this.timepicker.hours);
									this.focused.setMinutes(this.timepicker.minutes);
								}
								this.selectDate(this.focused);
								return;
							}
							this._handleAlreadySelectedDates(alreadySelected, this.focused)
						}
					}
				}

				// Esc
				if (code == 27) {
					this.hide();
				}
			},

			_onKeyUp: function (e) {
				var code = e.which;
				this._unRegisterKey(code);
			},

			_onHotKey: function (e, hotKey) {
				this._handleHotKey(hotKey);
			},

			_onMouseEnterCell: function (e) {
				var $cell = $(e.target).closest('.datepicker--cell'),
					date = this._getDateFromCell($cell);

				// Prevent from unnecessary rendering and setting new currentDate
				this.silent = true;

				if (this.focused) {
					this.focused = ''
				}

				$cell.addClass('-focus-');

				this.focused = date;
				this.silent = false;

				if (this.opts.range && this.selectedDates.length == 1) {
					this.minRange = this.selectedDates[0];
					this.maxRange = '';
					if (datepicker.less(this.minRange, this.focused)) {
						this.maxRange = this.minRange;
						this.minRange = '';
					}
					this.views[this.currentView]._update();
				}
			},

			_onMouseLeaveCell: function (e) {
				var $cell = $(e.target).closest('.datepicker--cell');

				$cell.removeClass('-focus-');

				this.silent = true;
				this.focused = '';
				this.silent = false;
			},

			_onTimeChange: function (e, h, m) {
				var date = new Date(),
					selectedDates = this.selectedDates,
					selected = false;

				if (selectedDates.length) {
					selected = true;
					date = this.lastSelectedDate;
				}

				date.setHours(h);
				date.setMinutes(m);

				if (!selected && !this._getCell(date).hasClass('-disabled-')) {
					this.selectDate(date);
				} else {
					this._setInputValue();
					if (this.opts.onSelect) {
						this._triggerOnChange();
					}
				}
			},

			_onClickCell: function (e, date) {
				if (this.timepicker) {
					date.setHours(this.timepicker.hours);
					date.setMinutes(this.timepicker.minutes);
				}
				this.selectDate(date);
			},

			set focused(val) {
				if (!val && this.focused) {
					var $cell = this._getCell(this.focused);

					if ($cell.length) {
						$cell.removeClass('-focus-')
					}
				}
				this._focused = val;
				if (this.opts.range && this.selectedDates.length == 1) {
					this.minRange = this.selectedDates[0];
					this.maxRange = '';
					if (datepicker.less(this.minRange, this._focused)) {
						this.maxRange = this.minRange;
						this.minRange = '';
					}
				}
				if (this.silent) return;
				this.date = val;
			},

			get focused() {
				return this._focused;
			},

			get parsedDate() {
				return datepicker.getParsedDate(this.date);
			},

			set date(val) {
				if (!(val instanceof Date)) return;

				this.currentDate = val;

				if (this.inited && !this.silent) {
					this.views[this.view]._render();
					this.nav._render();
					if (this.visible && this.elIsInput) {
						this.setPosition();
					}
				}
				return val;
			},

			get date() {
				return this.currentDate
			},

			set view(val) {
				this.viewIndex = this.viewIndexes.indexOf(val);

				if (this.viewIndex < 0) {
					return;
				}

				this.prevView = this.currentView;
				this.currentView = val;

				if (this.inited) {
					if (!this.views[val]) {
						this.views[val] = new $.fn.airDatepicker.Body(this, val, this.opts)
					} else {
						this.views[val]._render();
					}

					this.views[this.prevView].hide();
					this.views[val].show();
					this.nav._render();

					if (this.opts.onChangeView) {
						this.opts.onChangeView(val)
					}
					if (this.elIsInput && this.visible) this.setPosition();
				}

				return val
			},

			get view() {
				return this.currentView;
			},

			get cellType() {
				return this.view.substring(0, this.view.length - 1)
			},

			get minTime() {
				var min = datepicker.getParsedDate(this.minDate);
				return new Date(min.year, min.month, min.date).getTime()
			},

			get maxTime() {
				var max = datepicker.getParsedDate(this.maxDate);
				return new Date(max.year, max.month, max.date).getTime()
			},

			get curDecade() {
				return datepicker.getDecade(this.date)
			}
		};

		//  Utils
		// -------------------------------------------------

		datepicker.getDaysCount = function (date) {
			return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
		};

		datepicker.getParsedDate = function (date) {
			return {
				year: date.getFullYear(),
				month: date.getMonth(),
				fullMonth: (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1, // One based
				date: date.getDate(),
				fullDate: date.getDate() < 10 ? '0' + date.getDate() : date.getDate(),
				day: date.getDay(),
				hours: date.getHours(),
				fullHours: date.getHours() < 10 ? '0' + date.getHours() : date.getHours(),
				minutes: date.getMinutes(),
				fullMinutes: date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
			}
		};

		datepicker.getDecade = function (date) {
			var firstYear = Math.floor(date.getFullYear() / 10) * 10;

			return [firstYear, firstYear + 9];
		};

		datepicker.template = function (str, data) {
			return str.replace(/#\{([\w]+)\}/g, function (source, match) {
				if (data[match] || data[match] === 0) {
					return data[match]
				}
			});
		};

		datepicker.isSame = function (date1, date2, type) {
			if (!date1 || !date2) return false;
			var d1 = datepicker.getParsedDate(date1),
				d2 = datepicker.getParsedDate(date2),
				_type = type ? type : 'day',

				conditions = {
					day: d1.date == d2.date && d1.month == d2.month && d1.year == d2.year,
					month: d1.month == d2.month && d1.year == d2.year,
					year: d1.year == d2.year
				};

			return conditions[_type];
		};

		datepicker.less = function (dateCompareTo, date, type) {
			if (!dateCompareTo || !date) return false;
			return date.getTime() < dateCompareTo.getTime();
		};

		datepicker.bigger = function (dateCompareTo, date, type) {
			if (!dateCompareTo || !date) return false;
			return date.getTime() > dateCompareTo.getTime();
		};

		datepicker.getLeadingZeroNum = function (num) {
			return parseInt(num) < 10 ? '0' + num : num;
		};

		/**
		 * Returns copy of date with hours and minutes equals to 0
		 * @param date {Date}
		 */
		datepicker.resetTime = function (date) {
			if (typeof date != 'object') return;
			date = datepicker.getParsedDate(date);
			return new Date(date.year, date.month, date.date)
		};

		$.fn.airDatepicker = function (options) {
			return this.each(function () {
				if (!$.data(this, pluginName)) {
					$.data(this, pluginName,
						new Datepicker(this, options));
				} else {
					var _this = $.data(this, pluginName);

					_this.opts = $.extend(true, _this.opts, options);
					_this.update();
				}
			});
		};

		$.fn.airDatepicker.Constructor = Datepicker;

		$.fn.airDatepicker.language = {
			en: {
				days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
				daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
				daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
				months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
				monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
				today: 'Today',
				clear: 'Clear',
				dateFormat: 'mm/dd/yy',
				timeFormat: 'hh:ii aa',
				firstDay: 0
			}
		};

		$(function () {
			$(autoInitSelector).datepicker();
		})

	})();

	; (function () {
		var templates = {
			days: '' +
				'<div class="datepicker--days datepicker--body">' +
				'<div class="datepicker--days-names"></div>' +
				'<div class="datepicker--cells datepicker--cells-days"></div>' +
				'</div>',
			months: '' +
				'<div class="datepicker--months datepicker--body">' +
				'<div class="datepicker--cells datepicker--cells-months"></div>' +
				'</div>',
			years: '' +
				'<div class="datepicker--years datepicker--body">' +
				'<div class="datepicker--cells datepicker--cells-years"></div>' +
				'</div>'
		},
			datepicker = $.fn.airDatepicker,
			dp = datepicker.Constructor;

		datepicker.Body = function (d, type, opts) {
			this.d = d;
			this.type = type;
			this.opts = opts;
			this.$el = $('');

			if (this.opts.onlyTimepicker) return;
			this.init();
		};

		datepicker.Body.prototype = {
			init: function () {
				this._buildBaseHtml();
				this._render();

				this._bindEvents();
			},

			_bindEvents: function () {
				this.$el.on('click', '.datepicker--cell', $.proxy(this._onClickCell, this));
			},

			_buildBaseHtml: function () {
				this.$el = $(templates[this.type]).appendTo(this.d.$content);
				this.$names = $('.datepicker--days-names', this.$el);
				this.$cells = $('.datepicker--cells', this.$el);
			},

			_getDayNamesHtml: function (firstDay, curDay, html, i) {
				curDay = curDay != undefined ? curDay : firstDay;
				html = html ? html : '';
				i = i != undefined ? i : 0;

				if (i > 7) return html;
				if (curDay == 7) return this._getDayNamesHtml(firstDay, 0, html, ++i);

				html += '<div class="datepicker--day-name' + (this.d.isWeekend(curDay) ? " -weekend-" : "") + '">' + this.d.loc.daysMin[curDay] + '</div>';

				return this._getDayNamesHtml(firstDay, ++curDay, html, ++i);
			},

			_getCellContents: function (date, type) {
				var classes = "datepicker--cell datepicker--cell-" + type,
					currentDate = new Date(),
					parent = this.d,
					minRange = dp.resetTime(parent.minRange),
					maxRange = dp.resetTime(parent.maxRange),
					opts = parent.opts,
					d = dp.getParsedDate(date),
					render = {},
					html = d.date;

				switch (type) {
					case 'day':
						if (parent.isWeekend(d.day)) classes += " -weekend-";
						if (d.month != this.d.parsedDate.month) {
							classes += " -other-month-";
							if (!opts.selectOtherMonths) {
								classes += " -disabled-";
							}
							if (!opts.showOtherMonths) html = '';
						}
						break;
					case 'month':
						html = parent.loc[parent.opts.monthsField][d.month];
						break;
					case 'year':
						var decade = parent.curDecade;
						html = d.year;
						if (d.year < decade[0] || d.year > decade[1]) {
							classes += ' -other-decade-';
							if (!opts.selectOtherYears) {
								classes += " -disabled-";
							}
							if (!opts.showOtherYears) html = '';
						}
						break;
				}

				if (opts.onRenderCell) {
					render = opts.onRenderCell(date, type) || {};
					html = render.html ? render.html : html;
					classes += render.classes ? ' ' + render.classes : '';
				}

				if (opts.range) {
					if (dp.isSame(minRange, date, type)) classes += ' -range-from-';
					if (dp.isSame(maxRange, date, type)) classes += ' -range-to-';

					if (parent.selectedDates.length == 1 && parent.focused) {
						if (
							(dp.bigger(minRange, date) && dp.less(parent.focused, date)) ||
							(dp.less(maxRange, date) && dp.bigger(parent.focused, date))) {
							classes += ' -in-range-'
						}

						if (dp.less(maxRange, date) && dp.isSame(parent.focused, date)) {
							classes += ' -range-from-'
						}
						if (dp.bigger(minRange, date) && dp.isSame(parent.focused, date)) {
							classes += ' -range-to-'
						}

					} else if (parent.selectedDates.length == 2) {
						if (dp.bigger(minRange, date) && dp.less(maxRange, date)) {
							classes += ' -in-range-'
						}
					}
				}


				if (dp.isSame(currentDate, date, type)) classes += ' -current-';
				if (parent.focused && dp.isSame(date, parent.focused, type)) classes += ' -focus-';
				if (parent._isSelected(date, type)) classes += ' -selected-';
				if (!parent._isInRange(date, type) || render.disabled) classes += ' -disabled-';

				return {
					html: html,
					classes: classes
				}
			},

			/**
			 * Calculates days number to render. Generates days html and returns it.
			 * @param {object} date - Date object
			 * @returns {string}
			 * @private
			 */
			_getDaysHtml: function (date) {
				var totalMonthDays = dp.getDaysCount(date),
					firstMonthDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay(),
					lastMonthDay = new Date(date.getFullYear(), date.getMonth(), totalMonthDays).getDay(),
					daysFromPevMonth = firstMonthDay - this.d.loc.firstDay,
					daysFromNextMonth = 6 - lastMonthDay + this.d.loc.firstDay;

				daysFromPevMonth = daysFromPevMonth < 0 ? daysFromPevMonth + 7 : daysFromPevMonth;
				daysFromNextMonth = daysFromNextMonth > 6 ? daysFromNextMonth - 7 : daysFromNextMonth;

				var startDayIndex = -daysFromPevMonth + 1,
					m, y,
					html = '';

				for (var i = startDayIndex, max = totalMonthDays + daysFromNextMonth; i <= max; i++) {
					y = date.getFullYear();
					m = date.getMonth();

					html += this._getDayHtml(new Date(y, m, i))
				}

				return html;
			},

			_getDayHtml: function (date) {
				var content = this._getCellContents(date, 'day');

				return '<div class="' + content.classes + '" ' +
					'data-date="' + date.getDate() + '" ' +
					'data-month="' + date.getMonth() + '" ' +
					'data-year="' + date.getFullYear() + '">' + content.html + '</div>';
			},

			/**
			 * Generates months html
			 * @param {object} date - date instance
			 * @returns {string}
			 * @private
			 */
			_getMonthsHtml: function (date) {
				var html = '',
					d = dp.getParsedDate(date),
					i = 0;

				while (i < 12) {
					html += this._getMonthHtml(new Date(d.year, i));
					i++
				}

				return html;
			},

			_getMonthHtml: function (date) {
				var content = this._getCellContents(date, 'month');

				return '<div class="' + content.classes + '" data-month="' + date.getMonth() + '">' + content.html + '</div>'
			},

			_getYearsHtml: function (date) {
				var d = dp.getParsedDate(date),
					decade = dp.getDecade(date),
					firstYear = decade[0] - 1,
					html = '',
					i = firstYear;

				for (i; i <= decade[1] + 1; i++) {
					html += this._getYearHtml(new Date(i, 0));
				}

				return html;
			},

			_getYearHtml: function (date) {
				var content = this._getCellContents(date, 'year');

				return '<div class="' + content.classes + '" data-year="' + date.getFullYear() + '">' + content.html + '</div>'
			},

			_renderTypes: {
				days: function () {
					var dayNames = this._getDayNamesHtml(this.d.loc.firstDay),
						days = this._getDaysHtml(this.d.currentDate);

					this.$cells.html(days);
					this.$names.html(dayNames)
				},
				months: function () {
					var html = this._getMonthsHtml(this.d.currentDate);

					this.$cells.html(html)
				},
				years: function () {
					var html = this._getYearsHtml(this.d.currentDate);

					this.$cells.html(html)
				}
			},

			_render: function () {
				if (this.opts.onlyTimepicker) return;
				this._renderTypes[this.type].bind(this)();
			},

			_update: function () {
				var $cells = $('.datepicker--cell', this.$cells),
					_this = this,
					classes,
					$cell,
					date;
				$cells.each(function (cell, i) {
					$cell = $(this);
					date = _this.d._getDateFromCell($(this));
					classes = _this._getCellContents(date, _this.d.cellType);
					$cell.attr('class', classes.classes)
				});
			},

			show: function () {
				if (this.opts.onlyTimepicker) return;
				this.$el.addClass('active');
				this.acitve = true;
			},

			hide: function () {
				this.$el.removeClass('active');
				this.active = false;
			},

			//  Events
			// -------------------------------------------------

			_handleClick: function (el) {
				var date = el.data('date') || 1,
					month = el.data('month') || 0,
					year = el.data('year') || this.d.parsedDate.year,
					dp = this.d;
				// Change view if min view does not reach yet
				if (dp.view != this.opts.minView) {
					dp.down(new Date(year, month, date));
					return;
				}
				// Select date if min view is reached
				var selectedDate = new Date(year, month, date),
					alreadySelected = this.d._isSelected(selectedDate, this.d.cellType);

				if (!alreadySelected) {
					dp._trigger('clickCell', selectedDate);
					return;
				}

				dp._handleAlreadySelectedDates.bind(dp, alreadySelected, selectedDate)();

			},

			_onClickCell: function (e) {
				var $el = $(e.target).closest('.datepicker--cell');

				if ($el.hasClass('-disabled-')) return;

				this._handleClick.bind(this)($el);
			}
		};
	})();

	; (function () {
		var template = '' +
			'<div class="datepicker--nav-action" data-action="prev">#{prevHtml}</div>' +
			'<div class="datepicker--nav-title">#{title}</div>' +
			'<div class="datepicker--nav-action" data-action="next">#{nextHtml}</div>',
			buttonsContainerTemplate = '<div class="datepicker--buttons"></div>',
			button = '<span class="datepicker--button" data-action="#{action}">#{label}</span>',
			datepicker = $.fn.airDatepicker,
			dp = datepicker.Constructor;

		datepicker.Navigation = function (d, opts) {
			this.d = d;
			this.opts = opts;

			this.$buttonsContainer = '';

			this.init();
		};

		datepicker.Navigation.prototype = {
			init: function () {
				this._buildBaseHtml();
				this._bindEvents();
			},

			_bindEvents: function () {
				this.d.$nav.on('click', '.datepicker--nav-action', $.proxy(this._onClickNavButton, this));
				this.d.$nav.on('click', '.datepicker--nav-title', $.proxy(this._onClickNavTitle, this));
				this.d.$datepicker.on('click', '.datepicker--button', $.proxy(this._onClickNavButton, this));
			},

			_buildBaseHtml: function () {
				if (!this.opts.onlyTimepicker) {
					this._render();
				}
				this._addButtonsIfNeed();
			},

			_addButtonsIfNeed: function () {
				if (this.opts.todayButton) {
					this._addButton('today')
				}
				if (this.opts.clearButton) {
					this._addButton('clear')
				}
			},

			_render: function () {
				var title = this._getTitle(this.d.currentDate),
					html = dp.template(template, $.extend({ title: title }, this.opts));
				this.d.$nav.html(html);
				if (this.d.view == 'years') {
					$('.datepicker--nav-title', this.d.$nav).addClass('-disabled-');
				}
				this.setNavStatus();
			},

			_getTitle: function (date) {
				return this.d.formatDate(this.opts.navTitles[this.d.view], date)
			},

			_addButton: function (type) {
				if (!this.$buttonsContainer.length) {
					this._addButtonsContainer();
				}

				var data = {
					action: type,
					label: this.d.loc[type]
				},
					html = dp.template(button, data);

				if ($('[data-action=' + type + ']', this.$buttonsContainer).length) return;
				this.$buttonsContainer.append(html);
			},

			_addButtonsContainer: function () {
				this.d.$datepicker.append(buttonsContainerTemplate);
				this.$buttonsContainer = $('.datepicker--buttons', this.d.$datepicker);
			},

			setNavStatus: function () {
				if (!(this.opts.minDate || this.opts.maxDate) || !this.opts.disableNavWhenOutOfRange) return;

				var date = this.d.parsedDate,
					m = date.month,
					y = date.year,
					d = date.date;

				switch (this.d.view) {
					case 'days':
						if (!this.d._isInRange(new Date(y, m - 1, 1), 'month')) {
							this._disableNav('prev')
						}
						if (!this.d._isInRange(new Date(y, m + 1, 1), 'month')) {
							this._disableNav('next')
						}
						break;
					case 'months':
						if (!this.d._isInRange(new Date(y - 1, m, d), 'year')) {
							this._disableNav('prev')
						}
						if (!this.d._isInRange(new Date(y + 1, m, d), 'year')) {
							this._disableNav('next')
						}
						break;
					case 'years':
						var decade = dp.getDecade(this.d.date);
						if (!this.d._isInRange(new Date(decade[0] - 1, 0, 1), 'year')) {
							this._disableNav('prev')
						}
						if (!this.d._isInRange(new Date(decade[1] + 1, 0, 1), 'year')) {
							this._disableNav('next')
						}
						break;
				}
			},

			_disableNav: function (nav) {
				$('[data-action="' + nav + '"]', this.d.$nav).addClass('-disabled-')
			},

			_activateNav: function (nav) {
				$('[data-action="' + nav + '"]', this.d.$nav).removeClass('-disabled-')
			},

			_onClickNavButton: function (e) {
				var $el = $(e.target).closest('[data-action]'),
					action = $el.data('action');

				this.d[action]();
			},

			_onClickNavTitle: function (e) {
				if ($(e.target).hasClass('-disabled-')) return;

				if (this.d.view == 'days') {
					return this.d.view = 'months'
				}

				this.d.view = 'years';
			}
		}

	})();

	; (function () {
		var template = '<div class="datepicker--time">' +
			'<div class="datepicker--time-current">' +
			'   <span class="datepicker--time-current-hours">#{hourVisible}</span>' +
			'   <span class="datepicker--time-current-colon">:</span>' +
			'   <span class="datepicker--time-current-minutes">#{minValue}</span>' +
			'</div>' +
			'<div class="datepicker--time-sliders">' +
			'   <div class="datepicker--time-row">' +
			'      <input type="range" name="hours" value="#{hourValue}" min="#{hourMin}" max="#{hourMax}" step="#{hourStep}"/>' +
			'   </div>' +
			'   <div class="datepicker--time-row">' +
			'      <input type="range" name="minutes" value="#{minValue}" min="#{minMin}" max="#{minMax}" step="#{minStep}"/>' +
			'   </div>' +
			'</div>' +
			'</div>',
			datepicker = $.fn.airDatepicker,
			dp = datepicker.Constructor;

		datepicker.Timepicker = function (inst, opts) {
			this.d = inst;
			this.opts = opts;

			this.init();
		};

		datepicker.Timepicker.prototype = {
			init: function () {
				var input = 'input';
				this._setTime(this.d.date);
				this._buildHTML();

				if (navigator.userAgent.match(/trident/gi)) {
					input = 'change';
				}

				this.d.$el.on('selectDate', this._onSelectDate.bind(this));
				this.$ranges.on(input, this._onChangeRange.bind(this));
				this.$ranges.on('mouseup', this._onMouseUpRange.bind(this));
				this.$ranges.on('mousemove focus ', this._onMouseEnterRange.bind(this));
				this.$ranges.on('mouseout blur', this._onMouseOutRange.bind(this));
			},

			_setTime: function (date) {
				var _date = dp.getParsedDate(date);

				this._handleDate(date);
				this.hours = _date.hours < this.minHours ? this.minHours : _date.hours;
				this.minutes = _date.minutes < this.minMinutes ? this.minMinutes : _date.minutes;
			},

			/**
			 * Sets minHours and minMinutes from date (usually it's a minDate)
			 * Also changes minMinutes if current hours are bigger then @date hours
			 * @param date {Date}
			 * @private
			 */
			_setMinTimeFromDate: function (date) {
				this.minHours = date.getHours();
				this.minMinutes = date.getMinutes();

				// If, for example, min hours are 10, and current hours are 12,
				// update minMinutes to default value, to be able to choose whole range of values
				if (this.d.lastSelectedDate) {
					if (this.d.lastSelectedDate.getHours() > date.getHours()) {
						this.minMinutes = this.opts.minMinutes;
					}
				}
			},

			_setMaxTimeFromDate: function (date) {
				this.maxHours = date.getHours();
				this.maxMinutes = date.getMinutes();

				if (this.d.lastSelectedDate) {
					if (this.d.lastSelectedDate.getHours() < date.getHours()) {
						this.maxMinutes = this.opts.maxMinutes;
					}
				}
			},

			_setDefaultMinMaxTime: function () {
				var maxHours = 23,
					maxMinutes = 59,
					opts = this.opts;

				this.minHours = opts.minHours < 0 || opts.minHours > maxHours ? 0 : opts.minHours;
				this.minMinutes = opts.minMinutes < 0 || opts.minMinutes > maxMinutes ? 0 : opts.minMinutes;
				this.maxHours = opts.maxHours < 0 || opts.maxHours > maxHours ? maxHours : opts.maxHours;
				this.maxMinutes = opts.maxMinutes < 0 || opts.maxMinutes > maxMinutes ? maxMinutes : opts.maxMinutes;
			},

			/**
			 * Looks for min/max hours/minutes and if current values
			 * are out of range sets valid values.
			 * @private
			 */
			_validateHoursMinutes: function (date) {
				if (this.hours < this.minHours) {
					this.hours = this.minHours;
				} else if (this.hours > this.maxHours) {
					this.hours = this.maxHours;
				}

				if (this.minutes < this.minMinutes) {
					this.minutes = this.minMinutes;
				} else if (this.minutes > this.maxMinutes) {
					this.minutes = this.maxMinutes;
				}
			},

			_buildHTML: function () {
				var lz = dp.getLeadingZeroNum,
					data = {
						hourMin: this.minHours,
						hourMax: lz(this.maxHours),
						hourStep: this.opts.hoursStep,
						hourValue: this.hours,
						hourVisible: lz(this.displayHours),
						minMin: this.minMinutes,
						minMax: lz(this.maxMinutes),
						minStep: this.opts.minutesStep,
						minValue: lz(this.minutes)
					},
					_template = dp.template(template, data);

				this.$timepicker = $(_template).appendTo(this.d.$datepicker);
				this.$ranges = $('[type="range"]', this.$timepicker);
				this.$hours = $('[name="hours"]', this.$timepicker);
				this.$minutes = $('[name="minutes"]', this.$timepicker);
				this.$hoursText = $('.datepicker--time-current-hours', this.$timepicker);
				this.$minutesText = $('.datepicker--time-current-minutes', this.$timepicker);

				if (this.d.ampm) {
					this.$ampm = $('<span class="datepicker--time-current-ampm">')
						.appendTo($('.datepicker--time-current', this.$timepicker))
						.html(this.dayPeriod);

					this.$timepicker.addClass('-am-pm-');
				}
			},

			_updateCurrentTime: function () {
				var h = dp.getLeadingZeroNum(this.displayHours),
					m = dp.getLeadingZeroNum(this.minutes);

				this.$hoursText.html(h);
				this.$minutesText.html(m);

				if (this.d.ampm) {
					this.$ampm.html(this.dayPeriod);
				}
			},

			_updateRanges: function () {
				this.$hours.attr({
					min: this.minHours,
					max: this.maxHours
				}).val(this.hours);

				this.$minutes.attr({
					min: this.minMinutes,
					max: this.maxMinutes
				}).val(this.minutes)
			},

			/**
			 * Sets minHours, minMinutes etc. from date. If date is not passed, than sets
			 * values from options
			 * @param [date] {object} - Date object, to get values from
			 * @private
			 */
			_handleDate: function (date) {
				this._setDefaultMinMaxTime();
				if (date) {
					if (dp.isSame(date, this.d.opts.minDate)) {
						this._setMinTimeFromDate(this.d.opts.minDate);
					} else if (dp.isSame(date, this.d.opts.maxDate)) {
						this._setMaxTimeFromDate(this.d.opts.maxDate);
					}
				}

				this._validateHoursMinutes(date);
			},

			update: function () {
				this._updateRanges();
				this._updateCurrentTime();
			},

			/**
			 * Calculates valid hour value to display in text input and datepicker's body.
			 * @param date {Date|Number} - date or hours
			 * @param [ampm] {Boolean} - 12 hours mode
			 * @returns {{hours: *, dayPeriod: string}}
			 * @private
			 */
			_getValidHoursFromDate: function (date, ampm) {
				var d = date,
					hours = date;

				if (date instanceof Date) {
					d = dp.getParsedDate(date);
					hours = d.hours;
				}

				var _ampm = ampm || this.d.ampm,
					dayPeriod = 'am';

				if (_ampm) {
					switch (true) {
						case hours == 0:
							hours = 12;
							break;
						case hours == 12:
							dayPeriod = 'pm';
							break;
						case hours > 11:
							hours = hours - 12;
							dayPeriod = 'pm';
							break;
						default:
							break;
					}
				}

				return {
					hours: hours,
					dayPeriod: dayPeriod
				}
			},

			set hours(val) {
				this._hours = val;

				var displayHours = this._getValidHoursFromDate(val);

				this.displayHours = displayHours.hours;
				this.dayPeriod = displayHours.dayPeriod;
			},

			get hours() {
				return this._hours;
			},

			//  Events
			// -------------------------------------------------

			_onChangeRange: function (e) {
				var $target = $(e.target),
					name = $target.attr('name');

				this.d.timepickerIsActive = true;

				this[name] = $target.val();
				this._updateCurrentTime();
				this.d._trigger('timeChange', [this.hours, this.minutes]);

				this._handleDate(this.d.lastSelectedDate);
				this.update()
			},

			_onSelectDate: function (e, data) {
				this._handleDate(data);
				this.update();
			},

			_onMouseEnterRange: function (e) {
				var name = $(e.target).attr('name');
				$('.datepicker--time-current-' + name, this.$timepicker).addClass('-focus-');
			},

			_onMouseOutRange: function (e) {
				var name = $(e.target).attr('name');
				if (this.d.inFocus) return; // Prevent removing focus when mouse out of range slider
				$('.datepicker--time-current-' + name, this.$timepicker).removeClass('-focus-');
			},

			_onMouseUpRange: function (e) {
				this.d.timepickerIsActive = false;
			}
		};
	})();
})(window, jQuery);