"use strict";
/* jshint globalstrict: true */
/* global Modernizr */

/**
 * An implementation of the picker base class to operate against date data.
 * @module sce-picker
 * @submodule sce-picker-date
 */

var CLASS_NAME = 'scePickerDate';

//messages key
var ERROR_DATE_RANGE = "error.date.range",
    ERROR_DATE_MIN = "error.date.min",
    ERROR_DATE_MAX = "error.date.max",
    ERROR_DATE_PARSE = "error.date.type";

var _supportHtml5Date = function(inputType) {
    if (Y.UA.mobile !== null) {
        return true;
    }
    if (typeof(Modernizr) !== 'undefined' && Modernizr.inputtypes[inputType]) {
        return true;
    }
    return false;
};

var _template = Y.Handlebars.compile(
    '<div class="sce-picker-hiding-label-container">' +
        '<input type="{{#if html5}}{{type}}{{else}}text{{/if}}" class="sce-picker-date {{validationClass}}" name="{{name}}" {{#if html5}}step="any"{{else}}placeholder="{{placeholder}}"{{/if}} value="{{value}}" {{#if minDate}}min="{{minDate}}"{{/if}} {{#if maxDate}}max="{{maxDate}}"{{/if}} {{required}}>' +
        '{{#unless html5}}<label class="small sce-picker-hiding-label">{{formatLabel}}</label>{{/unless}}' +
    '</div>'
);

var _formatIntlMap = {
    'datetime-local': '%x %X',
    'date': '%x',
    'time': '%X'
};

var _formatStandardMap = {
    'datetime-local': 'YYYY-MM-DDTHH:mm:ss',
    'date': 'YYYY-MM-DD',
    'time': 'HH:mm:ss'
};

var _momentMapper = {
    'java.sql.Date': function (value) {
        return moment.utc(value);
    },
    'java.sql.Time': function (value) {
        return moment.utc(value);
    },
    'java.time.Instant': function (value) {
        return moment(value);
    },
    'java.time.LocalDate': function (value) {
        return moment(value);
    },
    'java.time.LocalTime': function (value) {
        return moment(Y.Lang.isString(value) ? '1970-01-01T' + value : value);
    },
    'java.time.LocalDateTime': function (value) {
        return moment(value);
    }
};

var _formatMomentMap = function(dateType) {
    var key = 'format.' + dateType,
        label = Y.SCE.Intl.getComponentLabel(CLASS_NAME, key);
    if (label !== key) {
        return label;
    }
    switch (dateType) {
        case 'datetime-local': return _formatMomentMap('date') + ' ' + _formatMomentMap('time');
        case 'date': return 'L';
        case 'time': return 'LTS';
    }
};

var _isLocalDate = function(dataType) {
    return dataType === 'sce.ui.Date'
        || dataType === 'sce.ui.Instant'
        || dataType === 'sce.ui.LocalDateTime'
        || dataType === 'java.time.LocalTime'
        || dataType === 'java.time.LocalDate'
        || dataType === 'java.time.LocalDateTime';
};

// code from moment.js
var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
function expandFormat(format, locale) {
    var i = 5;

    function replaceLongDateFormatTokens(input) {
        return locale.longDateFormat(input) || input;
    }

    localFormattingTokens.lastIndex = 0;
    while (i >= 0 && localFormattingTokens.test(format)) {
        format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
        localFormattingTokens.lastIndex = 0;
        i -= 1;
    }

    return format;
}

// code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
function regexEscape(s) {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}

