/**
 * 
 * All content on this website (including text, images, source
 * code and any other original works), unless otherwise noted,
 * is licensed under a Creative Commons License.
 * 
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * 
 * Copyright (C) 2004-2009 Open-Xchange, Inc.
 * Mail: info@open-xchange.com 
 * 
 * @author Matthias Biggeleben <matthias.biggeleben@open-xchange.com>
 * 
 */

// DEVELOPMENT IN PROGRESS // SUBJECT TO PERMANENT CHANGE!

/** @ignore */
var oxWidgetFactory = {};

ox.gui.Widget = Class.extend({
/** @lends ox.gui.Widget.prototype */
    
    /** 
     * The basic widget class
     * @constructs
     * @param {String} id An optional unique id for the widget.
     */
    construct: function(id) {
    
        // widget ID
        this.id = this.autoId(id, "widget");
        
        // add to global index?
        if (ox.desktop && this.id) {
            ox.desktop.registerWidget(this);
        }
        
        /** The parent widget */
        this.parent = null;
        /** The widget's position if inside a container widget */
        this.position = 0;
        /** Layout parameters used by the layout manager **/
        this.layout = {};
        /** user data **/
        this.data = {};
        
        // can be cloned?
        this.clonable = this.clonable || false;
        
        // status
        this.statusLoaded = false;
        this.statusVisible = true;
        this.statusEnabled = true;
        this.statusFocused = false;
        this.statusDraggable = false;
        this.statusDropzone = false;
        
        this.requiresPaint = true;
        this.requiresRepaint = false;
        this.requiresResize = true;
        this.requiresNanny = false;
        this.requiresValidation = true;
        
        this.dom = {};
        /** The widget's DOM node */
        this.dom.node = null;
        /** Listeners */
        this.listeners = {};
        
        this.statusAllowContextMenu = false;
        this.statusAllowSelect = false;
        
        this.numHovers = 0;
        
        // create DOM node
        this.nodeCreate();
    },
    
    /**
     * The class name of the widget, e.g. ox.gui.Widget.
     * @returns (String) The class name.
     */
    getClass: function() { return "ox.gui.Widget"; },

    /**
     * @private
     */
    autoId: function(id, prefix) { 
        return id || ((prefix || "widget") + "-" + (ox.desktop.globalCounter++)); 
    },
    
    /**
     * @private
     * @param {String} The tag name of the widget's DOM node, e.g. DIV or SPAN.
     */
    nodeCreate: function(tagName) {
        // dom? (can happen if a class must call createNode before calling the super constructor)
        if (!this.dom) {
            this.dom = { node: null, placeholder: null };
        }
        // create node?
        if (!this.dom.node) {
            // get class name
            var widgetClass = this.getClass();
            // prototype in widget factory?
            if (this.clonable && oxWidgetFactory[widgetClass]) {
                // clone prototype
                this.dom.node = oxWidgetFactory[widgetClass].cloneNode(true);
            } else {
                this.dom.node = document.createElement(tagName || "div");
                this.dom.node.style.cursor = "default";
                // Selection
                this.nodePreventSelection();
                // Context Menu
                this.nodePreventContextMenu();
                // initialize node (can be overwritten by sub classes)
                this.nodeInit();
                   // can be cloned?
                if (this.clonable) {
                    oxWidgetFactory[widgetClass] = this.dom.node.cloneNode(true);
                }
            }
            // id
            if (this.id) { this.dom.node.id = this.id; }
            // class
            this.dom.node.setAttribute("oxWidgetClass", this.getClass());
            // add listeners
            this.nodeListeners();
        }
    },
    
    nodePreventContextMenu: function() {
        // FF, IE, Safari
        $(this.dom.node).bind('contextmenu', $handleContextMenu);
    },
    
    nodePreventSelection: function() {
        if (ox.browser.Gecko) {
            // Firefox
            this.dom.node.style.MozUserSelect = "-moz-none";
        } else {
            // this way for IE and others / prevents strange IE7 scrollbar flicker issue as well
            $(this.dom.node).bind('selectstart', $handleSelection);
        }
    },
    
    /**
     * @private
     */
    nodeInit: function() {
    },
    
    /**
     * @private
     */
    nodeListeners: function() {
    },
    
    /**
     * Add to container
     */
    addTo: function (parent) {
        if (parent && parent.add) {
            parent.add(this);
        }
        return this;
    },
    
    /**
     * Specifies the DOM node this widget will be added to.
     * @param Parent DOM node.
     */
    setParentDOMNode: function(parentDOMNode) {
        // keep visibility status
        var vStatus = this.statusVisible;
        // already in DOM?
        if (this.dom.node.parentNode) {
            this.removeFromDOM();
        }
        // append to DOM
        parentDOMNode.appendChild(this.dom.node);
        // restore visibility
        this.statusVisible = vStatus;
        // support chaining
        return this;
    },
    
    /**
     * Removes this widget from the DOM.
     */
    removeFromDOM: function() {
        // loop node object
        for (var name in this.dom) {
            // we cannot use jquery's remove here since it removes all event handlers
            // which is often not wanted
            var node = this.dom[name], p = node.parentNode;
            if (p) { p.removeChild(node); }
        }
        this.requiresValidation = true;
        this.requiresRepaint = true;
        // support chaining
        return this;
    },
    
    remove: function() {
        var p = this.parent;
        if (p) {
            p.removeChild(this);
        }
    },
    
    replaceBy: function(widget) {
        var p = this.parent;
        if (p) {
            p.replace(this, widget);
        }
    },
    
    /**
     * Paints the widget. Can be overridden by sub classes.
     */
    paint: function() {
    },
    
    clear: function() {
        $(this.dom.node).empty();
    },

    /**
     * Repaints the widget. Can be overridden by sub classes.
     * @param {function()} cont A continuation function which is called after
     * the repaint completes.
     */
    repaint: function (cont) {
        this.clear();
        this.paint(cont);
    },
    
    /**
     * Updates the widget if it is invalid (triggers repaint)
     */
//    __update: function() {
//        if (!this.statusValid) {
//            this.repaint();
//            this.statusValid = true;
//        }
//    },

    isShowing: function() {
        return this.statusVisible && (this.parent === null || this.parent.isShowing());
    },
    
    /**
     * Invalidates the widget. Layout managers will repaint the widget next time.
     */
    invalidate: function() {
        this.requiresValidation = true;
        if (this.parent) { this.parent.invalidate(); }
    },
    
    invalidateFamily: function() {
        this.requiresNanny = true;
        // trigger
        this.triggerWidgetEvent("familychanged");
        // bubble
        this.invalidateSize();
    },

    invalidateVisibility: function() {
        var p = this.parent;
        if (p) {
            p.requiresRepaint = true;
            p.invalidateSize();
        }
    },

    invalidateSize: function() {
        this.requiresResize = true;
        this.requiresValidation = true;
        if (this.parent) { this.parent.invalidateSize(); }
    },

//    __invalidate: function() {
//        this.statusValid = false;
//        if (this.parent) {
//            this.parent.invalidate();
//        }
//    },
    
    validate: function (force) {
        if (force || (this.requiresValidation && this.isShowing())) {
            // validate tree
            this.validateTree();
            // resize!
            this.resize();
        }
        return this;
    },
    
    validateTree: function() {
        
        if (this.requiresValidation && this.statusVisible) {
            // validate this
            if (this.requiresPaint || this.requiresNanny) {
                // paint!
                this.paint();
                this.requiresNanny = false;
                this.requiresPaint = false;
                this.requiresRepaint = false;
            }
            else if (this.requiresRepaint) {
                // repaint!
                this.repaint();
                this.requiresRepaint = false;
            }
            // done!
            this.requiresValidation = false;
            // trigger event
            this.triggerWidgetEvent("valid");
        }
    },
    
    /**
     * @returns {bool} True if the widget is valid.
     */
    isValid: function() { 
        return this.requiresValidation === false && 
            this.statusVisible === true && this.requiresPaint === false &&
            this.requiresNanny === false && this.requiresRepaint === false;
    },

//    /**
//     * Validates the widget.
//     * @param force {bool} The widget will be validated (again) even if it's valid.
//     */
//    __validate: function(force) {
//        if (!this.statusValid || force) {
//            // is loaded?
//            if (!this.loaded) {
//                // must load before (async)
//                this.load(ox.gui.Widget.prototype.validate.bind(this));
//            } else {
//                // visible?
//                if (this.statusVisible) {
//                    // paint
//                    if (this.statusWasValid) {
//                        this.repaint();
//                    } else {
//                        this.paint();
//                    }
//                    // paint enabled
//                    if (!this.statusEnabled) {
//                        this.paintDisabled();
//                    } else {
//                        this.paintEnabled();
//                    }
//                    // change?
//                    if (!this.statusVisibleCurrent) {
//                        // paint visible
//                        this.paintVisible();
//                        // now, visible!
//                        this.statusVisibleCurrent = true;
//                        this.triggerWidgetEvent("show", null);
//                    }
//                } else {
//                    // change?
//                    if (this.statusVisibleCurrent) {
//                        // invisible
//                        this.paintInvisible();
//                        // notify
//                        this.statusVisibleCurrent = false;
//                        this.triggerWidgetEvent("hide", null);
//                    }
//                }
//                // set status
//                this.statusValid = true;
//                this.statusWasValid = true;
//                // trigger event
//                this.triggerWidgetEvent("valid", null);
//                // resize
//                this.resize();
//            }
//        }
//        // support chaining
//        return this;
//    },

//    /**
//     * Shows the widget.
//     */
//    __show: function() {
//        // not visible?
//        if (!this.statusVisible || !this.statusValid) {
//            // is valid?
//            if (!this.statusValid) {
//                // must be painted before
//                this.validate();
//            }
//            // now valid?
//            if (this.statusValid) {
//                // set display style
//                this.paintVisible();
//                this.statusVisible = true;
//                // trigger event
//                this.triggerWidgetEvent("show", null);
//            }
//        }
//        // support chaining
//        return this;
//    },
    
    show: function() { return this.setVisible(true); },
    hide: function() { return this.setVisible(false); },
    
    setVisible: function(flag, cont) {
        var prev = this.statusVisible;
        flag = !!flag;
        // changed?
        if (flag != prev) {
            // update
            this.statusVisible = flag;
            // invalidate
            this.invalidateVisibility();
            // validate now?
            if (this.isShowing()) {
                this.validateTree();
            }
            if (this.statusVisible) {
                this.paintVisible(cont);
                ox.desktop.triggerEvent("visible", this, null);
            } else {
                this.paintInvisible(cont);
                ox.desktop.triggerEvent("invisible", this, null);
            }
        } else {
            ox.call(cont);
        }
        return this;
    },
    
    /**
     * @private
     */
    paintVisible: function(cont) {
        this.dom.node.style.display = "block";
        ox.call(cont);
    },
    
    /**
     * @private
     */
    paintInvisible: function(cont) {
        this.dom.node.style.display = "none";
        ox.call(cont);
    },
    
    setDraggable: function(flag, options) {
        var defaults = {
            type: "generic",
            handle: null,
            helper: "clone",
            customize: null,
            drop: "window",
            clone: function() { return this.clone(); }
        };
        this.dragOptions = $.extend(defaults, options);
        // bind functions?
        if ($.isFunction(this.dragOptions.helper)) {
            this.dragOptions.helper = $.proxy(this.dragOptions.helper, this);
        }
        if ($.isFunction(this.dragOptions.clone)) {
            this.dragOptions.clone = $.proxy(this.dragOptions.clone, this);
        }
        this.statusDraggable = !!flag;
        this.paintDraggable();
        return this;
    },
    
    /**
     * @private
     */
    paintDraggable: function() {
        var self = this;
        if (this.statusDraggable) {
            // add type && make draggable
            $(this.dom.node).attr("dragtype", this.dragOptions.type).draggable({
                addClasses: false,
                appendTo: 'body',
                scroll: false,
                zIndex: 65000,
                cursorAt: { left: 10, top: 10 },
                helper: this.dragOptions.helper,
                grid: [10, 10],
                distance: 10,
                iframeFix: true,
                revert: 'invalid',
                start: function(event, ui) {
                    // adjust css
                    $(ui.helper).css({
                        width: "auto", height: "auto", right: "auto", bottom: "auto"
                    });
                    // add class
                    ui.helper.addClass("oxDragHelper");
                    // customize?
                    if ($.isFunction(self.dragOptions.customize)) {
                        $.proxy(self.dragOptions.customize, self)(ui.helper);
                    }
                    // add reference to widget
                    ui.helper.data("widget", self);
                },
                stop: function(event, ui) {
                }
            });
            // handle?
            if (this.dragOptions.handle) {
                $(this.dom.node).draggable('option', 'handle', this.dragOptions.handle);
            }
            // has no dropzone?
            if (!this.statusDropzone) {
                // add dummy dropzone (accepts nothing)
                this.setDropzone(true);// { accept: ox.True });
            }
        } else {
            $(this.dom.node).removeAttr("dragtype").draggable('destroy');
        }
    },
    
    setDropzone: function(flag, options) {
        var defaults = {
            accept: ox.True
        };
        // process options
        this.dropOptions = $.extend(defaults, options);
        // make function
        var accept = this.dropOptions.accept;
        this.dropOptions.accept = $.isFunction(this.dropOptions.accept) ? this.dropOptions.accept : function(d) {
            return d.is(accept);
        };
        // set status
        this.statusDropzone = !!flag;
        this.paintDropzone();
        return this;
    },
    
    /**
     * @private
     */
    paintDropzone: function() {
        var self = this;
        if (this.statusDropzone) {
            $(this.dom.node).droppable({
                accept: this.dropOptions.accept,
                greedy: true,
                over: function(event, ui) {
                    // draggable? (exclude e.g. a normal window)
                    if(ui.helper.hasClass("oxDragHelper")) {
                        ui.helper.addClass("oxOverDropzone");
                    }
                },
                out: function(event, ui) {
                    // draggable? (exclude e.g. a normal window)
                    if(ui.helper.hasClass("oxDragHelper")) {
                        ui.helper.removeClass("oxOverDropzone");
                    }
                },
                drop: function(event, ui) {
                    // draggable? (exclude e.g. a normal window)
                    if(ui.helper.hasClass("oxDragHelper")) {
                        // get widget
                        var widget = ui.helper.data("widget");
                        var opt = widget.dragOptions;
                        if (opt.drop == "window" && $.isFunction(opt.clone)) {
                            var offset = ui.helper.offset();
                            var width = Math.max(400, Math.min(ui.helper.width() + 20, 800));
                            var height = Math.min(ui.helper.width() + 20, 300);
                            var win = ox.desktop.createWindow(self.autoId(null, widget.id+"-clone-window")).
                                setTitle("A clone").
                                setSize(width, height).
                                setPosition(offset.left, offset.top);
                            // clone
                            var ret = opt.clone(win);
                            if (ret) {
                                win.add(ret).show();
                            }
                        }
                    }
                }
            });
        } else {
            $(this.dom.node).droppable('destroy');
        }
    },
    
    clone: function(id) {
        // create new widget of same class
        var constructor = this.getConstructor();
        var clone = new constructor(id || (this.id+"-clone"));
        // copy layout information
        clone.layout = ox.cloneObject(this.layout);
        // copy customized functions
        for (var fn in this) {
            if (typeof(this[fn]) == 'function' && this[fn] != clone[fn]) {
                clone[fn] = this[fn];
            }
        }
        // copy listeners
        var listeners = this.getListeners();
        for (var i in listeners) {
            var listener = listeners[i];
            clone.addListener("widget:" + listener.type, listener.callback);
        }
        // custom handler
        this.cloneDeep(clone);
        // return clone
        return clone;
    },
    
    cloneDeep: function(clone) {
    },
    
//    /**
//     * Hides the widget
//     */
//    __hide: function() {
//        // has node?
//        if (this.statusVisible) {
//            this.paintInvisible();
//            // set status
//            this.statusVisible = false;
//            // trigger event
//            this.triggerWidgetEvent("hide", null);
//        }
//        // support chaining
//        return this;
//    },
    
    /**
     * Sets the CSS class name of the widget's DOM node.
     * @param className {String} The CSS class name.
     */
    setCSSClass: function(className) {
        $(this.dom.node).removeClass().addClass(className);
        return this;
    },

    /**
     * Adds a CSS class name to the widget's DOM node.
     * @param className {String} The CSS class name.
     */
    addCSSClass: function(className) {
        $(this.dom.node).addClass(className);
        return this;
    },

    /**
     * Removes a CSS class name from the widget's DOM node.
     * @param className {String} The CSS class name.
     */
    removeCSSClass: function(className) {
        $(this.dom.node).removeClass(className);
        return this;
    },

    /**
     * Specifies the CSS text of the widget's DOM node.
     * @param cssText {String} The CSS text, e.g. "background-color: yellow".
     */
    css: function() {
        var tmp = $(this.dom.node);
        tmp.css.apply(tmp, arguments);
        return this;
    },
    
    setStyle: function() {
        if (this.dom.node) {
            // pass to jQuery' CSS method
            var args = $.makeArray(arguments);
            switch (args.length) {
            case 1: $(this.dom.node).css(args[0]); break;
            case 2: $(this.dom.node).css(args[0], args[1]); break;
            }
        }
        return this;
        
//        var cssTextList = null;
//        // is string or array?
//        if (typeof(cssText) == "string") {
//            cssTextList = new Array(cssText);
//        } else {
//            cssTextList = cssText;
//        }
//        // loop styles
//        for (var i = 0; i < cssTextList.length; i++) {
//            // get key & value
//            var keyValue = cssTextList[i].split(/\:\s?/);
//            var name = keyValue[0], value = keyValue[1];
//            if (name.search(/-/) != -1) {
//                // CSS text syntax
//                // adjust key
//                name = name.toLowerCase().replace(/-[a-z]/, function(match) { return match.substr(1,1).toUpperCase(); });
//                // set DOM style
//                if (this.dom.node) { this.dom.node.style[name] = value || ""; } // IE needs explicit empty string
//            } else {
//                // DOM style syntax
//                if (this.dom.node) { this.dom.node.style[name] = value || ""; } // IE needs explicit empty string
//            }
//        }
//        // support chaining
//        return this;
    },
    
    /**
     * Adds an event listener.
     * @param {String} eventKey The event key, e.g. "dom:click", "dom:mousedown" as well
     * as "widget:..." for widget-specific events, and "ox:..." for global events.
     * @param {Object} target [Optional] The target of the event. Can be a DOM node or a widget.
     * @param {Object} callback A callback function. The callback will be bound to the widget, i.e.
     * "this" will refer to the widget.
     */
    addListener: function() {
        
        var opt = this.prepareListener.apply(this, arguments);
        
        // set listener for domain
        switch (opt.domain) {
            case "ox":
                // add global listener
                ox.desktop.addGlobalListener(opt);
                break;
            case "widget":
                // add widget listener
                opt.target = opt.target || this;
                ox.desktop.addWidgetListener(opt);
                break;
            case "dom":
                // add listener
                opt.target = opt.target || this.dom.node;
                ox.desktop.addDOMListener(opt);
                break;
            default:
                console.error("addListener() Unkown domain", opt.domain);
                break;
        }
        // support chaining
        return this;
    },
    
    removeListener: function() {
        
        var opt = this.prepareListener.apply(this, arguments);
        // set listener for domain
        switch (opt.domain) {
            case "ox":
                // add global listener
                ox.desktop.removeGlobalListener(opt);
                break;
            case "widget":
                // add widget listener
                opt.target = opt.target || this;
                ox.desktop.removeWidgetListener(opt);
                break;
            case "dom":
                // adjust target
                opt.target = opt.target || this.dom.node;
                // add listener
                ox.desktop.removeDOMListener(opt);
                break;
        }
        // support chaining
        return this;
    },
    
    getListeners: function() {
        return ox.desktop.getWidgetListeners(this);
    },
    
    /**
     * @private
     */
    prepareListener: function() {
        // get parameter
        var type, target, callback, args = arguments, domain, pair;
        switch(args.length) {
            // one parameter
            case 1:
                type = args[0]; target = null; callback = null;
                break;
            // two parameter
            case 2:
                type = args[0]; target = null; callback = args[1];
                break;
            // three parameter
            case 3:
                type = args[0]; target = args[1]; callback = args[2];
                break;
            default:
                return {};
        }
        
        // get domain of event (default is "dom")
        pair = type.search(/:/) != -1 ? type.split(":") : ["dom", type];
        domain = pair[0].toLowerCase();
        type = pair[1].toLowerCase();
        
        return {
            "type": type, "target": target, "callback": callback,
            "domain": domain, "scope": this
        };
    },
    
    /**
     * Triggers a global event.
     * @param eventName {String} The name of the event.
     * @param eventData {Object} Any data that will be send to listening widgets.
     */
    triggerEvent: function(eventName, eventData) {
        // is enabled?
        if (this.statusEnabled) {
            // yes, trigger event
            ox.desktop.triggerGlobalEvent(eventName, eventData !== undefined ? eventData : {});
        }
        // support chaining
        return this;
    },
    
    /**
     * Triggers a widget event.
     * @param eventName {String} The name of the event.
     * @param eventData {Object} Any data that will be send to listening widgets.
     */
    triggerWidgetEvent: function(eventName, eventData) {
        // is enabled?
        if (this.statusEnabled) {
            // yes, trigger event
            ox.desktop.triggerEvent(eventName, this, eventData !== undefined ? eventData : {});
        }
        // support chaining
        return this;
    },
    
    trigger: function() {
        
        var self = this;
        
        // internal helper
        var trigger = function (options) {
            var namespace = "", name = "";
            // namespace?
            var index = options.name.indexOf(":");
            if (index > 0) {
                namespace = options.name.substr(0, index);
                name = options.name.substr(index+1);
            } else {
                namespace = "global";
                name = options.name;
            }
            // go!
            switch (namespace) {
                case "widget": ox.desktop.triggerEvent(name, self, options.data || null, options.callback || $.noop); break;
                case "global": ox.desktop.triggerGlobalEvent(name, options.data || null, options.callback || $.noop); break;
            }
        };
        // get arguments
        var args = $.makeArray(arguments);
        // one argument?
        if (args.length == 1 && typeof(args[0]) == "string") {
            trigger({ name: args[0], data: null });
        } else if (args.length == 2 && typeof(args[0]) == "string" && typeof(args[1]) != "function") {
            trigger({ name: args[0], data: args[1] });
        } else if (args.length == 2 && typeof(args[0]) == "string" && typeof(args[1]) == "function") {
            trigger({ name: args[0], data: null, callback: args[1] });
        } else if (args.length == 3 && typeof(args[0]) == "string"  && typeof(args[1]) != "function" && typeof(args[2]) == "function") {
            trigger({ name: args[0], data: args[1], callback: args[2] });
        }
    },
    
    /**
     * Disables the widget. Still visible, but its appearance is changed and 
     * no events are processed.
     */
    disable: function() {
        // trigger event now! since this widget will be disabled next and
        // therefore it will be ignored by event handlers
        this.triggerWidgetEvent("disable", null);
        // set enabled to false
        this.statusEnabled = false;
        // render
        this.paintDisabled();
        // support chaining
        return this;
    },

    /**
     * @private
     */
    paintEnabled: function() {
        this.removeCSSClass("disabled");
    },

    /**
     * @private
     */
    paintDisabled: function() {
        this.addCSSClass("disabled");
    },
    
    setEnabled: function (state) {
        if (this.statusEnabled !== state) {
            if (state === true) {
                this.enable();
            } else {
                this.disable();
            }
        } 
    },
    
    /**
     * Enables the widget.
     * @see disable
     */
    enable: function() {
        // set enabled to true
        this.statusEnabled = true;
        this.paintEnabled();
        // trigger event
        this.triggerWidgetEvent("enable", null);
        // support chaining
        return this;
    },
    
    /**
     * @private
     */
    paintEnable: function() {
        this.removeCSSClass("disabled");
    },
    
    /**
     * @returns {bool} True if the widget is enabled.
     */
    isEnabled: function() {
        return this.statusEnabled;
    },
    
    /**
     * @returns {bool} True if the widget is visible.
     */
    isVisible: function() {
        return this.statusVisible;
    },
    
    /**
     * @returns {bool} True if the widget has the focus.
     */
    hasFocus: function() {
        return this.statusFocused;
    },
    
    setFocus: function(flag) {
        this.statusFocused = !!flag;
        if (this.statusFocused) {
            this.triggerWidgetEvent("focus", null);
        } else {
            this.triggerWidgetEvent("blur", null);
        }
    },
    
    override: function(name, fn) {
        if ($.isFunction(fn)) {
            this[name] = fn;
        }
        return this;
    },
    
    /**
     * Sets a layout parameter
     * @param {String} name The name of the parameter.
     * @param {String} value The value of the parameter.
     */
    setLayoutParam: function(name, value) {
        if (typeof name === "object") {
            // name -> properties
            $.extend(this.layout, name);
        } else {
            this.layout[name] = value;
        }
        // invalidates layout
        this.invalidate();
        // allow chaining
        return this;
    },
    
    resize: function() { },
    
    hasValue: function() { return false; },
    
    getSize: function() {
        var width = 0, height = 0;
        // is yet painted?
        if (!this.requiresPaint) {
            // get "real" size
            var $node = $(this.dom.node);
            width = $node.width(); 
            height = $node.height();
        } else {
            // get default size
            width = this.layout.width || this.layout.defaultWidth || 0;
            height = this.layout.height || this.layout.defaultHeight || 0;
        }
        return { "width": width, "height": height };
    },
    
    phantomize: function(invisible) {
        $(this.dom.node).phantomize(invisible);
    },
    
    materialize: function() {
        $(this.dom.node).materialize();
    },
    
    /**
     * Destroys the widget (and helps the garbage collector as much as possible)
     */
    destroy: function() {

        var p = this.parent;
        
        // unregister
        ox.desktop.unregisterWidget(this);
        
        // remove parent reference
        if (this.parent !== null) {
            this.parent.removeChild(this);
            this.parent = null;
        }
        // delete layout data
        this.layout = null;
        
        // remove widget listeners
        ox.desktop.removeListenerByScope(this);
        
        // clear DOM node (remove event handler, remove child nodes, remove node)
        this.removeFromDOM();
        // loop
        for (var id in this.dom) {
            $(this.dom[id]).unbind().empty().remove();
        }
        
        // remove DOM node reference
        this.dom.node = null;
        
        // if (p) { p.validate(); }
    },
    
    getData: function(key) {
        return this.data[key];
    },

    setData: function(key, value) {
        this.data[key] = value;
        return this;
    },
    
    enableI18n: function () {
        if (!this.i18nEnabled) {
            this.addListener("ox:LanguageChanged", this.i18nHandler);
            this.i18nEnabled = true;
        }
    },
    
    disableI18n: function () {
        if (this.i18nEnabled) {
            this.removeListener("ox:LanguageChanged", this.i18nHandler);
            this.i18nEnabled = false;
        }
    },
    
    i18nHandler: function () {
    }
});

