/* 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: Box.js 743 2013-07-18 10:12:39Z geert $
 */

"use strict";

SUI.Box = SUI.defineClass(
	/** @lends SUI.Box.prototype */{

	/**
	 * @classdesc
	 * <p>SUI.Box is the basic building block of the SUI library. SUI.Box are
	 * objects that represent absolutely positioned div elements and implement
	 * the functionality for the framework to size and display them as
	 * required. SUI.Boxes are basically wrappers for HTML elements and the
	 * method <i>el()</i> will return the box's element ({@link SUI.Box#el}).
	 * </p>
	 * <p>Because we like to work with absolute distances measured in pixels
	 * you'll find the properties common to the style attributes of HTML
	 * elements like top, width, etc. (re)defined as fields of the SUI.Box
	 * class. However in the SUI.Box class no CSS lengths but only integer
	 * values are supported to simplify layout arithmetic.
	 * </p>
	 * <p>To support the layout mechanism you'll find two methods:
	 * <i>layOut</i> ({@link SUI.Box#layOut}) and <i>display</i>
	 * ({@link SUI.Box#display}). These methods will be called when
	 * rendering box objects on the browser's client area. The basic idea
	 * is that the <i>layOut</i> method will calculate the size and position
	 * of the box (or an object that inherits SUI.Box) and the <i>display</i>
	 * method will actually set the CSS properties to the size and position
	 * that was calculated by the <i>layOut</i> method during the lay out
	 * phase.
	 * </p>
	 * <p>The reason to separate between these two operations is that it is
	 * possible that layout operations interact: trying to set a box to
	 * a certain width might be readjusted later in the layout process due
	 * to a child box that has a larger minimum width set. We first want to
	 * fully calculate the layout before starting the rendering process. It
	 * is expected that this will result in a snappier GUI experience and
	 * that's what we're after.
	 * </p>
	 * <p>Boxes can be anchored. This only has meaning if the box is a child
	 * box of a {@link SUI.AnchorLayout}. When anchored a child box of
	 * a container will set its size and position relative to the
	 * container's sides for the given anchor.
	 * </p>
	 * <p>Suppose all sides (left, top, bottom, right) of a box are set to
	 * 10 and the box is anchored to all sides of the container box
	 * (<i>{left: true, right: true, top: true, bottom: true}</i>).
	 * The result will be a box on the container's client area with a
	 * 10 pixel margin, no matter how the parent container is (and will be)
	 * sized, thus ignoring the box's width and height settings. Setting
	 * the bottom anchor to false will keep the box 10 pixels from the top,
	 * right and left sides of the container but the box height setting will
	 * now be respected.
	 * </p>
	 * <p>SUI.Box objects also offer a way to a attach events. More complex
	 * derivatives of SUI.Box will use these: f.i. an subclass of SUI.Box might
	 * implement an 'onLoad' event because it loads its contents using an XHR
	 * request.
	 * </p>
	 * 
	 * @summary
	 * A SUI.Box represents a box structure and is the basic building block of
	 * the SUI library.
	 *
	 * @description
	 * Create a SUI.Box component.
	 *
	 * @constructs
	 *
	 * @param {Object} [arg.elemAttr] Additional (element) attributes for the
	 *    box's element given as an object containing name and value pairs.
	 * @param {Object} [arg.anchor={left: true, top: true}] The anchors for
	 *    the box.
	 * @param {SUI.Border} [arg.border=new SUI.Border()] the border (width)
	 *    of the box. Note: don't edit the border's members, you'll edit the
	 *    boxes prototype. Assign a new SUI.Border object instead.
	 * @param {int} [arg.bottom=0] The bottom position of the box.
	 * @param {int} [arg.height=0] The height of the box.
	 * @param {int} [arg.id=auto generated] The id of the HTML element of
	 *    the box.
	 * @param {int} [arg.left=0] The left position of the box.
	 * @param {int} [arg.maxHeight=1000000] The maximum height of the box.
	 * @param {int} [arg.minHeight=0] The minimal height of the box.
	 * @param {int} [arg.maxWidth=100000] The maximal width of the box.
	 * @param {int} [arg.minWidth=0] The minimal width of the box.
	 * @param {SUI.Padding} [arg.padding=new SUI.Padding()] the padding of
	 *    the box. Note: don't edit the padding's members, you'll edit the
	 *    boxes prototype. Assign a new SUI.Padding object instead.
	 * @param {SUI.Box} [arg.parent] the parent box of the box.
	 * @param {int} [arg.right=0] The right position of the box.
	 * @param {String} [arg.tag="DIV"] An alternative tag name for the box.
	 * @param {int} [arg.top=0] The top position of the box.
	 * @param {int} [arg.width=0] The width of the box.
	 *
	 * @exception {String} If the box was created without argument object.
	 *    Note: an empty argument object is allowed.
	 */
	initializer: function(arg) {

		// no argument object is an error in the SUI library
		if (!arg) {
			throw "creating a box without argument object";
		}

		// anchors default to left and right
		this.anchor = {left: true, right: false, top: true, bottom: false};

		// start with an empty bin
		this.bin = [];

		// create the box's element, allow for non-div elements too
		this._el = SUI.browser.createElement(
			arg.tag || null, arg.elemAttr || null);

		// set the element's id
		if (arg.id) {
			this._el.id = arg.id;
		}

		// set properties from arguments
		if (arg.anchor) {
			this.anchor = arg.anchor;
		}
		if (arg.bottom) {
			this.bottom(arg.bottom);
		}
		if (arg.left) {
			this.left(arg.left);
		}
		if (arg.right) {
			this.right(arg.right);
		}
		if (arg.top) {
			this.top(arg.top);
		}
		if (arg.maxHeight) {
			this.maxHeight(arg.maxHeight);
		}
		if (arg.minHeight) {
			this.minHeight(arg.minHeight);
		}
		if (arg.minWidth) {
			this.minWidth(arg.minWidth);
		}
		if (arg.maxWidth) {
			this.maxWidth(arg.maxWidth);
		}
		if (arg.border) {
			this.border(arg.border);
		}
		if (arg.padding) {
			this.padding(arg.padding);
		}

		// note: set height and weight after max/min setting
		if (arg.height) {
			this.height(arg.height);
		}
		if (arg.width) {
			this.width(arg.width);
		}

		// note: set parent after element node was created
		if (arg.parent) {
			this.parent(arg.parent);
		}
	},

	/**
	 * Get the absolute position (top/left) of this box on the page.
	 * Note: You can only use this function if the HTML content of the page
	 * is fully rendered.
	 */
	absPos: function() {
		var l = 0, t = 0, x = 0;
		var el = this._el;
		// Note that we can use computed style because offsetLeft and
		// offsetRight are computed members themselves are computed members
		// themselves
		if (el.offsetParent) {
			do {
				l += el.offsetLeft;
				x = parseInt(
					SUI.browser.currentStyle(el, "borderLeftWidth"), 10);
				l += (isNaN(x) ? 0 : x);
				t += el.offsetTop;
				x = parseInt(
					SUI.browser.currentStyle(el, "borderTopWidth"), 10);
				t += (isNaN(x) ? 0 : x);
			} while (null !== (el = el.offsetParent));
		}

		return {l:l,t:t};
	},

	/**
	 * Add a CSS class name to the class list of the HTML element associated
	 * with the box.
	 * @param {String} cls the CSS class name to add
	 */
	addClass: function(cls) {
		SUI.style.addClass(this._el, cls);
	},

	/**
	 * Add/register a listener function. This way it is possible to register
	 * more than one listener function on one target.
	 * @param {String} type The listener type (i.e. "onClick", "onOK", etc).
	 * @param {Function} listener The listener function.
	 */
	addListener: function(type, listener) {
		// if the listeners object is not initialized the do it now
		if (!this.listeners) {
			this.listeners = {};
		}
		// if there are no listeners for this type create the listeners array
		if (!this.listeners[type]) {
			this.listeners[type] = [];
		}
		// add the listener
		this.listeners[type].push(listener);
	},

	/**
	 * Get or set the border definition of the box.
	 * @param {SUI.Border} [b] The new border definition (or none to use
	 *    this method as a getter).
	 * @return {SUI.Border} the border definition of the box (or null if this
	 *    method was used as a setter).
	 */
	border: function(b) {

		if (b !== undefined && typeof b === "string") {
			throw "Convert to type Border please";
		}

		return b !== undefined ? (this._border = b) && null : this._border;
	},

	/**
	 * Call a listener function. Execute the default and additional listener
	 * functions. Note: the framework should not execute the default listeners
	 * directly but always through this method to ensure the execution of
	 * additional listener functions.
	 * @param {String} type The listener type (i.e. "onClick", "onOK", etc).
	 */
	callListener: function(type) {
		// get the arguments for the listener: current arguments minus 'type'
		var p = [].slice.call(arguments, 1);
		// execute the default listener if any
		if (this[type]) {
			this[type].apply(this, p);
		}
		// check the additional listeners ...
		if (this.listeners) {
			if (this.listeners[type]) {
				if (this.listeners[type].length) {
					// ... and if there are any, execute them
					for (var i=0; i<this.listeners[type].length; i++) {
						this.listeners[type][i].apply(this, p);
					}
				}
			}
		}
	},

	/**
	 * Get or set the bottom of the box.
	 * @param {int} [b] The new bottom of the box (or none to use this
	 *    method as a getter).
	 * @return {int} The bottom of the box (or null if this method was used as
	 *    a setter).
	 */
	bottom: function(b) {
		return b !== undefined ? (this._bottom = b) && null : this._bottom;
	},


	/**
	 * Get the client height of the box. The client height is the height of
	 * the box minus the top and bottom border and padding width.
	 * @return {int} The client height of the box.
	 */
	clientHeight: function() {
		return this.height() - (this._border ? this._border.height : 0)
			- (this._padding ? this._padding.height : 0);
	},

	/**
	 * Get the client width of the box. The client width is the width of
	 * the box minus the left and right border and padding width.
	 * @return {int} The client width of the box.
	 */
	clientWidth: function() {
		return this.width() - (this._border ? this._border.width : 0)
			- (this._padding ? this._padding.width : 0);
	},

	/**
	 * Display the box. Set the CSS positions of the element's box(es) and
	 * of the children of the box.
	 */
	display: function() {
		if (this.width() > 0 && this.height() > 0) {
			this.setDim();
		}
	},

	/**
	 * Draw the box on the screen. It executes a two phase process: a layout
	 * phase in which the size and positions of the box (and of it's contents,
	 * for more complex derivatives) is calculated and a display phase in
	 * which the CSS size and position of the box's (and possible it's
	 * child elements) is set.
	 */
	draw: function() {
		this.layOut();
		this.display();
	},

	/**
	 * Get the HTML element node of the box.
	 * @return {HTMLElementNode} the HTML element node of the box.
	 */
	el: function() {
		return this._el;
	},

	/**
	 * Get or set the height of the box.
	 * @param {int} [h] The new height of the box (or none to use this
	 *    method as a getter).
	 * @return {int} The height of the box (or null if this method was used as
	 *    a setter).
	 */
	height: function(h) {
		if (h !== undefined) {
			h = h < this._minHeight ? this._minHeight :
				h > this._maxHeight ? this._maxHeight : h;
			// TODO: checking for errors
			if (isNaN(h)) {
				console.log("height: NaN");
			}
			if (h === undefined) {
				console.log("height: undefined");
			}
			this._height = h;
			return null;
		} else {
			return this._height;
		}
	},

	/**
	 * Lay out the box. Calculate the position of the box and its contents. The
	 * layOut function of a simple box does nothing, but it's important for
	 * more complex objects extended from SUI.Box. The layOut mechanism will
	 * set the size and position of the child boxes of the box based on the
	 * on the available space and within the restrictions of the box's minimum
	 * and maximum width and height. So the job of the overridden layOut method
	 * is to recalculate the size and position of all the child elements of
	 * the box when the layout manager sets the size of the box and calls the
	 * box's layOut method.
	 */
	layOut: function() {
	},

	/**
	 * Get or set the left of the box.
	 * @param {int} [l] The new left of the box (or none to use this
	 *    method as a getter).
	 * @return {int} The left of the box (or null if this method was used as
	 *    a setter).
	 */
	left: function(l) {
		return l !== undefined ? (this._left = l) && null : this._left;
	},

	/**
	 * Get or set the maximum height of the box.
	 * @param {int} [mh] The new maximum height of the box (or none to use
	 *    this method as a getter).
	 * @return {int} The maximum height of the box (or null if this method was
	 *    used as a setter).
	 */
	maxHeight: function(mh) {
		if (mh !== undefined) {
			this._maxHeight = mh;
			if (this._height > this._maxHeight) {
				this._height = this._maxHeight;
			}
			return null;
		} else {
			return this._maxHeight;
		}
	},

	/**
	 * Get or set the maximum width of the box.
	 * @param {int} [mw] The new maximum width of the box (or none to use
	 *    this method as a getter).
	 * @return {int} The maximum width of the box (or null if this method was
	 *    used as a setter).
	 */
	maxWidth: function(mw) {
		if (mw !== undefined) {
			this._maxWidth = mw;
			if (this._width > this._maxWidth) {
				this._width = this._maxWidth;
			}
			return null;
		} else {
			return this._maxWidth;
		}
	},

	/**
	 * Get or set the minimum height of the box.
	 * @param {int} [mh] The new minimum height of the box (or none to use
	 *    this method as a getter).
	 * @return {int} The minimum height of the box (or null if this method was
	 *    used as a setter).
	 */
	minHeight: function(mh) {
		if (mh !== undefined) {
			this._minHeight = mh;
			if (this._height < this._minHeight) {
				this._height = this._minHeight;
			}
			return null;
		} else {
			return this._minHeight;
		}
	},

	/**
	 * Get or set the minimum width of the box.
	 * @param {int} [mw] The new minimum width of the box (or none to use
	 *    this method as a getter).
	 * @return {int} The minimum width of the box (or null if this method was
	 *    used as a setter).
	 */
	minWidth: function(mw) {
		if (mw !== undefined) {
			this._minWidth = mw;
			if (this._width < this._minWidth) {
				this._width = this._minWidth;
			}
			return null;
		} else {
			return this._minWidth;
		}
	},

	/**
	 * Get or set the padding definition of the box.
	 * @param {SUI.Padding} [p] The new padding definition (or none to use
	 *    this method as a getter).
	 * @return {SUI.Padding} The padding definition of the box (or null if
	 *    this method was used as a setter).
	 */
	padding: function(p) {
		if (p !== undefined) {
			if (typeof p === "string") {
				throw "Convert to type Padding please";
			}
			this._padding = p;
			return null;
		} else {
			return this._padding;
		}
	},

	/**
	 * Get or set the parent box of the box. When setting the parent the box's
	 * element node will be appended to the parent box's HTML element node.
	 * @param {SUI.Box} p The parent box for this box (or none to use
	 *    this method as a getter).
	 * @return {SUI.Box} The parent box of the box (or null if this method was
	 *    used as a setter).
	 */
	parent: function(p) {
		if (p !== undefined) {
			this._parent = p;
			this._parent.el().appendChild(this._el);
			return null;
		} else {
			return this._parent;
		}
	},

	/**
	 * Remove a box from the DOM tree.
	 */
	removeBox: function() {
		this._parent.el().removeChild(this._el);
	},

	/**
	 * Remove a CSS class name from the class list of the HTML element
	 * associated with the box.
	 * @param {String} cls The CSS class name to remove.
	 */
	removeClass: function(cls) {
		SUI.style.removeClass(this._el, cls);
	},

	/**
	 * Get or set the right of the box.
	 * @param {int} r The new right of the box (or none to use this
	 *    method as a getter).
	 * @return {int} The right of the box (or null if this method was used
	 *    as a setter).
	 */
	right: function(r) {
		return r !== undefined ? (this._right = r) && null : this._right;
	},

	/**
	 * Set the CSS dimensions of this box and it's borders and padding. This is
	 * typically used in display functions to display boxes at the size and
	 * position that was calculated during layout.
	 */
	setDim: function() {
		this.setPos();

		// get the control's client width
		var cw = this.clientWidth();
		// TODO: cw is sometimes null: in what case?
		// Answer: Select list on the link dialog had no initial width
		// Do or don't we want that?
		if (isNaN(cw)) {
			console.log(cw);
			cw = 0;
		}
		// if it is negative try to sacrifice some of the padding
		if (cw <0) {
			this._padding.growW(cw);
			cw = 0;
		}
		// set the CSS width to the client width
		this._el.style.width = cw+"px";

		// get the control's client height
		var ch = this.clientHeight();
		// if it is negative try to sacrifice some of the padding
		if (ch <0) {
			this._padding.growH(ch);
			ch = 0;
		}
		// set the CSS height to the client height
		this._el.style.height = ch+"px";

		// set the CSS border widths
		if (this._border) {
			this._border.set(this._el);
		}

		// set the CSS padding widths
		if (this._padding) {
			this._padding.set(this._el);
		}

	},

	/**
	 * Set the CSS postion of this box. This is typically used in display
	 * functions to display boxes at position that was calculated during
	 * layout. Note: use setDim if you want to set the position and size
	 * of the box.
	 */
	setPos: function() {
		// set the top and left position of the box
		var p = this.parent() instanceof SUI.Box && this.parent().padding();
		this._el.style.top = ((p ? p.top : 0) + this.top())+"px";
		this._el.style.left = ((p ? p.left : 0) + this.left())+"px";
		//this._el.style.top = this.top()+"px";
		//this._el.style.left = this.left()+"px";
	},

	/**
	 * Set the top, left, width and height of a box in one go.
	 * @param {int|SUI.Box} t The new top of the box or another
	 *    reference box to copy the values from.
	 * @param {int} [l] The new left of the box (if the t parameter wasn't a
	 *    reference Box).
	 * @param {int} [w] The new width of the box (if the t parameter wasn't a
	 *    reference Box).
	 * @param {int} [h] The new length of the box (if the t parameter wasn't a
	 *    reference Box).
	 */
	setRect: function(t,l,w,h) {
		if (1 === arguments.length) {
			this.top(t.top());
			this.left(t.left());
			this.width(t.width());
			this.height(t.height());
		} else {
			this.top(parseInt(t, 10));
			this.left(parseInt(l, 10));
			this.width(parseInt(w, 10));
			this.height(parseInt(h, 10));
		}
	},

	/**
	 * Get or set the top of the box.
	 * @param {int} [t] The new top of the box (or none to use this method
	 *    as a getter).
	 * @return {int} The top of the box (or null if this method was used as
	 *    a setter).
	 */
	top: function(t) {
		return t !== undefined ? (this._top = t) && null : this._top;
	},

	/**
	 * Get or set the width of the box.
	 * @param {int} [w] The new width of the box (or none to use this method
	 *    as a getter).
	 * @return {int} The width of the box (or null if this method was used as
	 *    a setter).
	 */
	width: function(w) {
		if (w !== undefined) {
			w = w < this._minWidth ? this._minWidth :
				w > this._maxWidth ? this._maxWidth : w;
			// TODO: checking for errors
			if (isNaN(w)) {
					console.log("width: NaN");
			}
			this._width = w;
			return null;
		} else {
			return this._width;
		}
	},

	/**
	 * The box's border.
	 * @type SUI.Border
	 * @private
	 */
	_border: new SUI.Border(),

	/**
	 * The bottom position of the box.
	 * @type int
	 * @private
	 */
	_bottom: 0,

	/**
	 * The box's element node.
	 * @type HTMLElementNode
	 * @private
	 */
	_el: null,

	/**
	 * The height of the box.
	 * @type int
	 * @private
	 */
	_height: 0,

	/**
	 * The left position of the box.
	 * @type int
	 * @private
	 */
	_left: 0,

	/**
	 * The listeners array.
	 * @type int
	 * @private
	 */
	_listeners: null,

	/**
	 * The maximum height of the box.
	 * @type int
	 * @private
	 */
	_maxHeight: 1000000,

	/**
	 * The maximum width of the box.
	 * @type int
	 * @private
	 */
	_maxWidth: 100000,

	/**
	 * The minimum height of the box.
	 * @type int
	 * @private
	 */
	_minHeight: 0,

	/**
	 * The minimum width of the box.
	 * @type int
	 * @private
	 */
	_minWidth: 0,

	/**
	 * The box's padding.
	 * @type SUI.Padding
	 * @private
	 */
	_padding: new SUI.Padding(),

	/**
	 * The box's parent element.
	 * @type SUI.Box
	 * @private
	 */
	_parent: null,

	/**
	 * The right position of the box.
	 * @type int
	 * @private
	 */
	_right: 0,

	/**
	 * The top position of the box.
	 * @type int
	 * @private
	 */
	_top: 0,

	/**
	 * The width of the box.
	 * @type int
	 * @private
	 */
	_width: 0

});