/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: Date.js 616 2013-04-22 23:48:38Z geert $
 */

"use strict";

SUI.control.Date = SUI.defineClass(
	/** @lends SUI.control.Date.prototype */{

	/** @ignore */ baseClass: SUI.Box,

	/**
	 * @class
	 * SUI.control.Date is a control for date entry. It displays date and or
	 * time entry fields and a button for a date/time selection  box. The
	 * control works in a JavaScript setting, but also as a form field. The
	 * order in which the date fields are presented depends on the
	 * internationalization settings.
	 *
	 * @augments SUI.Box
	 *
	 * @description
	 * Create a date(time) control: a set of input fields in the order of
	 * date(time) format, together with a button for a date(time) dialog
	 * box to facilitate date entry.
	 *
	 * @constructs
	 * @param see base class
	 * @param {Date} arg.value Initial date(time) to show in the control.
	 * @param {String} arg.type "date" (default), "time" or "datetime"
	 */
	initializer: function(arg) {

		SUI.control.Date.initializeBase(this, arg);

		// set the control's type, default to date
		this._type = arg.type || "date";
		// initialize the fields array
		this._fields = [];

		// for the button set overflow to visible
		this.el().style.overflow = "visible";

		// get the internationalized date format ...
		var df = SUI.i18n.dateFormat;
		if (this._type == "datetime") {
			df = SUI.i18n.dateFormat + " " + SUI.i18n.timeFormat;
		} else if (this._type == "time") {
			df = SUI.i18n.timeFormat;
		}
		// create a hidden control that will contains a single string
		// representation of the date in the input fields of the control.
		// Note: can't add to doc tree and set type afterwards in IE
		this._hidden = new SUI.form.Input({/*parent: this*/});
		this._hidden.el().type = "hidden";
		this._hidden.el().name = arg.name || this.el().id;
		this._hidden.parent(this);

		// ... and use that set the control's fields in that order
		var l = this._addInputFields(df);

		// create a button that opens the date dialog box
		var that = this;
		this._button = new SUI.ToolbarButton({
			parent: this,
			left: l + this.BUTTON_MARGIN,
			width: this.BUTTON_SIZE,
			height: this.BUTTON_SIZE,
			top: (this.HEIGHT-this.BUTTON_SIZE)/2,
			title: "", // TODO
			icon: this._type == "time"
				? SUI.resource.calTime : SUI.resource.calDate,
			handler: function() {
				new SUI.dialog.Calendar({
					type: that._type,
					date: that._toDate(),
					onOK: function(date) {
						that._setDate(date);
				 }
				}).show();
			}
		});

		// also add the button and the hidden field so that they will be
		// handled by layOut and display
		this._fields.push(this._hidden);
		this._fields.push(this._button);

		// set the width and height of the control
		this.width(this._button.left() + this._button.width());
		this.height(this.HEIGHT);

		// if a value as given in the arguments ...
		if (arg.value) {
		 // ... put that value into the control
			this._setValue(arg.value);
		}
	},

	/**
	 * Margin between the button and the input fields.
	 */
	BUTTON_MARGIN: 7,

	/**
	 * Size (width/height) of the button
	 */
	BUTTON_SIZE: 22,

	/**
	 * Height of the control (not that the button is larger)
	 */
	HEIGHT: 20,

	/**
	 * Width of the 2 digit input fields (day, month, minutes, hours)
	 */
	WIDTH_2: 20,

	/**
	 * Width of the 4 digit input field (year)
	 */
	WIDTH_4: 36,

	/**
	 * Width between the input fields
	 */
	WIDTH_SEP: 10,

	/**
	 * Display the date control. Set the CSS size and position of the input
	 * fields and button.
	 */
	display: function() {
		this.setDim();
		for (var i=0; i<this._fields.length; i++) {
			this._fields[i].display();
		}
	},

	/**
	 * Return the first input field of the control. This is needed to set the
	 * forBox (for) field of a label field.
	 * @return {SUI.Box} the first input box of the date control.
	 */
	firstBox: function() {
		return this._fields[0];
	},

	/**
	 * Lay out the date control. Calculate the sizes and positions of the
	 * input fields and button.
	 */
	layOut: function() {
		for (var i=0; i<this._fields.length; i++) {
			this._fields[i].layOut();
		}
	},

	/**
	 * Get or set the value of the date control.
	 * @param {string|Date} v Either a JavaScript Date object or string in SQL
	 *     Date format to which the control's date will be set. Or the string
	 *     "date" or "string" to select the return type of the function. If no
	 *     parameter is given, the method returns an object. containing the
	 *     fields: date, dateStr and error.
	 * @returns {Object|string|Date} Depending on the given arguments the
	 *     return value can be an object containing the fields: date, dateStr
	 *     and error (no arguments), a Date object (argument: "date") or
	 *     a date string in SQL format (argument: "string"). If the method is
	 *     used as a setter the method returns null.
	 */
	value: function(v) {
	 // act as a getter
	 if (v === undefined) {
	   return this._getValue();
	 } else if (v === "string") {
	   return this._getValue().strDate;
	 } else if (v === "date") {
	   return this._getValue().date;
		}
	 // got here? than act as a setter
	 this._setValue(v);
	 return null;
	},

	// the button that opens the date selection dialog
	_button: null,

	// the day input field
	_day: null,

	// list of all the child boxes of this control
	_fields: null,

	// hidden input field that holds a SQL date string of value set in control
	_hidden: null,

	// the hours input field
	_hour: null,

	// the minutes input field
	_min: null,

	// the month input field
	_month: null,

	// the control's type: date time or datetime
	_type: "",

	// the year input field
	_year: null,

	// add the date input fields including the characters in between them to
	// the _fields array, and add event listeners to the input fields
	_addInputFields: function(df) {

	 // current left position of the field/text
		var l = 0;
	 // loop through the characters of the format string
		for (var i=0; i<df.length; i++) {
		 // add a input field or text for each character in format string
			switch(df.charAt(i)) {

			case "d":
			 // add a 2 character field for the day
				this.day = new SUI.form.Input(
					{parent: this, left: l, width: this.WIDTH_2 });
				this.day.el().maxLength = 2;
				this._fields.push(this.day);
				l += this.WIDTH_2;
				break;

			case "m":
			 // add a 2 character field for the month
				this.month = new SUI.form.Input(
					{parent: this, left: l, width: this.WIDTH_2 });
				this.month.el().maxLength = 2;
				this._fields.push(this.month);
				l += this.WIDTH_2;
				break;

			case "y":
			 // add a 4 character field for the year
				this.year = new SUI.form.Input(
					{parent: this, left: l, width: this.WIDTH_4 });
				this.year.el().maxLength = 4;
				this._fields.push(this.year);
				l += this.WIDTH_4;
				break;

			case "h":
			 // add a 2 character field for the hours
				this.hour = new SUI.form.Input(
					{parent: this, left: l, width: this.WIDTH_2 });
				this.hour.el().maxLength = 2;
				this._fields.push(this.hour);
				l += this.WIDTH_2;
				break;

			case "i":
			 // add a 2 character field for the minutes
				this.min = new SUI.form.Input(
					{parent: this, left: l, width: this.WIDTH_2 });
				this.min.el().maxLength = 2;
				this._fields.push(this.min);
				l += this.WIDTH_2;
				break;

			default:
			 // add a 1 character field for the (separator) character
				var tmp = new SUI.TextBox({parent: this, text: df.charAt(i),
					left: l, width: this.WIDTH_SEP, height: this.HEIGHT });
				tmp.el().style.textAlign = "center";
				this._fields.push(tmp);
				l += this.WIDTH_SEP;
				break;
			}
		}

		// set the hidden field on each "oninput" event on any of the input
		// fields
		var that = this;
		for (var i=0; i<this._fields.length; i++) {
			if (this._fields[i] instanceof SUI.form.Input) {
				SUI.browser.addEventListener(this._fields[i].el(), "input",
					function(e) {
						if (!that._setHidden()) {
							SUI.browser.noPropagation(e);
						}
					}
				);
			}
		}

		// return the total length of the controls so far
		return l;
	},

	// return the value of the control in different formats: as Date object,
	// SQL string and type "error/empty/date/time/datetime"
	_getValue: function(e) {
	 // start with the default return value
		var res = {date: null, strDate: "error", type: "error"};
		// is the control empty ...
		if (this._isEmpty()) {
		 // ... then set return values to empty ...
			res.type = "empty";
			res.strDate = "";
		} else {
		 // ... else try to construct a date value from the input fields ...
			var dt = this._toDate();
			if (dt) {
			 // (ascertain that the hidden field is set)
			 this._setHidden();
			 // ... if successful return these values
				res = {date: dt, strDate: this._hidden.el().value,
					type: this._type};
			}
		}
		// return the result
		return res;
	},

	// check if there are values entered in any of the date fields
	_isEmpty: function() {
	 // value to start with
		var res = "";
		// if we have date input fields ...
		if (this.year) {
			// ... add these values to res
			res += this.year.el().value + this.month.el().value +
				this.day.el().value;
		}
		// if we have time input fields ...
		if (this.hour) {
			// ... add these values to res
			res += this.hour.el().value + this.min.el().value;
		}
		// does a SUI.trim results in an empty string?
		return SUI.trim(res) === "";
	},

	// set the input field to the values given by the JavaScript Date object
	_setDate: function(date) {
		// if we have date input fields ...
		if (this.year) {
			if (date) {
				// ... set date fields with zero padded values ...
				this.year.el().value = SUI.date.padZero(date.getFullYear(), 4);
				this.day.el().value = SUI.date.padZero(date.getDate());
				this.month.el().value = SUI.date.padZero(date.getMonth()+1);
			} else {
				// ... else clear the values
				this.year.el().value = this.day.el().value =
					this.month.el().value = "";
			}
		}
		// if we have time input fields ...
		if (this.hour) {
			if (date) {
				// ... set time fields with zero padded values ...
				this.hour.el().value = SUI.date.padZero(date.getHours());
				this.min.el().value = SUI.date.padZero(date.getMinutes());
			} else {
				// ... else clear the values
				if (this.hour) {
					this.hour.el().value = this.min.el().value = "";
				}
			}
		}
		// set the hidden control field
		this._setHidden();
	},

	// set the hidden field to the current date selection in SQL date format
	// ("YY-MM-DD HH:II:SS"). "error" and "" are also possible values.
	_setHidden: function() {
	 // expect the worst
		var val = "error";
		// do none of the fields contain any values ...
		if (this._isEmpty()) {
		 // ... return an empty string ...
			val = "";
		} else {
			// ... get try the control's date selection ...
			var d = this._toDate();
			if (d) {
				// ... if successful set the SQL date string
				val = SUI.date.toSqlDate(d, this.hour);
			}
		}
		// set the hidden input field of the control
		this._hidden.el().value = val;
	},

	// set the value of the date control using an SQL date string or a
	// JavaScript Date object
	_setValue: function(val) {
	 // if the argument was no Date object ...
	 if (!(val instanceof Date)) {
			// ... was it null or empty? ...
			if (val === null || val === "") {
				// ... yes, then the value null will clear the fields ...
				val = null;
			} else {
				// ... no, try to convert it to a date
				val = SUI.date.parseSqlDate(val);
				if (!val) {
					throw "SUI.control.Date: "
						+ "_setValue called with invalid date";
				}
			}
	 }
	 // set the date into the control fields
	  this._setDate(val);
	},

	// get the values of the input fields and construct a JavaScript Date
	// object.
	_toDate: function() {

		// date object to return
		var dt = new Date();
		// is the date value is valid?
		var valid = true;

		// if we have date input fields ...
		if (this.year) {
		 // ... get the values ...
			var y = parseInt(this.year.el().value, 10);
			var m = parseInt(this.month.el().value, 10);
			var d = parseInt(this.day.el().value, 10);
			// ... set the values in the date object
			dt.setFullYear(y, m-1, d);
			// the JavaScript Date object corrects erroneous entry: 31th of
			// June becomes the 1th of July, so compare entered values with
			// the ones set in the date object.
			valid = valid && dt.getFullYear() === y &&
				(dt.getMonth() + 1) === m && dt.getDate() === d;
		}

		// if we have time input fields ...
		if (this.hour) {
		 // ... get the values ...
			var h = parseInt(this.hour.el().value, 10);
			if (isNaN(h)) {
				h = 0;
			}
			var i = parseInt(this.min.el().value, 10);
			if (isNaN(i)) {
				i = 0;
			}
			// ... set the values in the date object
			dt.setHours(h);
			dt.setMinutes(i);
			// the JavaScript Date object corrects erroneous entry: 14:65
			// becomes 15:05, so compare entered values with the
			// ones set in the date object.
			valid = valid && dt.getHours() === h && dt.getMinutes() === i;
		}

		// return the result
		return valid ? dt : null;
	}

});