ox.gui.DebugWidget = ox.gui.Widget.extend({
/** @lends ox.gui.DebugWidget.prototype */
    
    getClass: function() { return "ox.gui.DebugWidget"; },
    
    /** 
     * A simple widget that can be used for debugging
     * @constructs
     * @extends ox.gui.Widget
     * @param {String} id An optional unique id for the widget.
     */
    construct: function(id) {
        // call super class constructor (inherit from Container)
        this._super(id);
        // local constructor
        this.currentState = "";
    },
    
    nodeInit: function() {
        this.dom.node.style.cssText = "background-color: lightyellow; border: 1px solid #fc0; padding: 5px; line-height: 11pt;";
    },

    paint: function() {
        this.dom.node.innerHTML = "I am a debug widget! My name is: <b>" + this.id + "</b>";
    }
});

ox.gui.Canvas = ox.gui.Custom = ox.gui.Widget.extend({
/** @lends ox.gui.Custom.prototype */
    
    getClass: function() { return "ox.gui.Custom"; },
    
    /** 
     * A simple widget
     * @constructs
     * @extends ox.gui.Widget
     * @param {String} id An optional unique id for the widget.
     */
    construct: function(paint, id) {
        // call super class constructor (inherit from Container)
        this._super(id);
        // override paint?
        if ($.isFunction(paint)) {
            this.paint = paint;
        }
    },
    
    nodeInit: function() {},

    paint: function() { }
});

