/*! DateTime picker for DataTables.net v1.0.3
 *
 * ©2020 SpryMedia Ltd, all rights reserved.
 * License: MIT datatables.net/license/mit
 */

/**
 * @summary     DateTime picker for DataTables.net
 * @version     1.0.3
 * @file        dataTables.dateTime.js
 * @author      SpryMedia Ltd
 * @contact     www.datatables.net/contact
 */
(function( factory ){
	if ( typeof define === 'function' && define.amd ) {
		// AMD
		define( ['jquery'], function ( $ ) {
			return factory( $, window, document );
		} );
	}
	else if ( typeof exports === 'object' ) {
		// CommonJS
		module.exports = function (root, $) {
			if ( ! root ) {
				root = window;
			}

			return factory( $, root, root.document );
		};
	}
	else {
		// Browser
		factory( jQuery, window, document );
	}
}(function( $, window, document, undefined ) {
'use strict';

// Support libraries which support a Moment like API
var dateLib = window.moment
	? window.moment
	: window.dayjs
		? window.dayjs
		: window.luxon
			? window.luxon
			: null;

/*
 * This file provides a DateTime GUI picker (calendar and time input). Only the
 * format YYYY-MM-DD is supported without additional software, but the end user
 * experience can be greatly enhanced by including the momentjs, dayjs or luxon library
 * which provide date / time parsing and formatting options.
 *
 * This functionality is required because the HTML5 date and datetime input
 * types are not widely supported in desktop browsers.
 *
 * Constructed by using:
 *
 *     new DateTime( input, opts )
 *
 * where `input` is the HTML input element to use and `opts` is an object of
 * options based on the `DateTime.defaults` object.
 */
var DateTime = function ( input, opts ) {
	this.c = $.extend( true, {}, DateTime.defaults, opts );
	var classPrefix = this.c.classPrefix;
	var i18n = this.c.i18n;

	// Only IS8601 dates are supported without moment, dayjs or luxon
	if ( ! dateLib && this.c.format !== 'YYYY-MM-DD' ) {
		throw "DateTime: Without momentjs, dayjs or luxon only the format 'YYYY-MM-DD' can be used";
	}

	// Min and max need to be `Date` objects in the config
	if (typeof this.c.minDate === 'string') {
		this.c.minDate = new Date(this.c.minDate);
	}
	if (typeof this.c.maxDate === 'string') {
		this.c.maxDate = new Date(this.c.maxDate);
	}

	var timeBlock = function ( type ) {
		return '<div class="'+classPrefix+'-timeblock">'+
			'</div>';
	};

	var gap = function () {
		return '<span>:</span>';
	};

	// DOM structure
	var structure = $(
		'<div class="'+classPrefix+'">'+
			'<div class="'+classPrefix+'-date">'+
				'<div class="'+classPrefix+'-title">'+
					'<div class="'+classPrefix+'-iconLeft">'+
						'<button title="'+i18n.previous+'">'+i18n.previous+'</button>'+
					'</div>'+
					'<div class="'+classPrefix+'-iconRight">'+
						'<button title="'+i18n.next+'">'+i18n.next+'</button>'+
					'</div>'+
					'<div class="'+classPrefix+'-label">'+
						'<span></span>'+
						'<select class="'+classPrefix+'-month"></select>'+
					'</div>'+
					'<div class="'+classPrefix+'-label">'+
						'<span></span>'+
						'<select class="'+classPrefix+'-year"></select>'+
					'</div>'+
				'</div>'+
				'<div class="'+classPrefix+'-calendar"></div>'+
			'</div>'+
			'<div class="'+classPrefix+'-time">'+
				'<div class="'+classPrefix+'-hours"></div>'+
				'<div class="'+classPrefix+'-minutes"></div>'+
				'<div class="'+classPrefix+'-seconds"></div>'+
			'</div>'+
			'<div class="'+classPrefix+'-error"></div>'+
		'</div>'
	);

	this.dom = {
		container: structure,
		date:      structure.find( '.'+classPrefix+'-date' ),
		title:     structure.find( '.'+classPrefix+'-title' ),
		calendar:  structure.find( '.'+classPrefix+'-calendar' ),
		time:      structure.find( '.'+classPrefix+'-time' ),
		error:     structure.find( '.'+classPrefix+'-error' ),
		input:     $(input)
	};

	this.s = {
		/** @type {Date} Date value that the picker has currently selected */
		d: null,

		/** @type {Date} Date of the calendar - might not match the value */
		display: null,

		/** @type {number} Used to select minutes in a range where the range base is itself unavailable */
		minutesRange: null,

		/** @type {number} Used to select minutes in a range where the range base is itself unavailable */
		secondsRange: null,

		/** @type {String} Unique namespace string for this instance */
		namespace: 'dateime-'+(DateTime._instance++),

		/** @type {Object} Parts of the picker that should be shown */
		parts: {
			date:    this.c.format.match( /[YMD]|L(?!T)|l/ ) !== null,
			time:    this.c.format.match( /[Hhm]|LT|LTS/ ) !== null,
			seconds: this.c.format.indexOf( 's' )   !== -1,
			hours12: this.c.format.match( /[haA]/ ) !== null
		}
	};

	this.dom.container
		.append( this.dom.date )
		.append( this.dom.time )
		.append( this.dom.error );

	this.dom.date
		.append( this.dom.title )
		.append( this.dom.calendar );

	this._constructor();
};

$.extend( DateTime.prototype, {
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Public
	 */
	
	/**
	 * Destroy the control
	 */
	destroy: function () {
		this._hide(true);
		this.dom.container.off().empty();
		this.dom.input
			.removeAttr('autocomplete')
			.off('.datetime');
	},

	errorMsg: function ( msg ) {
		var error = this.dom.error;

		if ( msg ) {
			error.html( msg );
		}
		else {
			error.empty();
		}

		return this;
	},

	hide: function () {
		this._hide();

		return this;
	},

	max: function ( date ) {
		this.c.maxDate = typeof date === 'string'
			? new Date(date)
			: date;

		this._optionsTitle();
		this._setCalander();

		return this;
	},

	min: function ( date ) {
		this.c.minDate = typeof date === 'string'
			? new Date(date)
			: date;

		this._optionsTitle();
		this._setCalander();

		return this;
	},

	/**
	 * Check if an element belongs to this control
	 *
	 * @param  {node} node Element to check
	 * @return {boolean}   true if owned by this control, false otherwise
	 */
	owns: function ( node ) {
		return $(node).parents().filter( this.dom.container ).length > 0;
	},

	/**
	 * Get / set the value
	 *
	 * @param  {string|Date} set   Value to set
	 * @param  {boolean} [write=true] Flag to indicate if the formatted value
	 *   should be written into the input element
	 */
	val: function ( set, write ) {
		if ( set === undefined ) {
			return this.s.d;
		}

		if ( set instanceof Date ) {
			this.s.d = this._dateToUtc( set );
		}
		else if ( set === null || set === '' ) {
			this.s.d = null;
		}
		else if ( set === '--now' ) {
			this.s.d = new Date();
		}
		else if ( typeof set === 'string' ) {
			// luxon uses different method names so need to be able to call them
			if(dateLib && dateLib == window.luxon) {
				var luxDT = dateLib.DateTime.fromFormat(set, this.c.format)
				this.s.d = luxDT.isValid ? luxDT.toJSDate() : null;
			}
			else if ( dateLib ) {
				// Use moment, dayjs or luxon if possible (even for ISO8601 strings, since it
				// will correctly handle 0000-00-00 and the like)
				var m = dateLib.utc( set, this.c.format, this.c.locale, this.c.strict );
				this.s.d = m.isValid() ? m.toDate() : null;
			}
			else {
				// Else must be using ISO8601 without a date library (constructor would
				// have thrown an error otherwise)
				var match = set.match(/(\d{4})\-(\d{2})\-(\d{2})/ );
				this.s.d = match ?
					new Date( Date.UTC(match[1], match[2]-1, match[3]) ) :
					null;
			}
		}

		if ( write || write === undefined ) {
			if ( this.s.d ) {
				this._writeOutput();
			}
			else {
				// The input value was not valid...
				this.dom.input.val( set );
			}
		}

		// We need a date to be able to display the calendar at all
		if ( ! this.s.d ) {
			this.s.d = this._dateToUtc( new Date() );
		}

		this.s.display = new Date( this.s.d.toString() );

		// Set the day of the month to be 1 so changing between months doesn't
        // run into issues when going from day 31 to 28 (for example)
		this.s.display.setUTCDate( 1 );

		// Update the display elements for the new value
		this._setTitle();
		this._setCalander();
		this._setTime();

		return this;
	},


	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Constructor
	 */
	
	/**
	 * Build the control and assign initial event handlers
	 *
	 * @private
	 */
	_constructor: function () {
		var that = this;
		var classPrefix = this.c.classPrefix;
		var last = this.dom.input.val();

		var onChange = function () {
			var curr = that.dom.input.val();

			if (curr !== last) {
				that.c.onChange.call( that, curr, that.s.d, that.dom.input );
				last = curr;
			}
		};

		if ( ! this.s.parts.date ) {
			this.dom.date.css( 'display', 'none' );
		}

		if ( ! this.s.parts.time ) {
			this.dom.time.css( 'display', 'none' );
		}

		if ( ! this.s.parts.seconds ) {
			this.dom.time.children('div.'+classPrefix+'-seconds').remove();
			this.dom.time.children('span').eq(1).remove();
		}

		// Render the options
		this._optionsTitle();

		$(document).on('i18n.dt', function (e, settings) {
			if (settings.oLanguage.datetime) {
				$.extend(true, that.c.i18n, settings.oLanguage.datetime);
				this._optionsTitle();
			}
		});

		// When attached to a hidden input, we always show the input picker, and
		// do so inline
		if (this.dom.input.attr('type') === 'hidden') {
			this.dom.container.addClass('inline');
			this.c.attachTo = 'input';

			this.val( this.dom.input.val(), false );
			this._show();
		}

		// Set the initial value
		if (last) {
			this.val( last, false );
		}

		// Trigger the display of the widget when clicking or focusing on the
		// input element
		this.dom.input
			.attr('autocomplete', 'off')
			.on('focus.datetime click.datetime', function () {
				// If already visible - don't do anything
				if ( that.dom.container.is(':visible') || that.dom.input.is(':disabled') ) {
					return;
				}

				// In case the value has changed by text
				that.val( that.dom.input.val(), false );

				that._show();
			} )
			.on('keyup.datetime', function () {
				// Update the calendar's displayed value as the user types
				if ( that.dom.container.is(':visible') ) {
					that.val( that.dom.input.val(), false );
				}
			} );

		// Main event handlers for input in the widget
		this.dom.container
			.on( 'change', 'select', function () {
				var select = $(this);
				var val = select.val();

				if ( select.hasClass(classPrefix+'-month') ) {
					// Month select
					that._correctMonth( that.s.display, val );
					that._setTitle();
					that._setCalander();
				}
				else if ( select.hasClass(classPrefix+'-year') ) {
					// Year select
					that.s.display.setUTCFullYear( val );
					that._setTitle();
					that._setCalander();
				}
				else if ( select.hasClass(classPrefix+'-hours') || select.hasClass(classPrefix+'-ampm') ) {
					// Hours - need to take account of AM/PM input if present
					if ( that.s.parts.hours12 ) {
						var hours = $(that.dom.container).find('.'+classPrefix+'-hours').val() * 1;
						var pm = $(that.dom.container).find('.'+classPrefix+'-ampm').val() === 'pm';

						that.s.d.setUTCHours( hours === 12 && !pm ?
							0 :
							pm && hours !== 12 ?
								hours + 12 :
								hours
						);
					}
					else {
						that.s.d.setUTCHours( val );
					}

					that._setTime();
					that._writeOutput( true );

					onChange();
				}
				else if ( select.hasClass(classPrefix+'-minutes') ) {
					// Minutes select
					that.s.d.setUTCMinutes( val );
					that._setTime();
					that._writeOutput( true );

					onChange();
				}
				else if ( select.hasClass(classPrefix+'-seconds') ) {
					// Seconds select
					that.s.d.setSeconds( val );
					that._setTime();
					that._writeOutput( true );

					onChange();
				}

				that.dom.input.focus();
				that._position();
			} )
			.on( 'click', function (e) {
				var d = that.s.d;
				var nodeName = e.target.nodeName.toLowerCase();
				var target = nodeName === 'span' ?
					e.target.parentNode :
					e.target;

				nodeName = target.nodeName.toLowerCase();

				if ( nodeName === 'select' ) {
					return;
				}

				e.stopPropagation();

				if ( nodeName === 'button' ) {
					var button = $(target);
					var parent = button.parent();

					if ( parent.hasClass('disabled') && ! parent.hasClass('range') ) {
						button.blur();
						return;
					}

					if ( parent.hasClass(classPrefix+'-iconLeft') ) {
						// Previous month
						that.s.display.setUTCMonth( that.s.display.getUTCMonth()-1 );
						that._setTitle();
						that._setCalander();

						that.dom.input.focus();
					}
					else if ( parent.hasClass(classPrefix+'-iconRight') ) {
						// Next month
						that._correctMonth( that.s.display, that.s.display.getUTCMonth()+1 );
						that._setTitle();
						that._setCalander();

						that.dom.input.focus();
					}
					else if ( button.parents('.'+classPrefix+'-time').length ) {
						var val = button.data('value');
						var unit = button.data('unit');

						if ( unit === 'minutes' ) {
							if ( parent.hasClass('disabled') && parent.hasClass('range') ) {
								that.s.minutesRange = val;
								that._setTime();
								return;
							}
							else {
								that.s.minutesRange = null;
							}
						}

						if ( unit === 'seconds' ) {
							if ( parent.hasClass('disabled') && parent.hasClass('range') ) {
								that.s.secondsRange = val;
								that._setTime();
								return;
							}
							else {
								that.s.secondsRange = null;
							}
						}

						// Specific to hours for 12h clock
						if ( val === 'am' ) {
							if ( d.getUTCHours() >= 12 ) {
								val = d.getUTCHours() - 12;
							}
							else {
								return;
							}
						}
						else if ( val === 'pm' ) {
							if ( d.getUTCHours() < 12 ) {
								val = d.getUTCHours() + 12;
							}
							else {
								return;
							}
						}

						var set = unit === 'hours' ?
							'setUTCHours' :
							unit === 'minutes' ?
								'setUTCMinutes' :
								'setSeconds';

						d[set]( val );
						that._setTime();
						that._writeOutput( true );
						onChange();
					}
					else {
						// Calendar click
						if ( ! d ) {
							d = that._dateToUtc( new Date() );
						}

						// Can't be certain that the current day will exist in
						// the new month, and likewise don't know that the
						// new day will exist in the old month, But 1 always
						// does, so we can change the month without worry of a
						// recalculation being done automatically by `Date`
						d.setUTCDate( 1 );
						d.setUTCFullYear( button.data('year') );
						d.setUTCMonth( button.data('month') );
						d.setUTCDate( button.data('day') );

						that._writeOutput( true );

						// Don't hide if there is a time picker, since we want to
						// be able to select a time as well.
						if ( ! that.s.parts.time ) {
							// This is annoying but IE has some kind of async
							// behaviour with focus and the focus from the above
							// write would occur after this hide - resulting in the
							// calendar opening immediately
							setTimeout( function () {
								that._hide();
							}, 10 );
						}
						else {
							that._setCalander();
						}

						onChange();
					}
				}
				else {
					// Click anywhere else in the widget - return focus to the
					// input element
					that.dom.input.focus();
				}
			} );
	},


	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Private
	 */

	/**
	 * Compare the date part only of two dates - this is made super easy by the
	 * toDateString method!
	 *
	 * @param  {Date} a Date 1
	 * @param  {Date} b Date 2
	 * @private
	 */
	_compareDates: function( a, b ) {
		// Can't use toDateString as that converts to local time
		// luxon uses different method names so need to be able to call them
		return dateLib && dateLib == window.luxon
			? dateLib.DateTime.fromJSDate(a).toISODate() === dateLib.DateTime.fromJSDate(b).toISODate()
			: this._dateToUtcString(a) === this._dateToUtcString(b);
	},

	/**
	 * When changing month, take account of the fact that some months don't have
	 * the same number of days. For example going from January to February you
	 * can have the 31st of Jan selected and just add a month since the date
	 * would still be 31, and thus drop you into March.
	 *
	 * @param  {Date} date  Date - will be modified
	 * @param  {integer} month Month to set
	 * @private
	 */
	_correctMonth: function ( date, month ) {
		var days = this._daysInMonth( date.getUTCFullYear(), month );
		var correctDays = date.getUTCDate() > days;

		date.setUTCMonth( month );

		if ( correctDays ) {
			date.setUTCDate( days );
			date.setUTCMonth( month );
		}
	},

	/**
	 * Get the number of days in a method. Based on
	 * http://stackoverflow.com/a/4881951 by Matti Virkkunen
	 *
	 * @param  {integer} year  Year
	 * @param  {integer} month Month (starting at 0)
	 * @private
	 */
	_daysInMonth: function ( year, month ) {
		// 
		var isLeap = ((year % 4) === 0 && ((year % 100) !== 0 || (year % 400) === 0));
		var months = [31, (isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

		return months[month];
	},

	/**
	 * Create a new date object which has the UTC values set to the local time.
	 * This allows the local time to be used directly for the library which
	 * always bases its calculations and display on UTC.
	 *
	 * @param  {Date} s Date to "convert"
	 * @return {Date}   Shifted date
	 */
	_dateToUtc: function ( s ) {
		return new Date( Date.UTC(
			s.getFullYear(), s.getMonth(), s.getDate(),
			s.getHours(), s.getMinutes(), s.getSeconds()
		) );
	},

	/**
	 * Create a UTC ISO8601 date part from a date object
	 *
	 * @param  {Date} d Date to "convert"
	 * @return {string} ISO formatted date
	 */
	_dateToUtcString: function ( d ) {
		// luxon uses different method names so need to be able to call them
		return dateLib && dateLib == window.luxon
			? dateLib.DateTime.fromJSDate(d).toISODate()
			: d.getUTCFullYear()+'-'+
				this._pad(d.getUTCMonth()+1)+'-'+
				this._pad(d.getUTCDate());
	},

	/**
	 * Hide the control and remove events related to its display
	 *
	 * @private
	 */
	_hide: function (destroy) {
		if (! destroy && this.dom.input.attr('type') === 'hidden') {
			return;
		}

		var namespace = this.s.namespace;

		this.dom.container.detach();

		$(window).off( '.'+namespace );
		$(document).off( 'keydown.'+namespace );
		$('div.dataTables_scrollBody').off( 'scroll.'+namespace );
		$('div.DTE_Body_Content').off( 'scroll.'+namespace );
		$('body').off( 'click.'+namespace );
	},

	/**
	 * Convert a 24 hour value to a 12 hour value
	 *
	 * @param  {integer} val 24 hour value
	 * @return {integer}     12 hour value
	 * @private
	 */
	_hours24To12: function ( val ) {
		return val === 0 ?
			12 :
			val > 12 ?
				val - 12 :
				val;
	},

	/**
	 * Generate the HTML for a single day in the calendar - this is basically
	 * and HTML cell with a button that has data attributes so we know what was
	 * clicked on (if it is clicked on) and a bunch of classes for styling.
	 *
	 * @param  {object} day Day object from the `_htmlMonth` method
	 * @return {string}     HTML cell
	 */
	_htmlDay: function( day )
	{
		if ( day.empty ) {
			return '<td class="empty"></td>';
		}

		var classes = [ 'selectable' ];
		var classPrefix = this.c.classPrefix;

		if ( day.disabled ) {
			classes.push( 'disabled' );
		}

		if ( day.today ) {
			classes.push( 'now' );
		}

		if ( day.selected ) {
			classes.push( 'selected' );
		}

		return '<td data-day="' + day.day + '" class="' + classes.join(' ') + '">' +
				'<button class="'+classPrefix+'-button '+classPrefix+'-day" type="button" ' +'data-year="' + day.year + '" data-month="' + day.month + '" data-day="' + day.day + '">' +
					'<span>'+day.day+'</span>'+
				'</button>' +
			'</td>';
	},


	/**
	 * Create the HTML for a month to be displayed in the calendar table.
	 * 
	 * Based upon the logic used in Pikaday - MIT licensed
	 * Copyright (c) 2014 David Bushell
	 * https://github.com/dbushell/Pikaday
	 *
	 * @param  {integer} year  Year
	 * @param  {integer} month Month (starting at 0)
	 * @return {string} Calendar month HTML
	 * @private
	 */
	_htmlMonth: function ( year, month ) {
		var now    = this._dateToUtc( new Date() ),
			days   = this._daysInMonth( year, month ),
			before = new Date( Date.UTC(year, month, 1) ).getUTCDay(),
			data   = [],
			row    = [];

		if ( this.c.firstDay > 0 ) {
			before -= this.c.firstDay;

			if (before < 0) {
				before += 7;
			}
		}

		var cells = days + before,
			after = cells;

		while ( after > 7 ) {
			after -= 7;
		}

		cells += 7 - after;

		var minDate = this.c.minDate;
		var maxDate = this.c.maxDate;

		if ( minDate ) {
			minDate.setUTCHours(0);
			minDate.setUTCMinutes(0);
			minDate.setSeconds(0);
		}

		if ( maxDate ) {
			maxDate.setUTCHours(23);
			maxDate.setUTCMinutes(59);
			maxDate.setSeconds(59);
		}

		for ( var i=0, r=0 ; i<cells ; i++ ) {
			var day      = new Date( Date.UTC(year, month, 1 + (i - before)) ),
				selected = this.s.d ? this._compareDates(day, this.s.d) : false,
				today    = this._compareDates(day, now),
				empty    = i < before || i >= (days + before),
				disabled = (minDate && day < minDate) ||
				           (maxDate && day > maxDate);

			var disableDays = this.c.disableDays;
			if ( Array.isArray( disableDays ) && $.inArray( day.getUTCDay(), disableDays ) !== -1 ) {
				disabled = true;
			}
			else if ( typeof disableDays === 'function' && disableDays( day ) === true ) {
				disabled = true;
			}

			var dayConfig = {
				day:      1 + (i - before),
				month:    month,
				year:     year,
				selected: selected,
				today:    today,
				disabled: disabled,
				empty:    empty
			};

			row.push( this._htmlDay(dayConfig) );

			if ( ++r === 7 ) {
				if ( this.c.showWeekNumber ) {
					row.unshift( this._htmlWeekOfYear(i - before, month, year) );
				}

				data.push( '<tr>'+row.join('')+'</tr>' );
				row = [];
				r = 0;
			}
		}

		var classPrefix = this.c.classPrefix;
		var className = classPrefix+'-table';
		if ( this.c.showWeekNumber ) {
			className += ' weekNumber';
		}

		// Show / hide month icons based on min/max
		if ( minDate ) {
			var underMin = minDate >= new Date( Date.UTC(year, month, 1, 0, 0, 0 ) );

			this.dom.title.find('div.'+classPrefix+'-iconLeft')
				.css( 'display', underMin ? 'none' : 'block' );
		}

		if ( maxDate ) {
			var overMax = maxDate < new Date( Date.UTC(year, month+1, 1, 0, 0, 0 ) );

			this.dom.title.find('div.'+classPrefix+'-iconRight')
				.css( 'display', overMax ? 'none' : 'block' );
		}

		return '<table class="'+className+'">' +
				'<thead>'+
					this._htmlMonthHead() +
				'</thead>'+
				'<tbody>'+
					data.join('') +
				'</tbody>'+
			'</table>';
	},

	/**
	 * Create the calendar table's header (week days)
	 *
	 * @return {string} HTML cells for the row
	 * @private
	 */
	_htmlMonthHead: function () {
		var a = [];
		var firstDay = this.c.firstDay;
		var i18n = this.c.i18n;

		// Take account of the first day shift
		var dayName = function ( day ) {
			day += firstDay;

			while (day >= 7) {
				day -= 7;
			}

			return i18n.weekdays[day];
		};
		
		// Empty cell in the header
		if ( this.c.showWeekNumber ) {
			a.push( '<th></th>' );
		}

		for ( var i=0 ; i<7 ; i++ ) {
			a.push( '<th>'+dayName( i )+'</th>' );
		}

		return a.join('');
	},

	/**
	 * Create a cell that contains week of the year - ISO8601
	 *
	 * Based on https://stackoverflow.com/questions/6117814/ and
	 * http://techblog.procurios.nl/k/n618/news/view/33796/14863/
	 *
	 * @param  {integer} d Day of month
	 * @param  {integer} m Month of year (zero index)
	 * @param  {integer} y Year
	 * @return {string}   
	 * @private
	 */
	_htmlWeekOfYear: function ( d, m, y ) {
		var date = new Date( y, m, d, 0, 0, 0, 0 );

		// First week of the year always has 4th January in it
		date.setDate( date.getDate() + 4 - (date.getDay() || 7) );

		var oneJan = new Date( y, 0, 1 );
		var weekNum = Math.ceil( ( ( (date - oneJan) / 86400000) + 1)/7 );

		return '<td class="'+this.c.classPrefix+'-week">' + weekNum + '</td>';
	},

	/**
	 * Create option elements from a range in an array
	 *
	 * @param  {string} selector Class name unique to the select element to use
	 * @param  {array} values   Array of values
	 * @param  {array} [labels] Array of labels. If given must be the same
	 *   length as the values parameter.
	 * @private
	 */
	_options: function ( selector, values, labels ) {
		if ( ! labels ) {
			labels = values;
		}

		var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
		select.empty();

		for ( var i=0, ien=values.length ; i<ien ; i++ ) {
			select.append( '<option value="'+values[i]+'">'+labels[i]+'</option>' );
		}
	},

	/**
	 * Set an option and update the option's span pair (since the select element
	 * has opacity 0 for styling)
	 *
	 * @param  {string} selector Class name unique to the select element to use
	 * @param  {*}      val      Value to set
	 * @private
	 */
	_optionSet: function ( selector, val ) {
		var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
		var span = select.parent().children('span');

		select.val( val );

		var selected = select.find('option:selected');
		span.html( selected.length !== 0 ?
			selected.text() :
			this.c.i18n.unknown
		);
	},

	/**
	 * Create time options list.
	 *
	 * @param  {string} unit Time unit - hours, minutes or seconds
	 * @param  {integer} count Count range - 12, 24 or 60
	 * @param  {integer} val Existing value for this unit
	 * @param  {integer[]} allowed Values allow for selection
	 * @param  {integer} range Override range
	 * @private
	 */
	_optionsTime: function ( unit, count, val, allowed, range ) {
		var classPrefix = this.c.classPrefix;
		var container = this.dom.container.find('div.'+classPrefix+'-'+unit);
		var i, j;
		var render = count === 12 ?
			function (i) { return i; } :
			this._pad;
		var classPrefix = this.c.classPrefix;
		var className = classPrefix+'-table';
		var i18n = this.c.i18n;

		if ( ! container.length ) {
			return;
		}

		var a = '';
		var span = 10;
		var button = function (value, label, className) {
			// Shift the value for PM
			if ( count === 12 && typeof value === 'number' ) {
				if (val >= 12 ) {
					value += 12;
				}

				if (value == 12) {
					value = 0;
				}
				else if (value == 24) {
					value = 12;
				}
			}

			var selected = val === value || (value === 'am' && val < 12) || (value === 'pm' && val >= 12) ?
				'selected' :
				'';
			
			if (allowed && $.inArray(value, allowed) === -1) {
				selected += ' disabled';
			}

			if ( className ) {
				selected += ' '+className;
			}

			return '<td class="selectable '+selected+'">' +
				'<button class="'+classPrefix+'-button '+classPrefix+'-day" type="button" data-unit="'+unit+'" data-value="'+value+ '">' +
					'<span>'+label+'</span>'+
				'</button>' +
			'</td>';
		}

		if ( count === 12 ) {
			// Hours with AM/PM
			a += '<tr>';
			
			for ( i=1 ; i<=6 ; i++ ) {
				a += button(i, render(i));
			}
			a += button('am', i18n.amPm[0]);

			a += '</tr>';
			a += '<tr>';

			for ( i=7 ; i<=12 ; i++ ) {
				a += button(i, render(i));
			}
			a += button('pm', i18n.amPm[1]);
			a += '</tr>';

			span = 7;
		}
		else if ( count === 24 ) {
			// Hours - 24
			var c = 0;
			for (j=0 ; j<4 ; j++ ) {
				a += '<tr>';
				for ( i=0 ; i<6 ; i++ ) {
					a += button(c, render(c));
					c++;
				}
				a += '</tr>';
			}

			span = 6;
		}
		else {
			// Minutes and seconds
			a += '<tr>';
			for (j=0 ; j<60 ; j+=10 ) {
				a += button(j, render(j), 'range');
			}
			a += '</tr>';
			
			// Slight hack to allow for the different number of columns
			a += '</tbody></thead><table class="'+className+' '+className+'-nospace"><tbody>';

			var start = range !== null ?
				range :
				Math.floor( val / 10 )*10;

			a += '<tr>';
			for (j=start+1 ; j<start+10 ; j++ ) {
				a += button(j, render(j));
			}
			a += '</tr>';

			span = 6;
		}

		container
			.empty()
			.append(
				'<table class="'+className+'">'+
					'<thead><tr><th colspan="'+span+'">'+
						i18n[unit] +
					'</th></tr></thead>'+
					'<tbody>'+
						a+
					'</tbody>'+
				'</table>'
			);
	},

	/**
	 * Create the options for the month and year
	 *
	 * @param  {integer} year  Year
	 * @param  {integer} month Month (starting at 0)
	 * @private
	 */
	_optionsTitle: function () {
		var i18n = this.c.i18n;
		var min = this.c.minDate;
		var max = this.c.maxDate;
		var minYear = min ? min.getFullYear() : null;
		var maxYear = max ? max.getFullYear() : null;

		var i = minYear !== null ? minYear : new Date().getFullYear() - this.c.yearRange;
		var j = maxYear !== null ? maxYear : new Date().getFullYear() + this.c.yearRange;

		this._options( 'month', this._range( 0, 11 ), i18n.months );
		this._options( 'year', this._range( i, j ) );
	},

	/**
	 * Simple two digit pad
	 *
	 * @param  {integer} i      Value that might need padding
	 * @return {string|integer} Padded value
	 * @private
	 */
	_pad: function ( i ) {
		return i<10 ? '0'+i : i;
	},

	/**
	 * Position the calendar to look attached to the input element
	 * @private
	 */
	_position: function () {
		var offset = this.c.attachTo === 'input' ? this.dom.input.position() : this.dom.input.offset();
		var container = this.dom.container;
		var inputHeight = this.dom.input.outerHeight();

		if (container.hasClass('inline')) {
			container.insertAfter( this.dom.input );
			return;
		}

		if ( this.s.parts.date && this.s.parts.time && $(window).width() > 550 ) {
			container.addClass('horizontal');
		}
		else {
			container.removeClass('horizontal');
		}

		if(this.c.attachTo === 'input') {
			container
				.css( {
					top: offset.top + inputHeight,
					left: offset.left
				} )
				.insertAfter( this.dom.input );
		}
		else {
			container
				.css( {
					top: offset.top + inputHeight,
					left: offset.left
				} )
				.appendTo( 'body' );
		}

		var calHeight = container.outerHeight();
		var calWidth = container.outerWidth();
		var scrollTop = $(window).scrollTop();

		// Correct to the bottom
		if ( offset.top + inputHeight + calHeight - scrollTop > $(window).height() ) {
			var newTop = offset.top - calHeight;

			container.css( 'top', newTop < 0 ? 0 : newTop );
		}

		// Correct to the right
		if ( calWidth + offset.left > $(window).width() ) {
			var newLeft = $(window).width() - calWidth;

			// Account for elements which are inside a position absolute element
			if (this.c.attachTo === 'input') {
				newLeft -= $(container).offsetParent().offset().left;
			}

			container.css( 'left', newLeft < 0 ? 0 : newLeft );
		}
	},

	/**
	 * Create a simple array with a range of values
	 *
	 * @param  {integer} start   Start value (inclusive)
	 * @param  {integer} end     End value (inclusive)
	 * @param  {integer} [inc=1] Increment value
	 * @return {array}           Created array
	 * @private
	 */
	_range: function ( start, end, inc ) {
		var a = [];

		if ( ! inc ) {
			inc = 1;
		}

		for ( var i=start ; i<=end ; i+=inc ) {
			a.push( i );
		}

		return a;
	},

	/**
	 * Redraw the calendar based on the display date - this is a destructive
	 * operation
	 *
	 * @private
	 */
	_setCalander: function () {
		if ( this.s.display ) {
			this.dom.calendar
				.empty()
				.append( this._htmlMonth(
					this.s.display.getUTCFullYear(),
					this.s.display.getUTCMonth()
				) );
		}
	},

	/**
	 * Set the month and year for the calendar based on the current display date
	 *
	 * @private
	 */
	_setTitle: function () {
		this._optionSet( 'month', this.s.display.getUTCMonth() );
		this._optionSet( 'year', this.s.display.getUTCFullYear() );
	},

	/**
	 * Set the time based on the current value of the widget
	 *
	 * @private
	 */
	_setTime: function () {
		var that = this;
		var d = this.s.d;
		
		// luxon uses different method names so need to be able to call them. This happens a few time later in this method too
		var luxDT = null
		if (dateLib && dateLib == window.luxon) {
			luxDT = dateLib.DateTime.fromJSDate(d);
		}

		var hours = luxDT != null
			? luxDT.hour
			: d
				? d.getUTCHours()
				: 0;

		var allowed = function ( prop ) { // Backwards compt with `Increment` option
			return that.c[prop+'Available'] ?
				that.c[prop+'Available'] :
				that._range( 0, 59, that.c[prop+'Increment'] );
		}

		this._optionsTime( 'hours', this.s.parts.hours12 ? 12 : 24, hours, this.c.hoursAvailable )
		this._optionsTime(
			'minutes',
			60,
			luxDT != null
				? luxDT.minute
				: d
					? d.getUTCMinutes()
					: 0, allowed('minutes'),
			this.s.minutesRange
		);
		this._optionsTime(
			'seconds',
			60,
			luxDT != null
				? luxDT.second
				: d
					? d.getSeconds()
					: 0,
			allowed('seconds'),
			this.s.secondsRange
		);
	},

	/**
	 * Show the widget and add events to the document required only while it
	 * is displayed
	 * 
	 * @private
	 */
	_show: function () {
		var that = this;
		var namespace = this.s.namespace;

		this._position();

		// Need to reposition on scroll
		$(window).on( 'scroll.'+namespace+' resize.'+namespace, function () {
			that._position();
		} );

		$('div.DTE_Body_Content').on( 'scroll.'+namespace, function () {
			that._position();
		} );

		$('div.dataTables_scrollBody').on( 'scroll.'+namespace, function () {
			that._position();
		} );

		var offsetParent = this.dom.input[0].offsetParent;

		if ( offsetParent !== document.body ) {
			$(offsetParent).on( 'scroll.'+namespace, function () {
				that._position();
			} );
		}

		// On tab focus will move to a different field (no keyboard navigation
		// in the date picker - this might need to be changed).
		$(document).on( 'keydown.'+namespace, function (e) {
			if (
				e.keyCode === 9  || // tab
				e.keyCode === 27 || // esc
				e.keyCode === 13    // return
			) {
				that._hide();
			}
		} );

		// Hide if clicking outside of the widget - but in a different click
		// event from the one that was used to trigger the show (bubble and
		// inline)
		setTimeout( function () {
			$('body').on( 'click.'+namespace, function (e) {
				var parents = $(e.target).parents();

				if ( ! parents.filter( that.dom.container ).length && e.target !== that.dom.input[0] ) {
					that._hide();
				}
			} );
		}, 10 );
	},

	/**
	 * Write the formatted string to the input element this control is attached
	 * to
	 *
	 * @private
	 */
	_writeOutput: function ( focus ) {
		var date = this.s.d;

		// Use moment, dayjs or luxon if possible - otherwise it must be ISO8601 (or the
		// constructor would have thrown an error)
		// luxon uses different method names so need to be able to call them.
		var out = dateLib && dateLib == window.luxon
			? dateLib.DateTime.fromJSDate(this.s.d).toFormat(this.c.format)
			: dateLib ?
				dateLib.utc( date, undefined, this.c.locale, this.c.strict ).format( this.c.format ) :
				date.getUTCFullYear() +'-'+
					this._pad(date.getUTCMonth() + 1) +'-'+
					this._pad(date.getUTCDate());

			this.dom.input
				.val( out )
				.trigger('change', {write: date});
		
		if ( this.dom.input.attr('type') === 'hidden' ) {
			this.val(out, false);
		}

		if ( focus ) {
			this.dom.input.focus();
		}
	}
} );

/**
 * Use a specificmoment compatible date library
 */
DateTime.use = function (lib) {
	dateLib = lib;
};

/**
 * For generating unique namespaces
 *
 * @type {Number}
 * @private
 */
DateTime._instance = 0;

/**
 * Defaults for the date time picker
 *
 * @type {Object}
 */
DateTime.defaults = {
	attachTo: 'body',

	// Not documented - could be an internal property
	classPrefix: 'dt-datetime',

	// function or array of ints
	disableDays: null,

	// first day of the week (0: Sunday, 1: Monday, etc)
	firstDay: 1,

	format: 'YYYY-MM-DD',

	hoursAvailable: null,

	i18n: {
		previous: 'Previous',
		next:     'Next',
		months:   [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
		weekdays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
		amPm:     [ 'am', 'pm' ],
		hours:    'Hour',
		minutes:  'Minute',
		seconds:  'Second',
		unknown:  '-'
	},

	maxDate: null,

	minDate: null,

	minutesAvailable: null,

	minutesIncrement: 1, // deprecated

	strict: true,

	locale: 'en',

	onChange: function () {},

	secondsAvailable: null,

	secondsIncrement: 1, // deprecated

	// show the ISO week number at the head of the row
	showWeekNumber: false,

	// overruled by max / min date
	yearRange: 25
};

DateTime.version = '1.0.3';

// Global export - if no conflicts
if (! window.DateTime) {
	window.DateTime = DateTime;
}

// Make available via jQuery
$.fn.dtDateTime = function (options) {
	return this.each(function() {
		new DateTime(this, options);
	});
}

// Attach to DataTables if present
if ($.fn.dataTable) {
	$.fn.dataTable.DateTime = DateTime;
	$.fn.DataTable.DateTime = DateTime;

	if ($.fn.dataTable.Editor) {
		$.fn.dataTable.Editor.DateTime = DateTime;
	}
}

return DateTime;

}));