var baseFormat = {
	'M': 'MM',
	'D': 'DD',
	'a': 'A',
	'H': 'HH',
	'h': 'hh',
	'k': 'HH',
	'kk': 'HH',
	'm': 'mm',
	's': 'ss',
	'S': 'SSS',
	'SS': 'SSS'
};
var inputMask = {
// moment format tokens
//								Token	Output
// Month							M		1 2 ... 11 12
//								Mo		1st 2nd ... 11th 12th
//								MM		01 02 ... 11 12
//								MMM		Jan Feb ... Nov Dec
//								MMMM		January February ... November December
	'MM': [ /[01]/, /\d/ ],
// Quarter						Q		1 2 3 4
//								Qo		1st 2nd 3rd 4th
// Day of Month					D		1 2 ... 30 31
//								Do		1st 2nd ... 30th 31st
//								DD		01 02 ... 30 31
	'DD': [ /[0-3]/, /\d/ ],
// Day of Year					DDD		1 2 ... 364 365
//								DDDo		1st 2nd ... 364th 365th
//								DDDD		001 002 ... 364 365
// Day of Week					d		0 1 ... 5 6
//								do		0th 1st ... 5th 6th
//								dd		Su Mo ... Fr Sa
//								ddd		Sun Mon ... Fri Sat
//								dddd		Sunday Monday ... Friday Saturday
// Day of Week (Locale)			e		0 1 ... 5 6
// Day of Week (ISO)				E		1 2 ... 6 7
// Week of Year					w		1 2 ... 52 53
//								wo		1st 2nd ... 52nd 53rd
//								ww		01 02 ... 52 53
// Week of Year (ISO)			W		1 2 ... 52 53
//								Wo		1st 2nd ... 52nd 53rd
//								WW		01 02 ... 52 53
// Year							YY		70 71 ... 29 30
//								YYYY		1970 1971 ... 2029 2030
//								Y		1970 1971 ... 9999 +10000 +10001 
//								Note: This complies with the ISO 8601 standard for dates past the year 9999
	'YY': [ /\d/, /\d/ ],
	'YYYY': [ /\d/, /\d/, /\d/, /\d/ ],
// Week Year						gg		70 71 ... 29 30
//								gggg		1970 1971 ... 2029 2030
// Week Year (ISO)				GG		70 71 ... 29 30
//								GGGG		1970 1971 ... 2029 2030
// AM/PM							A		AM PM
//								a		am pm
	'A': function(token, locale) {
        var values = [],
            collectValues = function(lowercase) {
                for (var hour = 0; hour < 24; hour++) {
                    var value = locale.meridiem(hour, 0, lowercase);
                    if (values.indexOf(value) === -1) {
                        values.push(value);
                    }
                }
            },
            minLength,
            exprs;
        if (locale.meridiem) {
            collectValues(true);
            collectValues(false);
        }
        if (values.length) {
            minLength = values.slice(1).reduce(function(result, value) {
                return value.length < result ? value.length : result;
            }, values[0].length);
            exprs = values.reduce(function(result, value) {
                for (var i = 0; i < minLength; i++) {
                    var char = value.charAt(i);
                    if (result[i] === undefined) {
                        result[i] = [];
                    }
                    if (result[i].indexOf(char) === -1) {
                        result[i].push(char);
                    }
                }
                return result;
            }, []);
            return exprs.map(function(expr) {
                return new RegExp('[' + regexEscape(expr.join('')) + ']');
            });
        }
        return [ /[AaPp]/, /[Mm]/ ];
    },
// Hour							H		0 1 ... 22 23
//								HH		00 01 ... 22 23
//								h		1 2 ... 11 12
//								hh		01 02 ... 11 12
//								k		1 2 ... 23 24
//								kk		01 02 ... 23 24
	'HH': [ /[0-2]/, /\d/ ],
	'hh': [ /[01]/, /\d/ ],
// Minute						m		0 1 ... 58 59
//								mm		00 01 ... 58 59
	'mm': [ /[0-5]/, /\d/ ],
// Second						s		0 1 ... 58 59
//								ss		00 01 ... 58 59
	'ss': [ /[0-5]/, /\d/ ],
// Fractional Second				S		0 1 ... 8 9
//								SS		00 01 ... 98 99
//								SSS		000 001 ... 998 999
//								SSSS
//								... 		000[0..] 001[0..] ... 998[0..] 999[0..]
//								SSSSSSSSS	
	'SSS': [ /\d/, /\d/, /\d/ ],
// Time Zone						z or zz	EST CST ... MST PST 
//								Note: as of 1.6.0, the z/zz format tokens have been deprecated from plain moment objects.
//										Read more about it here (https://github.com/moment/moment/issues/162).
//										However, they do work if you are using a specific time zone with the moment-timezone addon.
//								Z		-07:00 -06:00 ... +06:00 +07:00
//								ZZ		-0700 -0600 ... +0600 +0700
	'Z': [ /[-+]/, /[01]/, /\d/, ':', /[03]/, /[0]/ ],
	'ZZ': [ /[-+]/, /[01]/, /\d/, /[03]/, /[0]/ ]
// Unix Timestamp				X		1360013296
// Unix Millisecond Timestamp	x		1360013296123
};

