﻿(function (window, $) {
	function Calendar(jParent, options) {
		var oCalendar = this, oOldCalendar = this, dStart, jDays,
			dateTypes = ['Selected', 'Avalible'];
		if (!(this instanceof Calendar)) {
			return new Calendar(jParent, options);
		}
		if (!(jParent instanceof $) || !jParent.length) {
			throw new Error('Неверный объект родительского элемента календаря (' + jParent.toString() + ')');
		}
		this.jParent = jParent;
		oOldCalendar = jParent.data('calendarObj');
		if (oOldCalendar) {
			return oOldCalendar;
		}
		this.optValues = $.extend(true, {}, this.defOptions, options);
		this.prepareElements();
		jParent.data('calendarObj', this);
		this.Today = this.getToday();
		this.setInterval(this.optValues.monthsRange);
		this.setInterval(this.optValues.avalibleRange, 'Date');
		//Определяем, с какой даты начать + проверяем, не выходит ли она за интервал доступных
		dStart = this.getStartDate();
		//получаем доступные для выбора и выбранные даты или пути для их получения
		this.Dates = {};
		for (var typeNo = 0, typesLength = dateTypes.length; typeNo < typesLength; typeNo++) {
			var typeName = dateTypes[typeNo], oDates = this.optValues[typeName.toLowerCase() + 'Dates'];
			if (oDates instanceof Object) {
				this.Dates[typeName] = oDates;
			} else if (typeof oDates == 'string') {
				if(oDates.match(/^(?:http(?:s)?:\/\/)?(?:[a-z0-9-]+\.)+[a-z]{2,4}(?:\?.*)?$/i)) {
					this[typeName.toLowerCase() + 'Url'] = oDates;
					this.Dates[typeName] = {};
				} else {
					this.Dates[typeName] = false;
				}
			}
		}
		this.jDays.html('');
		this.drawDays();
		this.setCurMonth(dStart);
	}
	
	Calendar.prototype = {
		defOptions : {
			cssSelectors : {
				Days : '.js-days',
				Month : '.js-month',
				prevMonth : 'a.prev_month',
				nextMonth : 'a.next_month',
				Year : '.js-year'
			},
			cssClasses : {
				weekEnd : 's',
				Today : 'on',
				Avalible : 'rem',
				Unavalible : 'off',
				Selected : 'sel',
				Disabled : 'off'
			},
			monthNames : ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
			dayNames : ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс'],
			startFrom : '',
			monthsRange : {
				min: '2001-01',
				max: ''
			},
			avalibleDates: '',
			selectedDates: '',
			onDateSelect: false,
			baseHref: '',
			dateVar: 'date',
			dateFormat: 'YYYY-MM-dd',
			maxSel : false
		},
		setInterval: function (dateRange, rangeType) {
			if (typeof rangeType == 'undefined') {
				rangeType = 'Month';
			}
			for (var posName in dateRange) {
				var intervalPos = dateRange[posName],
					dIntervalPos, isMax = posName == 'max';
				if (intervalPos.length) {
					dIntervalPos = this.getDateFromStr(intervalPos);
				}
				if (!dIntervalPos || isNaN(dIntervalPos.getDate())) {
					dIntervalPos = this.getToday();
					if (isMax) {
						dIntervalPos.setMonth(dIntervalPos.getMonth() + 1);
					}
					dIntervalPos.setDate(isMax ? -1 : 1);
				}
				if (isMax) {
					dIntervalPos.setHours(24, 0, 0, -1);
				} else {
					dIntervalPos.setHours(0, 0, 0, 0);
				}
				this[posName + rangeType] = {
					Month : dIntervalPos.getMonth(),
					Year : dIntervalPos.getFullYear(),
					Date : dIntervalPos
				};
				dIntervalPos = undefined;
			}
		},
		getStartDate: function () {
			var dStart = this.getDateFromStr(this.optValues.startFrom);
			if (isNaN(dStart.getDate())) {
				dStart = this.getToday();
			}
			if (dStart > this.maxMonth.Date) {
				dStart = new Date(this.maxMonth.Date);
			} else if (dStart < this.minMonth.Date) {
				dStart = new Date(this.minMonth.Date);
			}
			return dStart;
		},
		prepareElements : function () {
			var oSelectors = this.optValues.cssSelectors, elName;
			for (var selName in oSelectors) {
				if (oSelectors[selName].length) {
					elName = 'j' + selName.substr(0, 1).toUpperCase() + selName.substring(1);
					this[elName] = this.jParent.find(oSelectors[selName]);
					if (!this[elName].length) {
						throw new Error('Неверный селектор "' + selName + '" (' + this[elName].selector + ').');
					}
					if (selName == 'Days' && this[elName].get(0).tagName != 'TABLE') { // TODO: Переделать в CASE?
						throw new Error('Cелектор "Days" (' + this.jDays.selector + ') должен указывать на таблицу.');
					}
					if (selName == 'nextMonth' || selName == 'prevMonth') {
						(function (oCalendar, jElement, methodName) {
							jElement.bind('click.Calendar', function (e) {
								e.preventDefault();
								if (jElement.hasClass(oCalendar.optValues.cssClasses.Disabled)) {
									return;
								}
								oCalendar[methodName]();
							});
						})(this, this[elName], selName);
					}
				}
			}
		},
		drawDays : function () {
			var jDaysHead = $('<thead />'), dayNames = this.optValues.dayNames, jDay;
			for (var dayNo = 0, daysLength = dayNames.length; dayNo < daysLength; dayNo++) {
				jDay = $('<th />').text(dayNames[dayNo]).appendTo(jDaysHead);
				if (dayNo > 4) {
					jDay.addClass(this.optValues.cssClasses.weekEnd);
				}
			}
			jDaysHead.appendTo(this.jDays);
		},
		drawDates : function (oDates, monthId, callHandler, dDate) {
			var jDates = $('<tbody />'), dayNo = 0, jTr = $('<tr />').appendTo(jDates), jTd, curDate, curMonth;
			if (typeof dDate == 'undefined') {
				dDate = new Date(this.Year, this.Month);
			}
			this.jDays.find('tbody').remove();
			dDate.setDate(1);
			while ((curMonth = dDate.getMonth()) == this.Month || dayNo < 7) {
				curDate = dDate.getDate();
				if (this.localDayNo(dDate.getDay()) != dayNo || curMonth != this.Month) {
					jTd = $('<td />');
				} else {
					jTd = this.createDateElm(dDate, oDates, monthId, callHandler);
					dDate.setDate(curDate + 1);
				}
				jTd.appendTo(jTr);
				dayNo++;
				if (dayNo == 7 && curMonth == this.Month) {
					dayNo = 0;
					jTr = $('<tr />').appendTo(jDates);
				}
			}
			jDates.appendTo(this.jDays);
		},
		createDateElm : function (dDate, oDates, monthId, callHandler) {
			var dayNo = this.localDayNo(dDate.getDay()), jTd = $('<td />'),
				sDate = dDate.getDate(),
				jLink = $('<a />').text(sDate),
				curDate = dDate.getDate(), cellClasses = [], linkHref = this.getHref(dDate), 
				oCalendar = this;
			if (typeof oDates == 'undefined') {
				oDates = {};
			}
			(function (date) {
				jLink.bind('click.Calendar', function (e) {
					e.preventDefault();
					oCalendar.selectDate(date, jTd, monthId, sDate);
				});
			})(new Date(dDate));
			if (linkHref) {
				jLink.attr('href', linkHref);
			}
			if (dayNo > 4) {
				jLink.addClass(this.optValues.cssClasses.weekEnd);
			}
			if (dDate.getTime() == this.Today.getTime()) {
				cellClasses.push(this.optValues.cssClasses.Today);
			}
			if (
				(!oDates.Avalible || (oDates.Avalible && $.inArray(curDate, oDates.Avalible) != -1))
				&& (!this.minDate || (this.minDate && dDate >= this.minDate.Date))
				&& (!this.maxDate || (this.maxDate && dDate <= this.maxDate.Date))
			) {
				cellClasses.push(this.optValues.cssClasses.Avalible);
			} else {
				cellClasses.push(this.optValues.cssClasses.Unavalible);
			}
			if (oDates.Selected && $.inArray(curDate, oDates.Selected) != -1) {
				cellClasses.push(this.optValues.cssClasses.Selected);
				if (callHandler !== false && typeof this.optValues.onDateSelect == 'function') {
					this.optValues.onDateSelect.apply(this, [false, dDate, jTd, monthId, sDate]);
				}
			}
			if (cellClasses.length) {
				jTd.addClass(cellClasses.join(' '));
			}
			jTd.append(jLink);
			return jTd;
		},
		selectDate : function (dDate, jTd, monthId, sDate) {
			var index, curSelected = 0;
			if (jTd.hasClass(this.optValues.cssClasses.Unavalible)) {
				return;
			}
			if (!this.Dates.Selected) {
				this.Dates.Selected = {};
			}
			if (!this.Dates.Selected[monthId]) {
				this.Dates.Selected[monthId] = [];
			}
			index = $.inArray(sDate, this.Dates.Selected[monthId]);
			if (index != -1) {
				this.Dates.Selected[monthId].splice(index, 1);
				jTd.removeClass(this.optValues.cssClasses.Selected);
			} else {
				if (this.optValues.maxSel !== false) {
					for (var mId in this.Dates.Selected) {
						if (this.Dates.Selected.hasOwnProperty(mId)) {
							curSelected += this.Dates.Selected[mId].length;
						}
					}
					if(curSelected >= this.optValues.maxSel) {
						if (this.optValues.maxSel == 1) {
							this.Dates.Selected = {};
							this.Dates.Selected[monthId] = [];
							this.jDays.find('.' + this.optValues.cssClasses.Selected).removeClass(this.optValues.cssClasses.Selected);
						} else {
							return false;
						}
					}
				}
				this.Dates.Selected[monthId].push(sDate);
				jTd.addClass(this.optValues.cssClasses.Selected);
			}
			if (typeof this.optValues.onDateSelect == 'function') {
				this.optValues.onDateSelect.apply(this, [index != -1, dDate, jTd, monthId, sDate]);
			}
			/*if (typeof this.optValues.onDateSelect == 'function') {
				this.optValues.onDateSelect.call(this, dDate, jTd);
			}*/
		},
		getHref : function (dDate) {
			var href = this.optValues.baseHref,
				formatedDate = this.optValues.dateFormat;
			if (this.optValues.baseHref === false) {
				return false;
			}
			href += (href.indexOf('?') == -1 ? '?' : '&');
			for (var rplcmnt in this.dateFormat) {
				var replaceWith = this.dateFormat[rplcmnt];
				switch (replaceWith) {
				case 'getMonth':
					replaceWith = dDate[replaceWith]() + 1;
					break;
				case 'getYear':
					replaceWith = (dDate.getFullYear() + '').substr(2);
					break;
				default :
					replaceWith = dDate[replaceWith]();
				}
				formatedDate = formatedDate.replace(rplcmnt, replaceWith);
			}
			return href + this.optValues.dateVar + '=' + formatedDate;
		},
		setCurMonth : function (dCur, callHandler) {
			var oDates = {}, monthId, preloadCnt = 0, preloadUrl;
			if (dCur > this.maxMonth.Date || dCur < this.minMonth.Date) {
				return false;
			}
			this.Month = dCur.getMonth();
			this.Year = dCur.getFullYear();
			monthId = this.Year + '-' + this.addZero(this.Month + 1);
			for (var typeName in this.Dates) {
				if (this.Dates[typeName]) {
					if (this.Dates[typeName][monthId] instanceof Array) {
						oDates[typeName] = this.Dates[typeName][monthId];
					} else {
						oDates[typeName] = [];
					}
				} else if ((preloadUrl = this[typeName.toLowerCase() + 'Url']) && preloadUrl.length) {
					if (!preloadCnt) {
						this.showPreloader();
					}
					preloadCnt++;
					(function (oCalendar, key) {
						$.getJSON(preloadUrl, function (data) {
							preloadCnt--;
							oCalendar.Dates[key] = data;
							if (!preloadCnt) {
								oCalendar.showPreloader(false);
								oCalendar.drawDates(data, monthId, callHandler);
							}
						});
					})(this, monthId);
				}
			}
			this.jMonth.text(this.optValues.monthNames[this.Month]);
			this.jYear.text(this.Year);
			if (typeof preloadUrl == 'undefined') {
				this.drawDates(oDates, monthId, callHandler);
			}
			this.updateLinks();
		},
		updateLinks : function () {
			var oPositions = {'max' :'jNextMonth', 'min': 'jPrevMonth'};
			for (var posName in oPositions) {
				var oPos = this[posName + 'Month'];
				if (oPos.Year == this.Year && oPos.Month == this.Month) {
					this[oPositions[posName]].addClass(this.optValues.cssClasses.Disabled);
				} else {
					this[oPositions[posName]].removeClass(this.optValues.cssClasses.Disabled);
				}
			}
		},
		nextMonth : function () {
			this.setCurMonth(new Date(this.Year, this.Month + 1));
		},
		prevMonth : function () {
			this.setCurMonth(new Date(this.Year, this.Month - 1));
		},
		localDayNo : function (dayNo) {
			dayNo--;
			if (dayNo < 0) {
				dayNo = 6;
			}
			return dayNo;
		},
		getToday : (function () { //получаем сегодняшнюю дату с временем 00:00:00
			var dToday = new Date();
			return function () {
				return new Date(dToday.getFullYear(), dToday.getMonth(), dToday.getDate());
			}
		})(),
		showPreloader : function (show) {
			if (typeof show == 'undefined') {
				show = true;
			}
		},
		dateFormat : {
			'YYYY' : 'getFullYear',
			'YY' : 'getYear',
			'MM' : 'getMonth',
			'dd' : 'getDate',
			'TT' : 'getTime'
		},
		addZero: function (num) {
			if (num < 10) {
				num = '0' + num;
			}
			return num;
		},
		getDateFromStr: (function () {
			if (isNaN(new Date('2011-12-13'))) {
				return function (strDate) {
					var aDate = strDate.split('-');
					return new Date(aDate[0], aDate[1] ? aDate[1] - 1 : 0, aDate[2] ? aDate[2] : 1);
				}
			} else {
				return function (strDate) {
					return new Date(strDate);
				}
			}
		})()
	};
	
	$.fn.goCalendar = function (options) {
		Calendar($(this), options);
		return this;
	};
})(self, jQuery);
