define('jls/gui/Element', [
  'jls/lang/Class',
  'jls/lang/Exception',
  'jls/lang/Logger',
  'jls/gui/Style',
  'jls/gui/Layout',
  'jls/util/EventBroker',
  'jls/util/Resource'
], function (
  Class,
  Exception,
  Logger,
  Style,
  Layout,
  EventBroker,
  Resource
) {

var Element;
/**
 * @namespace Provides graphical user interface.
 * @see jls.gui.Element
 * @name jls.gui
 */

Element = Class.create( /** @lends jls.gui.Element.prototype */
{
    /**
     * Creates an element.
     * 
     * @param {Object} [parameters] The element parameters that is the description of the element to create.
     * @param {Object} [parameters.attributes] The element attributes.
     * @param {Object} [parameters.style] The style properties for this element.
     * @param {Array} [parameters.children] The element children.
     * @param {Object} [parameters.events] The event description to observe on this element.
     * @param {jls.util.EventBroker} [parameters.broker] The event broker associated with this element.
     * @param {jls.util.Resource} [parameters.resource] The resource associated with this element.
     * @param {jls.gui.Element} [parent] The parent.
     * @constructs
	 * @class This class represents a graphical element.
     */
    initialize : function(parameters, parent) {
        parameters = parameters || {};
        this._children = [];
        this._parent = parent || null;
        this._eventsHandlers = {};
        this._preAttributes = {};
        this._eventBroker = parameters.broker || null;
        this._resource = parameters.resource || null;
        // Default parameters
        this._title = undefined;
        this._id = undefined;
        // Create style
        this._style = new Style(parameters.style);
        // Register style handler
    	this.getStyle().setHandler(this.onStyleChange.bind(this));
        if (this._parent) {
        	this._style.setParent(this._parent.getStyle());
        }
        // Create default layout
    	this._layout = this.createDefaultLayout();
    	// Set bounds
        this._x = this._y = this._w = this._h = 0;
        if (parameters.style) {
            this._x = parameters.style.left;
            this._y = parameters.style.top;
            this._w = parameters.style.width;
            this._h = parameters.style.height;
        }
        // Preset the attributes
        if (parameters.attributes) {
            this.setAttributes(parameters.attributes);
        }
        // Create the concrete element
        this.onCreate();
        // Add this element to its parent
        if (this._parent != null) {
            this._parent.addChild(this);
        }
        // Set the attributes
        var preAttributes = this._preAttributes;
        this._preAttributes = null;
        this.setAttributes(preAttributes);
        // Activate the style properties
        this._style.activate();
        // Add the children
        if (Object.isArray(parameters.children)) {
            this.addChildren(parameters.children);
        }
        // Add the events
        if (parameters.events) {
            for (var type in parameters.events) {
                this.observe(type, parameters.events[type]);
            }
        }
        if (this._layout != null) {
        	this._layout.onCreate();
        }
    },
    onCreate : Class.emptyFunction,
    getParent : function() {
        return this._parent;
    },
    getEventBroker : function() {
        if (this._eventBroker != null) {
            return this._eventBroker;
        }
        if (this._parent != null) {
            var eb = this._parent.getEventBroker();
            if (eb != null) {
                return eb;
            }
        }
        return Element.DEFAULT_EVENT_BROKER;
    },
    setEventBroker : function(eventBroker) {
        this._eventBroker = eventBroker;
    },
    getResource : function() {
        if (this._resource != null) {
            return this._resource;
        }
        if (this._parent != null) {
            var res = this._parent.getResource();
            if (res != null) {
                return res;
            }
        }
        return Element.DEFAULT_RESOURCE;
    },
    setResource : function(resource) {
        this._resource = resource;
    },
    getResourceLabel : function(text) {
        if ((typeof text != 'string') || (! text.startsWith('#'))) {
            return text;
        }
        return this.getResource().get(text.substring(1));
    },
    /*
     * Attributes
     */
    /**
     * Sets an attribute.
     *
     * @param {String} key The attribute key.
     * @param {String} value The attribute value.
     */
    setAttribute : function(key, value) {
        if (this._preAttributes) {
            //Logger.getInstance().trace('pre-setAttribute(' + key + ', "' + value + '")');
        	if (typeof value != 'undefined') {
        		this._preAttributes[key] = value;
        	} else {
            	delete this._preAttributes[key];
        	}
        	return this;
        }
        //Logger.getInstance().trace('setAttribute(' + key + ', "' + value + '")');
        /* look for a setter method in this */
        var setter = key.camelize('set');
        //Logger.getInstance().trace('setter: "' + setter + '"');
        if (Object.isFunction(this[setter])) {
            this[setter](value);
            return this;
        }
        this.onSetAttribute(key, value);
        return this;
    },
    onSetAttribute : Class.emptyFunction,
    /**
     * Returns an attribute.
     *
     * @param {String} key The attribute key.
     * @returns {String} The attribute value.
     */
    getAttribute : function(key) {
        //Logger.getInstance().trace('getAttribute(' + key + ')');
        if (this._preAttributes) {
        	var value = null;
            if (key in this._preAttributes) {
            	value = this._preAttributes[key];
            	delete this._preAttributes[key];
            }
            return value;
        }
        /* look for a getter method in this */
        var getter = key.camelize('get');
        //Logger.getInstance().trace('getter: "' + getter + '"');
        if (Object.isFunction(this[getter])) {
            return this[getter]();
        }
        return this.onGetAttribute(key);
    },
    onGetAttribute : Class.emptyFunction,
    /**
     * Sets some attributes.
     *
     * @param {Object} attributes The attributes to set.
     */
    setAttributes : function(attributes) {
        for (var k in attributes) {
            this.setAttribute(k, attributes[k]);
        }
        return this;
    },
    /**
     * Returns the element style.
     * 
     * @returns {jls.gui.Style} The element style.
     */
    getStyle : function() {
        return this._style;
    },
    // onStyleChange(key, oldValue, newValue)
    onStyleChange : Class.emptyFunction,
    /**
     * Returns the layout associated with this element.
     * 
     * @returns {jls.gui.Layout} The element layout or null if there is no layout.
     */
    getLayout : function() {
        return this._layout;
    },
    createDefaultLayout : function() {
        return null;
    },
    /**
     * Sets the layout for this element.
     * 
     * @param {jls.gui.Layout} layout The layout to associate to this element.
     */
    setLayout : function(layout) {
    	if (layout != null) {
    		if (typeof layout == 'string') {
    			var layoutClass = require(layout);
    			if (layoutClass == null) {
    			    throw new Exception('Invalid layout (' + layout + ')');
    			}
    			layout = new layoutClass(this);
    		} else if (layout instanceof Layout) {
    			layout.setElement(this);
    		} else {
    			throw new Exception('Invalid layout');
    		}
    	}
        this._layout = layout;
        return this;
    },
    /*
     * Element hierarchy
     * TODO Element should be split in Element/Container
     */
    /**
     * Adds a child.
     *
     * @param {Object} child The child to add.
     */
    addChild : function(child) {
        if (child instanceof Element) {
            //Logger.getInstance().trace('addChild for jls.gui.Element');
            // TODO if the child already have a parent remove it
            child._parent = this;
            this._children.push(child);
        } else if ((typeof child == 'object') && ('classname' in child)) {
            //Logger.getInstance().trace('addChild for object');
            child = Element.create(child, this);
            return child;
        } else {
            throw new Exception('don\'t know how to add "' + (typeof child) + '" child type');
        }
        if (this._layout != null) {
        	this._layout.onAddChild(child);
        }
    	this.onAddChild(child);
        return child;
    },
    onAddChild : Class.emptyFunction,
    /**
     * Adds children.
     *
     * @param {Object} children The children to add.
     */
    addChildren : function(children) {
        for (var i = 0; i < children.length; i++) {
            this.addChild(children[i]);
        }
        return this;
    },
    insertChild : function(index, child) {
        Logger.getInstance().trace('insertChild(' + index + ')');
        if (! (child instanceof Element)) {
            throw new Exception('don\'t know how to insert "' + (typeof child) + '" child type');
        }
        if ((index < 0) || (index > this._children.length)) {
            throw new Exception('index out of bounds');
        }
        if (index == this._children.length) {
            return this.addChild(child);
        }
        child._parent = this;
        this._children.splice(index, 0, child);
        if ((this._layout != null) && ('onInsertChild' in this._layout)) {
            this._layout.onInsertChild(index, child);
        }
        this.onInsertChild(index, child);
        return child;
    },
    onInsertChild : Class.emptyFunction,
    getChildIndex : function(child) {
        var index = this._children.length - 1;
        for (; (index >= 0) && (this._children[index] !== child); index--);
        return index;
    },
    /**
     * Removes a child.
     *
     * @param {Object} child The child to remove.
     */
    removeChild : function(child) {
        var index = 0;
        if (typeof child == 'number') {
            index = child;
            child = this._children[index];
        } else {
            index = this.getChildIndex(child);
            if (index < 0) {
                throw new Exception('Child not found');
            }
        }
        this._children.splice(index, 1);
        //Logger.getInstance().trace('removeChild(' + index + ')');
        if (child instanceof Element) {
        	child._parent = null;
        	child.onDestroy();
        }
        if (this._layout != null) {
        	this._layout.onRemoveChild(child);
        }
        this.onRemoveChild(child);
        return child;
    },
    onRemoveChild : Class.emptyFunction,
    /**
     * Removes all children.
     *
     */
    removeChildren : function() {
        for (var i = this._children.length - 1; i >= 0; i--) {
            this.removeChild(i);
        }
        return this;
    },
    /**
     * Returns all children.
     *
     */
    getChildren : function() {
        return this._children;
    },
    getChildCount : function() {
        return this._children.length;
    },
    /**
     * Gets a child.
     *
     * @param {Number} index The child index.
     * @returns {Object} The child.
     */
    getChild : function(index) {
        return this._children[index];
    },
    /**
     * Destroys this element.
     *
     */
    destroy : function() {
        //Logger.getInstance().trace('destroy()');
        if (this.getParent() != null) {
            this.getParent().removeChild(this);
        } else {
            this.onDestroy();
        }
        return this;
    },
    onDestroy : Class.emptyFunction,
    /*
     * Event.
     * TODO Move this in a specialized event broker class.
     */
    dispatch : function(event) {
        if (event.type in this._eventsHandlers) {
            var handlers = this._eventsHandlers[event.type];
            for (var i = handlers.length - 1; i >= 0; i--) {
                handlers[i](event);
            }
        }
        return this;
    },
    onStartObserving: Class.emptyFunction,
    onStopObserving: Class.emptyFunction,
    /**
     * Register an event handler.
     *
     * @param {String} type The event type to observe.
     * @param {Function} handler The handler to register.
     * @returns {Function} The handler.
     */
    observe: function(type, handler) {
        if (Object.keys(this._eventsHandlers).length == 0) {
            this.onStartObserving();
        }
        if (! (type in this._eventsHandlers)) {
            this._eventsHandlers[type] = [];
        }
        if (typeof handler == 'string') {
            // TODO Use the event broker directly
            //handler = this.getEventBroker().callback(handler);
        	var name = handler;
        	var self = this;
        	handler = function(message) {
        		self.getEventBroker().publish(name, message);
        	};
        }
        this._eventsHandlers[type].push(handler);
        return handler;
    },
    isObserving: function(type) {
        return type && (type in this._eventsHandlers);
    },
    /**
     * Unregister an event handler.
     *
     * @param {String} type The event type to unregister.
     * @param {Function} [handler] The handler to unregister.
     * @returns {Array} The removed handlers.
     */
    unobserve: function(type, handler) {
        var removed = [];
        if (type in this._eventsHandlers) {
            if (handler) {
                var handlers = this._eventsHandlers[type];
                for (var i = handlers.length - 1; i >= 0; i--) {
                    if (handler === handlers[i]) {
                        removed.push(handlers[i]);
                        handlers.splice(i, 1);
                    }
                }
                if (handlers.length == 0) {
                    delete this._eventsHandlers[type];
                }
            } else {
                delete this._eventsHandlers[type];
            }
        }
        if (Object.keys(this._eventsHandlers).length == 0) {
            this.onStopObserving();
        }
        return removed;
    },
    /*
     * Update
     */
    // TODO doLayout ?
    update : function() {
        if (this._layout != null) {
        	this._layout.onUpdate();
        }
    },
    /*
     * Default attributes
     */
    getX : function() {
        return this._x;
    },
    getY : function() {
        return this._y;
    },
    getW : function() {
        return this._w;
    },
    getH : function() {
        return this._h;
    },
    getBounds : function() {
        return [this._x, this._y, this._w, this._h];
    },
    setBounds : function(rect) {
        this._x = rect[0];
        this._y = rect[1];
        this._w = rect[2];
        this._h = rect[3];
        return this;
    },
    getLocation : function() {
        return [this._x, this._y];
    },
    setLocation : function(point) {
        this._x = point[0];
        this._y = point[1];
        return this;
    },
    getSize : function() {
        return [this._w, this._h];
    },
    setSize : function(dim) {
        this._w = dim[0];
        this._h = dim[1];
        return this;
    },
    getClientOffset : function() {
        return [0, 0];
    },
    /*getClientBounds : function() {
        return this.getBounds();
    },*/
    getClientSize : function() {
        return this.getSize();
    },
    getTitle : function() {
        return this._title;
    },
    setTitle : function(title) {
        this._title = title;
        return this;
    },
    getId : function() {
        return this._id;
    },
    setId : function(id) {
        this._id = id;
        return this;
    },
    getById : function(id) {
        var children = this.getChildren();
        for ( var i = 0; i < children.length; i++) {
            var child = children[i];
            if (child.getAttribute('id') == id) {
                return child;
            }
        }
        for ( var i = 0; i < children.length; i++) {
            var e = children[i].getById(id);
            if (e != null) {
                return e;
            }
        }
        return null;
    }
});

Object.extend(Element,
{
    DEFAULT_EVENT_BROKER: EventBroker.DEFAULT,
    DEFAULT_RESOURCE: Resource.DEFAULT,
    create : function(parameters, parent) {
        if (!(parameters && parameters.classname)) {
            throw new Exception('Invalid parameters, the classname property is required');
        }
        var classname = parameters.classname;
        var clazz = require(classname);
        if (clazz == null) {
            throw new Exception('Fail to require "' + classname + '"');
        }
        return new clazz(parameters, parent);
    },
    wrap : function(clazz, object) {
        if (object instanceof clazz) {
            return object;
        }
        if ((typeof object == 'object') && ('classname' in object)) {
            object = Element.create(object);
            if (object instanceof clazz) {
                return object;
            }
        }
        throw new Exception('Invalid object, cannot wrap with specified class');
    }
});

return Element;
});