var _getMask = function(dateType) {
    if (_supportHtml5Date(dateType)) {
        return null;
    }
    var tokens = expandFormat(_formatMomentMap(dateType), moment.localeData()).match(formattingTokens) || [],
        mask = [];
    tokens.forEach(function(token) {
        var regex = inputMask[baseFormat[token] || token];
		if (regex) {
			mask = mask.concat(Y.Lang.isFunction(regex) ? regex.call(null, token, moment.localeData()) : regex);
		} else {
			mask.push(token);
		}
    });
    return mask;
};

var _getFormatLabel = function(dateType) {
    var tokens = expandFormat(_formatMomentMap(dateType), moment.localeData()).match(formattingTokens) || [];
	return tokens.reduce(function(placeholder, token) {
	    if (!inputMask[baseFormat[token] || token]) {
	        return placeholder + ' ';
	    }
		var key = 'placeholder.' + (baseFormat[token] || token),
			label = Y.SCE.Intl.getComponentLabel(CLASS_NAME, key);
		return placeholder + (label === key ? token : label);
    }, '');
};

var _getPlaceholder = function(mask) {
    if (!mask) {
        return '';
    }
    return mask.map(function(char) {
        return (char instanceof RegExp) ? '_' : char;
    }).join('');
};

var _getFormattedDate = function(value, format, dateType, javaType) {
    var formattedDate = '';
    if (value || value === 0) {
        if (Y.Lang.isString(value)) {
            var number = Y.Number.parse(value);
            if (number) {
                value = number;
            }
        }
        formattedDate = _getMoment(value, dateType, javaType).format(format);
    }
    return formattedDate;
};

var _getMoment = function(value, dateType, javaType) {
    if (_momentMapper[javaType]) {
        return _momentMapper[javaType](value);
    }
    return dateType !== 'time' ? moment(value) : moment.utc(value);
};

/**
 * An implementation of the [picker base](SCE.Picker.Base.html) class to operate against date data:
 * in view mode it shows a short date representation of the data, while in edit and search mode it renders
 * as a _datetime-local_ HTML5 input type.
 * The date picker is subject to the [localization](SCE.Picker.Intl.html) changelang event, which changes
 * the way the date is formatted.
 * @class Date
 * @extends SCE.Picker.Base
 * @namespace SCE.Picker
 * @constructor
 * @param {Object} descriptor A hash object containing configuration and data related to the date picker.
 *     @param {Date|String} descriptor.data (optional) An initial value for the picker: input data will be fed to the
 *            native Javascript Date constructor.
 * @since 1.0
 */