ox.gui.Template = ox.gui.Widget.extend({
/** @lends ox.gui.Template.prototype */
    
    getClass: function() { return "ox.gui.Template"; },
    
    /** 
     * A simple template-based widget
     * @constructs
     * @extends ox.gui.Widget
     * @param {String} id An optional unique id for the widget.
     */
    construct: function(id) {
        
        // call super class constructor (inherit from Container)
        this._super(this.autoId(id, "template"));
        
        this.src = "";
        this.content = "";
        this.loading = false;
        
        this.data = {};
    },
    
    nodeInit: function() {
        $(this.dom.node).addClass("oxStretch");
    },
    
    setSrc: function(src) {
        this.src = src;
        return this;
    },
    
    paint: function() {
        var self = this;
        // already loading
        if (!this.loading) {
            this.loading = true;
            // load widget from external file
            $.ajax({
                method: "GET",
                url: this.src,
                success: function(response) {
                    self.loading = false;
                    // get content
                    self.content = response;
                    // create document fragment
                    var frag = self.getFragment(response);
                    // process & add fragment
                    self.dom.node.appendChild(self.prePaint(frag));
                    self.postPaint(self.dom.node);
                },
                error: function(response) {
                    self.loading = false;
                }
            });
        }
    },
    
    getFragment: function(html) {
        // create document fragment (take the long way, since a fragment does not "like" innerHTML)
        var frag = newfrag();
        var div = newnode("div");
        div.innerHTML = html;
        // append children
        var $c = div.childNodes;
        for (;$c.length;) { frag.appendChild($c[0]); }
        return frag;
    },
    
    postPaint: function(node) {
    },
    
    prePaint: function(frag) {
        return frag;
    },
    
    clear: function() {
        this.dom.node.innerHTML = "";
    },
    
    repaint: function() {
        // clear content
        this.clear();
        // create fragment
        var frag = this.getFragment(this.content);
        // process & add fragment
        this.dom.node.appendChild(this.prePaint(frag));
        this.postPaint(this.dom.node);
    }
});