Y.namespace('SCE.Picker').Date = Y.Base.create(CLASS_NAME,Y.SCE.Picker.Base, [], {

    initializer: function() {
        this.pickerDateBundlePath = Y.SCE.Intl.getComponentBundlePath(CLASS_NAME, null, null, true);
    },
    
    /**
     * Updates date data into the underlying node.
     * @method _updateNodeValue
     * @private
     */
    _updateNodeValue: function() {
        if (!this._isReadonly()) {
            var node = this.getValidationNode();
            if (node) {
                node.set('value', this.formatOutputValue());
            }
        } else if (this._node) {
            this._node.set('text', this.formatOutputValue());
        }
    },
    
    /**
     * Transforms the stored date to a text.
     * @method formatOutputValue
     * @return {String} text Representation of the stored date.
     */
    formatOutputValue: function() {
        // Use format based on date type if not specified otherwise in the format options
        // Custom format configuration must be a valid momentjs configuration
        var attributeConfig = this.getAttributeConfig(),
            dateType = attributeConfig.getDateInputType(),
            javaType = attributeConfig.get('dataType'),
            formatOptions = attributeConfig.get('formatOptions'),
            format = (formatOptions && formatOptions.format) ? formatOptions.format : _formatMomentMap(dateType);
        if (!this._isReadonly() && _supportHtml5Date(dateType)) {
            format = _formatStandardMap[dateType];
        }
        return _getFormattedDate(this.getData(), format, dateType, javaType);
    },
       
    /**
     * Creates a YUI Node representation of the contained date data.
     * @method getPicker
     * @return {Node} current picker node representation.
     */
    getPicker: function() {
        var nodeHTML,
            //correctedInputType = onDesktop ? 'text' : inputType;
            value = this.formatOutputValue(),
            config = this.getAttributeConfig(),
            dateType = config.getDateInputType(),
            javaType = config.get('dataType'),
            html5 = _supportHtml5Date(dateType),
            placeholder = this._getPlaceholder(),
            formatLabel = this._getFormatLabel(),
            constraints = config.get('constraints'),
            minDate = constraints != null && Y.SCE.Commons.resolvableAttributeSetter(constraints.minDate),
            maxDate = constraints != null && Y.SCE.Commons.resolvableAttributeSetter(constraints.maxDate);
        this._eventHandles.push(Y.SCE.Intl.on("sceIntl:changeLang", function() {
            this.render();
        }, this));
        if (!this._isReadonly()) {
            nodeHTML = _template({
                html5: html5,
                name: this.attributeName,
                type: dateType,
                value: value,
                minDate: (html5 && minDate) ? _getFormattedDate(minDate, _formatStandardMap[dateType], dateType, javaType) : null,
                maxDate: (html5 && maxDate) ? _getFormattedDate(maxDate, _formatStandardMap[dateType], dateType, javaType) : null,
                required: this._getRequired(),
                validationClass: Y.SCE.Picker.Base.CLASS_VALIDATION,
                placeholder: placeholder,
                formatLabel: formatLabel
            });
        } else {
            nodeHTML = '<span>' + value + '</span>';
        }
        return Y.Node.create(nodeHTML);
    },
    
    _getPlaceholder: function() {
        return _getPlaceholder(this._getNodeMask());
    },
    
    _getFormatLabel: function() {
        return _getFormatLabel(this.getAttributeConfig().getDateInputType());
    },
    
    /**
     * Parses a given string value.
     * @method parseData
     * @param {String} dateStr The value to be parsed.
     * @return {Date} A date object `true` or `null` if invalid
     */
    parseData: function(dateStr) {
        if (!dateStr) {
            return null;
        }
        var dateType = this.getAttributeConfig().getDateInputType(),
            javaType = this.getAttributeConfig().get('dataType'),
            outputFormat = this.getAttributeConfig().getDateFormat(),
            localDate = dateType === 'datetime-local' || _isLocalDate(javaType),
            validFormat,
            date;
        if (_supportHtml5Date(dateType)) { //check support html5 input date
            	var dateArray, timeArray;
                dateStr = dateType === 'time' ? '1970-01-01T' + dateStr : dateStr.replace(' ', 'T');
            //NOTE: Need to instantiate a new date in this standard way, because Date.parse using timezone depending from browser (e.g: chrome parse differently from explorer)
            if (dateStr.indexOf('T') !== -1) {
                var arrayDateStr = dateStr.split('T');
                dateArray = arrayDateStr[0].split('-');
                timeArray = arrayDateStr[1].split(':');
                if (localDate) {
                    date = new Date(dateArray[0], parseInt(dateArray[1], 10) - 1, dateArray[2], timeArray[0], timeArray[1], timeArray[2] || "00");
                } else {
                    date = new Date(Date.UTC(dateArray[0], parseInt(dateArray[1], 10) - 1, dateArray[2], timeArray[0], timeArray[1], timeArray[2] || "00"));
                }
            } else {
                dateArray = dateStr.split('-');
                if (localDate) {
                    date = new Date(dateArray[0], parseInt(dateArray[1], 10) - 1, dateArray[2]);
                } else {
                    date = new Date(Date.UTC(dateArray[0], parseInt(dateArray[1], 10) - 1, dateArray[2]));
                }
            }
            date = Date.parse(date);
            validFormat = (date && !isNaN(date)) || date === 0;
        } else {
            var format = _formatMomentMap(dateType),
                locale = moment.locale();
            if (localDate) {
                date = moment(dateStr, format, locale, true);
            } else if (dateType === 'time') {
                date = moment.utc(dateStr, format, locale, true).set({year:1970,month:0,date:1});
            } else {
                date = moment.utc(dateStr, format, locale, true);
            }
            validFormat = date.isValid();
            if (validFormat) {
                date = date.valueOf();
            }
        }
        if (!validFormat) {
            throw new Y.SCE.Error(ERROR_DATE_PARSE, null, null, null, this.pickerDateBundlePath);
        }
        if (outputFormat) {
            date = Y.SCE.Commons.formatMoment(moment(date), outputFormat);
        }
        return date;
    },
    
    /**
     * Validates date type
     * @method _validate
     * @param {Number|String} date date to validate in milliseconds or ISO8601 string
     * @return true if date is valid false otherwise
     */
    _validate: function(date) {
        var config = this.getAttributeConfig(),
            constraints = config.get('constraints'),
            dateType = config.getDateInputType(),
            javaType = config.get('dataType');
        if (constraints){
            var mDate = _getMoment(date, dateType, javaType),
                minDate = Y.SCE.Commons.resolvableAttributeSetter(constraints.minDate),
                maxDate = Y.SCE.Commons.resolvableAttributeSetter(constraints.maxDate);
            if (minDate && maxDate) {//check range
                if (!mDate.isBetween(minDate, maxDate)) {
                    throw new Y.SCE.Error(ERROR_DATE_RANGE, {
                        minDate : Y.SCE.Picker.Date.formatOutputValue(minDate, config),
                        maxDate : Y.SCE.Picker.Date.formatOutputValue(maxDate, config)
                    }, null, null, this.pickerDateBundlePath);
                }
            } else if (minDate) {//check minDate
                if (mDate.isBefore(minDate)) {
                    throw new Y.SCE.Error(ERROR_DATE_MIN, {
                        minDate : Y.SCE.Picker.Date.formatOutputValue(minDate, config)
                    }, null, null, this.pickerDateBundlePath);
                }
            } else if (maxDate) {//check maxDate
                if (mDate.isAfter(maxDate)) {
                    throw new Y.SCE.Error(ERROR_DATE_MAX, {
                        maxDate : Y.SCE.Picker.Date.formatOutputValue(maxDate, config)
                    }, null, null, this.pickerDateBundlePath);
                }
            }
        }
        return true;
    },

    _getNodeMask : function() {
        return _getMask(this.getAttributeConfig().getDateInputType());
    },

    _getValidationNodeValue: function() {
        var rawValue = this._validationNode.get('value'),
            uppercaseValue = rawValue.toUpperCase();
        if (rawValue !== uppercaseValue) {
            this._validationNode.set('value', uppercaseValue);
        }
        return uppercaseValue;
    }

}, {
    /**
     * Returns true if the configuration represents a date.
     * @method supportsConfiguration
     * @static
     * @param {SCE.AttributeModel} attributeConfig configuration model of attribute
     * @returns {boolean} True if the configuration refers to a date field.
     */
    supportsConfiguration: function(attributeConfig) {
        return attributeConfig.isDate();
    },

    /**
     * Return the internationalized `strftime` format string for the given attribute configuration.
     * @method getIntlFormat
     * @static
     * @param {SCE.AttributeModel} attributeConfig configuration model of attribute
     * @returns {String} The intl format string for the attribute
     */
    getIntlFormat: function(attributeConfig) {
        return _formatIntlMap[Y.SCE.Picker.Base.checkAttributeConfig(attributeConfig).getDateInputType()];
    },

    /**
     * Transforms the given data to a text.
     * @method formatOutputValue
     * @static
     * @param {Object} data Data bounded to the picker.
     * @param {SCE.AttributeModel} attributeConfig attribute configuration
     * @return {String} text Representation for the given data.
     */
    formatOutputValue : function(data, attributeConfig) {
        var attrConfig = Y.SCE.Picker.Base.checkAttributeConfig(attributeConfig),
            dateType = attrConfig.getDateInputType(),
            javaType = attrConfig.get('dataType'),
            formatOptions = attrConfig.get('formatOptions'),
            format = (formatOptions && formatOptions.format) ? formatOptions.format : _formatMomentMap(dateType);
        return _getFormattedDate(data, format, dateType, javaType);
    },

    /**
     * Transforms the given timestamp resulting from an Excel cell to a serialized date object.
     * @method fromExcel
     * @static
     * @param {Number} timestampExcel Timestamp in Excel format to serialize
     * @param {SCE.AttributeModel} attributeConfig attribute configuration
     * @return {String|Number} serialized value for the given timestamp
     */
    fromExcel: function (timestampExcel, attributeConfig) {
        var attributeConfiguration = Y.SCE.Picker.Base.checkAttributeConfig(attributeConfig),
            outputFormat = attributeConfiguration.getDateFormat(),
            dateType = attributeConfiguration.getDateInputType(),
            javaType = attributeConfiguration.get('dataType');

        // shift value to unix ts
        var timestamp = timestampExcel * 24 * 60 * 60 * 1000 + moment.utc({year:1899,month:11,day:30}).valueOf();

        // shift with local timezone
        if (['java.sql.Time', 'java.sql.Date', 'java.util.Date'].indexOf(javaType) === -1) {
            timestamp = timestamp - moment(timestamp).utcOffset() * 60000;
        }

        var date = _getMoment(timestamp, dateType, javaType);

        if (dateType === 'time') {
            date = date.set({year: 1970, month: 0, date: 1});
        }

        if (outputFormat) {
            date = Y.SCE.Commons.formatMoment(date, outputFormat);
        }

        return date.valueOf();
    },

    /**
     * Transforms the given serialized date to a value for an Excel cell.
     * @method toExcel
     * @static
     * @param {String|Number} date serialized date as Unix timestamp or string
     * @param {SCE.AttributeModel} attributeConfig attribute configuration
     * @return {Number} corresponding value for Excel cell
     */
    toExcel: function (date, attributeConfig) {
        var attributeConfiguration = Y.SCE.Picker.Base.checkAttributeConfig(attributeConfig),
            timestamp = Y.SCE.Picker.Date.parseDate(date, attributeConfiguration),
            javaType = attributeConfiguration.get('dataType'),
            timestampExcel = timestamp;

        // shift with local timezone
        if (['java.sql.Time', 'java.sql.Date', 'java.util.Date'].indexOf(javaType) === -1) {
            timestampExcel = timestamp + moment(timestamp).utcOffset() * 60000;
        }

        // try to avoid rounding error
        timestampExcel -= ( timestampExcel % 1000 );

        // shift value to excel
        timestampExcel = (timestampExcel - moment.utc({year:1899,month:11,day:30}).valueOf()) / (24 * 60 * 60 * 1000);

        return timestampExcel;
    },

    /**
     * Returns a string HTML representation of the date picker.
     * @method getPickerAsHtml
     * @param {Object} data Data bounded to the picker.
     * @param {SCE.AttributeModel} attributeConfig The configuration of attribute.
     * @return {String} HTML representation of the picker.
     * @static
     */
    getPickerAsHtml: function(data, attributeConfig) {
        return Y.SCE.Picker.Base.returnLabel(Y.SCE.Picker.Date.formatOutputValue(data, attributeConfig));
    },

    parseDate: function(data, attributeConfig) {
        var attributeConfiguration = Y.SCE.Picker.Base.checkAttributeConfig(attributeConfig);
        var dateType = attributeConfiguration.getDateInputType();
        return _getMoment(data, dateType, attributeConfiguration.get('dataType')).valueOf();
    }

});


can you explain the code above