ox.gui.Image = ox.gui.Widget.extend({
/** @lends ox.gui.Image.prototype */
    
    getClass: function() { return "ox.gui.Image"; },
    
    /** 
     * A simple image widget
     * @constructs
     * @extends ox.gui.Widget
     * @param {String} src The image source location.
     * @param {I18nString} An optional title (
     */
    construct: function(src, title) {
        // call super class constructor (inherit from Container)
        this._super(this.autoId(0, "image"));  
        this.src = src || "";
        this.title = title;
        this.image = $("<img/>");
        this.setSrc(this.src);
        this.setTitle(title);        
    },
        
    setSrc: function(src) {
        this.src = src;
        this.image.attr({ src: this.src });
        return this;
    },
    
    setTitle: function(title) {
        this.title = title;
        if (!this.title) {
            this.disableI18n();
            this.image.removeAttr("title");
        } else {
            this.enableI18n();
        }
        return this;
    },
    
    i18nHandler: function() {
        if (this.title) this.image.attr("title", expectI18n(this.title));
    },
    
    paint: function() {        
        $(this.dom.node).append(this.image);
    }
});

ox.gui.Atomic = ox.gui.Widget.extend({
/** @lends ox.gui.Image.prototype */
    
    getClass: function() { return "ox.gui.Atomic"; },
    
    /** 
     * An atomic widget (plain DIV)
     * @constructs
     * @extends ox.gui.Widget
     */
    construct: function() {
        this._super(this.autoId(0, "atomic"));
    }
});