/*
################################################################################
ox.ui.js

requires
    jquery-1.4.js
    ox.js
################################################################################
*/

// also see test.js, in demos ...

(function() {

    var oxui = {
            defaultTheme: "classic",
            elements: {},
            getDimensions: function(orientation) {
                return orientation == "horizontal" ?
                    ["width", "height"] : ["height", "width"];
            },
            getEdges: function(orientation) {
                return orientation == "horizontal" ?
                    ["left", "right", "top", "bottom"] :
                    ["top", "bottom", "left", "right"];
            },
            getBarSize: function(size) {
                var sizes = {
                    xsmall: 16,
                    small: 24,
                    medium: 28,
                    large: 32,
                    xlarge: 40
                };
                return sizes[size];
            },
            jQueryFunctions: function() {
                var functions = [],
                    $element = $("<div>");
                //delete $element.length;
                Ox.each($element, function(k, v) {
                    if (typeof v == "function") {
                        functions.push(k);
                    }
                });
                return functions.sort();
            }(),
            path: $("script[src*=ox.ui.js]").attr("src")
                .replace("js/ox.ui.js", ""),
            symbols: { // fixme: make lowercase
                alt: "\u2325",
                apple: "\uF8FF",
                arrow_down: "\u2193",
                arrow_left: "\u2190",
                arrow_right: "\u2192",
                arrow_up: "\u2191",
                backspace: "\u232B",
                backup: "\u2707",
                ballot: "\u2717",
                black_star: "\u2605",
                burn: "\u2622",
                caps_lock: "\u21EA",
                check: "\u2713",
                clear: "\u2327",
                click: "\uF803",
                close: "\u2715",
                command: "\u2318",
                control: "\u2303",
                cut: "\u2702",
                "delete": "\u2326",
                diamond: "\u25C6",
                edit: "\uF802",
                eject: "\u23CF",
                escape: "\u238B",
                end: "\u2198",
                enter: "\u2324",
                fly: "\u2708",
                gear: "\u2699",
                home: "\u2196",
                info: "\u24D8",
                navigate: "\u2388",
                option: "\u2387",
                page_up: "\u21DE",
                page_down: "\u21DF",
                redo: "\u21BA",
                "return": "\u21A9",
                select: "\u21D5",
                shift: "\u21E7",
                sound: "\u266B",
                space: "\u2423",
                tab: "\u21E5",
                trash: "\u267A",
                triangle_down: "\u25BC",
                triangle_left: "\u25C0",
                triangle_right: "\u25BA",
                triangle_up: "\u25B2",
                undo: "\u21BB",
                voltage: "\u26A1",
                warning: "\u26A0",
                white_star: "\u2606"
            }
        },
        $window, $document, $body;

    $(function() {
        $window = $(window),
        $document = $(document),
        $body = $("body");
        Ox.theme(oxui.defaultTheme);
    });

    /*
    ============================================================================
    Application
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.App
    ----------------------------------------------------------------------------
    */

    Ox.App = function() {
        /*
        options:
        requestTimeout
        requestType
        requestURL
        */
        return function(options) {

            options = options || {};
            var self = {},
                that = this;

            self.options = $.extend({
                requestTimeout: oxui.requestTimeout,
                requestType: oxui.requestType,
                requestURL: oxui.requestURL
            }, options);

            self.change = function() {
                
            };

            that.launch = function() {
                $.ajaxSetup({
                    timeout: self.options.requestTimeout,
                    type: self.options.requestType,
                    url: self.options.requestURL
                });
            };

            that.options = function() {
                return Ox.getset(self.options, Array.slice.call(arguments), self.change, that); 
            };

            that.request = function(action, data, callback) {
                if (arguments.length == 2) {
                    callback = data;
                    data = {};
                }
                return Ox.Request.send({
                    url: self.options.requestURL,
                    data: {
                        action: action,
                        data: JSON.stringify(data)
                    },
                    callback: callback
                });
            };

            return that;

        };

    }();

    /*
    ----------------------------------------------------------------------------
    Ox.Event
    ----------------------------------------------------------------------------

    naming convention for event/trigger
    verb.id.namespace, i.e. verb.sourceId.targetId (?)
    ...
    bind("keydown.shift+dot.numpad", function() {
        // ...
    })
    keyboard handler then would:
    $.each(stack, function(i, v) {
        elements[v].trigger("keydown.shift+0.numpad");
    });
    and the element would implement
    this.trigger(event, data) {
        
    }
    ...
    keyboard handler also triggers keydown.buffer
    */

    // use dom elements / jquery instead

    
    Ox.Event = function() {
        var keyboardEvents = {};
            $eventHandler = $("<div>");
        function isKeyboardEvent(event) {
            return event.substr(0, 4) == "key_";
        }
        return {
            bind: function(id, event, callback) {
                if (isKeyboardEvent(event)) {
                    keyboardEvents[id] = keyboardEvents[id] || {};
                    keyboardEvents[id][event] = callback;
                }
                if (!isKeyboardEvent(event) || Ox.Focus.focused() == id) {
                    $eventHandler.bind(event, callback);
                }
            },
            bindKeyboard: function(id) {
                $.each(keyboardEvents[id] || [], function(event, callback) {
                    Ox.Event.bind(id, event, callback);
                });
            },
            trigger: function(event, data) {
                Ox.print("trigger", event, data || {});
                $eventHandler.trigger(event, data || {});
            },
            unbind: function(id, event, callback) {
                if (isKeyboardEvent(event)) {
                    $.each(keyboardEvents[id] || [], function(e, callback) {
                        if (e == event) {
                            delete keyboardEvents[id][e];
                            return false;
                        }
                    });
                }
                $eventHandler.unbind(event, callback);
            },
            unbindKeyboard: function(id) {
                //Ox.print(keyboardEvents)
                //Ox.print("unbindKeyboard", id, keyboardEvents[id])
                $.each(keyboardEvents[id] || [], function(event, callback) {
                    $eventHandler.unbind(event, callback);
                });
            }
        }
    }();

    Ox.Event_ = function() {
        var events = {};
        return {
            // make these bind, trigger, unbind
            publish: function(event, data) {
                Ox.print("publish", event, data);
                if (events[event]) {
                    $.each(events[event], function(i, v) {
                        setTimeout(function() {
                            v(data);                                
                        }, 0);
                    });
                }
            },
            subscribe: function(event, callback) {
                Ox.print("subscribe", event, callback);
                if (events[event]) {
                    events[event].push(callback);
                } else {
                    events[event] = [callback];
                }
            },
            unsubscribe: function(event, callback) {
                Ox.print("unsubscribe", event, callback);
                $.each(events[event], function(i, v) {
                    if (Ox.startsWith(callback.toString(), v.toString())) {
                        events[event].splice(i, 1);
                    }
                });
            }
        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.Focus
    ----------------------------------------------------------------------------
    */

    Ox.Focus = function() {
        var stack = [];
        return {
            focus: function(id) {
                var index = stack.indexOf(id);
                if (stack.length) {
                    Ox.Event.unbindKeyboard(stack[stack.length - 1])
                }
                if (index > -1) {
                    stack.splice(index, 1);
                }
                stack.push(id);
                Ox.Event.bindKeyboard(id);
                Ox.print("focus", stack);
            },
            focused: function() {
                return stack[stack.length - 1];
            },
            blur: function(id) {
                if (stack.indexOf(id) > -1) {
                    stack.splice(stack.length - 2, 0, stack.pop());
                }
                Ox.Event.unbindKeyboard(id);
                Ox.Event.bindKeyboard(stack[stack.length - 1]);
                Ox.print("blur", stack);
            }
        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.History
    ----------------------------------------------------------------------------
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Keyboard
    ----------------------------------------------------------------------------
    */

    (function() {

        var buffer = "",
            bufferTime = 0,
            bufferTimeout = 1000,
            keyNames = function() {
                return {
                    0: "section",
                    8: "backspace",
                    9: "tab",
                    12: "clear",
                    13: "enter",
                    16: "shift",
                    17: "control",
                    18: "alt",
                    20: "capslock",
                    27: "escape",
                    32: "space",
                    33: "pageup",
                    34: "pagedown",
                    35: "end",
                    36: "home",
                    37: "left",
                    38: "up",
                    39: "right",
                    40: "down",
                    45: "insert",
                    46: "delete",
                    47: "help",
                    48: "0",
                    49: "1",
                    50: "2",
                    51: "3",
                    52: "4",
                    53: "5",
                    54: "6",
                    55: "7",
                    56: "8",
                    57: "9",
                    65: "a",
                    66: "b",
                    67: "c",
                    68: "d",
                    69: "e",
                    70: "f",
                    71: "g",
                    72: "h",
                    73: "i",
                    74: "j",
                    75: "k",
                    76: "l",
                    77: "m",
                    78: "n",
                    79: "o",
                    80: "p",
                    81: "q",
                    82: "r",
                    83: "s",
                    84: "t",
                    85: "u",
                    86: "v",
                    87: "w",
                    88: "x",
                    89: "y",
                    90: "z",
                    91: "meta.left",
                    92: "meta.right",
                    93: "select",
                    96: "0.numpad",
                    97: "1.numpad",
                    98: "2.numpad",
                    99: "3.numpad",
                    100: "4.numpad",
                    101: "5.numpad",
                    102: "6.numpad",
                    103: "7.numpad",
                    104: "8.numpad",
                    105: "9.numpad",
                    106: "asterisk.numpad",
                    107: "plus.numpad",
                    109: "minus.numpad",
                    108: "enter.numpad",
                    110: "dot.numpad",
                    111: "slash.numpad",
                    112: "f1",
                    113: "f2",
                    114: "f3",
                    115: "f4",
                    116: "f5",
                    117: "f6",
                    118: "f7",
                    119: "f8",
                    120: "f9",
                    121: "f10",
                    122: "f11",
                    123: "f12",
                    124: "f13",
                    125: "f14",
                    126: "f15",
                    127: "f16",
                    144: "numlock",
                    145: "scrolllock",
                    186: "semicolon",
                    187: "equal",
                    188: "comma",
                    189: "minus",
                    190: "dot",
                    191: "slash",
                    192: "backtick",
                    219: "openbracket",
                    220: "backslash",
                    221: "closebracket",
                    222: "quote"
                    // see dojo, for ex.
                };
            }(),
            modifierNames = {
                altKey: "alt", // mac: option
                ctrlKey: "control",
                metaKey: "meta", // mac: command
                shiftKey: "shift"
            };

        $(function() {
            // fixme: how to do this better?
            if ($.browser.safari) {
                $document.keydown(keypress);                
            } else {
                $document.keypress(keypress);
            }
        });
        function keypress(event) {
            var key,
                keys = [],
                ret = true,
                time;
            $.each(modifierNames, function(k, v) {
                if (event[k]) {
                    keys.push(v);
                }
            });
            // avoid pushing modifier twice
            if (keyNames[event.keyCode] && keys.indexOf(keyNames[event.keyCode]) == -1) {
                keys.push(keyNames[event.keyCode]);
            }
            key = keys.join(".");
            if (key.match(/^[\w\d-]$|SPACE/)) {
                time = Ox.getTime();
                if (time - bufferTime > bufferTimeout) {
                    buffer = "";
                }
                buffer += key == "SPACE" ? " " : key;
                bufferTime = time;                    
            }
            Ox.Event.trigger("key_" + key);
            //return false;
            /*
            $.each(stack, function(i, v) {
                // fixme: we dont get the return value!
                ret = Ox.event.trigger(keyboard + Ox.toCamelCase(key) + "." + v);
                return ret;
            });
            */                
        }
        
    })();

    /*
    ----------------------------------------------------------------------------
    Ox.Mouse (??)
    ----------------------------------------------------------------------------
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Request
    ----------------------------------------------------------------------------
    */

    Ox.Request = function() {

        var cache = {},
            pending = {},
            requests = {},
            self = {
                options: {
                    timeout: 15000,
                    type: "POST",
                    url: "api"
                }
            };

        return {

            cancel: function() {
                var index;
                if (arguments.length == 0) {
                    requests = {};
                } else if (Ox.isFunction(arguments[0])) {
                    // cancel with function
                    $.each(requests, function(id, req) {
                        if (arguments[0](req)) {
                            delete requests[id];
                        }
                    })
                } else {
                    // cancel by id
                    delete requests[arguments[0]]
                }
            },

            emptyCache: function() {
                cache = {};
            },

            options: function(options) {
                return Ox.getset(self.options, options, $.noop(), this);
            },

            send: function(options) {

                options = $.extend({
                    age: -1,
                    callback: function() {},
                    id: Ox.uid(),
                    timeout: self.options.timeout,
                    type: self.options.type,
                    url: self.options.url
                }, options);

                var req = JSON.stringify({
                        url: options.url,
                        data: options.data
                    });

                function callback(data) {
                    delete requests[options.id];
                    options.callback(data);
                }

                function debug(request) {
                    var $iframe = $("<iframe>")
                            .css({ // fixme: should go into a class
                                width: 768,
                                height: 384
                            }),
                        $dialog = new Ox.Dialog({
                                title: "Application Error",
                                buttons: [
                                    {
                                        value: "Close",
                                        click: function() {
                                            $dialog.close();
                                        }
                                    }
                                ],
                                width: 800,
                                height: 400
                            })
                            .append($iframe)
                            .open(),
                        iframe = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
                    iframe.open();
                    iframe.write(request.responseText);
                    iframe.close();
                }

                function error(request, status, error) {
                    var data;
                    if (arguments.length == 1) {
                        data = arguments[0]
                    } else {
                        try {
                            data = JSON.parse(request.responseText);
                        } catch (err) {
                            data = {
                                status: {
                                    code: request.status,
                                    text: request.statusText
                                }
                            };
                        }
                    }
                    if (data.status.code < 500) {
                        callback(data);
                    } else {
                        var $dialog = new Ox.Dialog({
                                title: "Application Error",
                                buttons: [
                                    {
                                        value: "Details",
                                        click: function() {
                                            $dialog.close(function() {
                                                debug(request);                                                
                                            });
                                        }
                                    },
                                    {
                                        value: "Close",
                                        click: function() {
                                            $dialog.close(function() {
                                                callback(data);
                                            });
                                        }
                                    }
                                ],
                                width: 400,
                                height: 100
                            })
                            .append("Sorry, we have encountered an application error while handling your request. To help us find out what went wrong, you may want to report this error to an administrator. Otherwise, please try again later.")
                            .open();
                            // fixme: change this to Send / Don't Send
                        Ox.print({
                            request: request,
                            status: status,
                            error: error
                        });
                    }
                    pending[options.id] = false;
                }

                function success(data) {
                    pending[options.id] = false;
                    try {
                        data = JSON.parse(data);                                    
                    } catch (err) {
                        error({
                            status: {
                                code: 500,
                                text: "Internal Server Error"
                            },
                            data: {}
                        });
                        return;
                    }
                    cache[req] = {
                        data: data,
                        time: Ox.getTime()
                    };
                    callback(data);
                }

                if (pending[options.id]) {
                    setTimeout(function() {
                        Ox.Request.send(options);
                    }, 0);
                } else {
                    requests[options.id] = {
                        url: options.url,
                        data: options.data
                    };
                    if (cache[req] && (options.age == -1 || options.age > Ox.getTime() - cache[req].time)) {
                        setTimeout(function() {
                            callback(cache[req].data);
                        }, 0);
                    } else {
                        pending[options.id] = true;
                        $.ajax({
                            data: options.data,
                            error: error,
                            success: success,
                            timeout: options.timeout,
                            type: options.type,
                            url: options.url
                        });
                    }
                }
                return options.id;
            }

        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.URL
    ----------------------------------------------------------------------------
    */

    /*
    ============================================================================
    Core
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Container
    ----------------------------------------------------------------------------
    */

    // fixme: wouldn't it be better to let the elements be,
    // rather then $element, $content, and potentially others,
    // 0, 1, 2, etc, so that append would append 0, and appendTo
    // would append (length - 1)?
    Ox.Container = function() {
        var that = new Ox.Element()
            .addClass("OxContainer");
        that.$content = new Ox.Element()
            .addClass("OxContent")
            .appendTo(that);
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Element
    ----------------------------------------------------------------------------
    */

    // check out http://ejohn.org/apps/learn/#36 (-#38, making fns work w/o new)

    Ox.Element = function() {

        var elements = {};

        return function(options, self) {

            // construct
            options = options || {};
            self = self || {};
            var that = this;

            // init
            (function() {
                // allow for Ox.Widget("tagname", self)
                if (typeof options == "string") {
                    options = {
                        element: options
                    };
                }
                that.ox = Ox.version;
                that.id = Ox.uid();
                that.$element = $("<" + (options.element || "div") + "/>", {
                    data: {
                        ox: that.id
                    }
                });
                elements[that.id] = that;
                wrapjQuery();
            })();

            // private
            function wrapjQuery() {
                $.each(oxui.jQueryFunctions, function(i, fn) {
                    that[fn] = function() {
                        var args = arguments,
                            length = args.length,
                            id, ret;
                        $.each(args, function(i, arg) {
                            // if an ox object was passed
                            // then pass its $element instead
                            // so we can do oxObj.jqFn(oxObj)
                            if (arg.ox) {
                                args[i] = arg.$element;
                            }
                            /*
                            if (arg.ox) { // fixme: or is this too much magic?
                                if (fn == "appendTo" && arg.$content) {
                                    args[i] = arg.$content
                                } else {
                                    args[i] = arg.$element;
                                }
                            }
                            */
                        });
                        /*
                        if (fn == "html" && that.$content) { // fixme: or is this too much magic?
                            $element = that.$content;
                        } else {
                            $element = that.$element;
                        }
                        */
                        // why does this not work?
                        // ret = that.$element[v].apply(this, arguments);
                        if (length == 0) {
                            ret = that.$element[fn]();
                        } else if (length == 1) {
                            ret = that.$element[fn](args[0]);
                        } else if (length == 2) {
                            ret = that.$element[fn](args[0], args[1]);
                        } else if (length == 3) {
                            ret = that.$element[fn](args[0], args[1], args[2]);
                        } else if (length == 4) {
                            ret = that.$element[fn](args[0], args[1], args[2], args[3]);
                        }
                        if (fn == "data") {
                            // Ox.print("data ret", ret, $(ret))
                        }
                        // if the $element of an ox object was returned
                        // then return the ox object instead
                        // so we can do oxObj.jqFn().oxFn()
                        return ret.jquery && elements[id = ret.data("ox")] ?
                                elements[id] : ret;
                    }
                });
            }

            // shared
            self.onChange = function() {
                // self.onChange(key, value)
                // is called when an option changes
                // (to be implemented by widget)
            };

            // public
            that.bindEvent = function() {
                // fixme: shouldn't this work the other way around,
                // and bind a function to an event triggered by this widget?
                /*
                bindEvent(event, fn) or bindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.bind(that.id, event, fn);
                    });
                } else {
                    Ox.Event.bind(that.id, arguments[0], arguments[1]);
                }
                return that;
            };
            that.defaults = function(defaults) {
                /*
                    that.defaults({foo: x})         sets self.defaults
                */
                self.defaults = defaults;
                return that;
            };
            that.gainFocus = function() {
                Ox.Focus.focus(that.id);
                return that;
            };
            that.hasFocus = function() {
                return Ox.Focus.focused() == that.id;
            };
            that.loseFocus = function() {
                Ox.Focus.blur(that.id);
                return that;
            };
            that.options = function() { // fixme: use Ox.getset
                /*
                    that.options()                  returns self.options
                    that.options("foo")             returns self.options.foo
                    that.options("foo", x)          sets self.options.foo,
                                                    returns that
                    that.options({foo: x, bar: y})  sets self.options.foo
                                                    and self.options.bar,
                                                    returns that
                */
                var length = arguments.length,
                    // args, options, ret;
                    args, ret;
                if (length == 0) {
                    // options()
                    ret = self.options;
                } else if (length == 1 && typeof arguments[0] == "string") {
                    // options(str)
                    ret = self.options[arguments[0]]
                } else {
                    // options (str, val) or options({str: val, ...})
                    // translate (str, val) to ({str: val})
                    args = Ox.makeObject.apply(that, arguments);
                    // if options have not been set, extend defaults,
                    // otherwise, extend options
                    /*
                    options = self.options;
                    */
                    self.options = $.extend(self.options || self.defaults, args);
                    $.each(args, function(key, value) {
                        self.onChange(key, value);
                        /*
                        fixme: why does this not work?
                        Ox.print("options", options, key, value)
                        //Ox.print(!options, !options || !options[key], !options || !options[key] || options[key] !== value)
                        if (!options || !options[key] || options[key] !== value) {
                            Ox.print("onChange...")
                            self.onChange(key, value);
                        } else {
                            Ox.print("NO CHANGE");
                        }
                        */
                    });
                    ret = that;                            
                }
                return ret;
            };
            that.remove = function() {
                that.$element.remove();
                delete elements[that.ox];
                return that;
            };
            that.triggerEvent = function() {
                /*
                triggerEvent(event, fn) or triggerEvent({event0: fn0, event1: fn1, ...})
                */
                if (Ox.isObject(arguments[0])) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.trigger(event + "_" + self.options.id, fn);
                    });
                } else {
                    Ox.Event.trigger(arguments[0] + "_" + self.options.id, arguments[1] || {});
                }
                return that;
            };
            that.unbindEvent = function() {
                /*
                unbindEvent(event, fn) or unbindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.unbind(that.id, event, fn);
                    })
                } else {
                    Ox.Event.unbind(that.id, arguments[0], arguments[1]);
                }
                return that;
            };

            // return
            return that;

        }

    }();

    Ox._Element = function(element) {
        var that = this;
        that.def = {};
        that.opt = {};
        that.ox = Ox.version;
        that.id = Ox.uid();
        //Ox.print("that.id", that.id)
        that.$element = $("<" + (element || "div") + "/>")
            //.addClass("OxElement")
            .data("ox", that.id);
        oxui.elements[that.id] = that;
        // Ox.print("oxui.elements", oxui.elements)
        //function setOption() {};
        that.setOption = function() {};
        /*
        */
        that.destroy = function() {
            that.$element.remove();
            delete oxui.elements[that.ox];
        }
        /*
        */
        that.disable = function() {
            
        }
        /*
        */
        that.enable = function() {
            
        }
        /*
        */
        ///*
        that.defaults = function() {
            var length = arguments.length,
                ret;
            if (length == 0) {
                ret = that.def
            } else if (length == 1 && typeof arguments[0] == "string") {
                ret = that.def[arguments[0]];
            } else {
                // translate ("key", "value") to {"key": "value"}
                that.def = $.extend(
                    that.def, Ox.makeObject.apply(that, arguments)
                );
                ret = that;
            }
            return ret;
        }
        //*/
        /*
        Ox.Element.options()
            get options
        Ox.Element.options("foo")
            get options.foo
        Ox.Element.options("foo", 0)
            set options.foo
        Ox.Element.options({foo: 0, bar: 1})
            set options.foo and options.bar
        */
        ///*
        that.options = function() {
            var length = arguments.length,
                args, ret;
            if (length == 0) {
                //Ox.print("getting all options", options);
                ret = that.opt;
            } else if (length == 1 && typeof arguments[0] == "string") {
                //Ox.print("getting one option", options, arguments[0], options[arguments[0]]);
                ret = that.opt[arguments[0]];
            } else {
                // translate ("key", "value") to {"key": "value"}
                args = Ox.makeObject.apply(that, arguments);
                // if options have been set then extend options,
                // otherwise extend defaults
                that.opt = $.extend(Ox.length(that.opt) ?
                    that.opt : that.def, args);
                // that.trigger("OxElement" + that.id + "SetOptions", args);
                $.each(args, function(k, v) {
                    that.setOption(k, v);
                    //Ox.print("triggering", "OxElement" + that.id + "SetOption", {k: v})
                    //that.trigger("OxElement" + that.id + "SetOption", {k: v});
                })
                ret = that;
            }
            return ret;
        }
        // should become self.publish
        that.publish = function(event, data) {
            Ox.Event.publish(event + that.id, data);
            return that;
        }
        that.subscribe = function(event, callback) {
            Ox.Event.subscribe(event, callback);
            return that;
        }
        //that.setOptions = function() {};
        //*/
        // wrap jquery functions
        // so we can do oxObj.jqFn()
        $.each(oxui.jqueryFunctions, function(i, v) {
            that[v] = function() {
                var args = arguments,
                    length = args.length,
                    $element, id, ret;
                $.each(args, function(i, v) {
                    // if an oxui object was passed
                    // then pass its $element instead
                    // so we can do jqObj.append(oxObj)
                    if (v.ox) {
                        args[i] = v.$element;
                    }
                });
                if (v == "html" && that.$content) {
                    $element = that.$content;
                } else {
                    $element = that.$element;
                }
                // why does this not work?
                // ret = that.$element[v].apply(this, arguments);
                // maybe because we pass this, and not that.$element[v] ... ?
                // ret = that.$element[v].apply(that.$element[v], arguments);
                // doesn't work either ...
                if (length == 0) {
                    ret = $element[v]();
                } else if (length == 1) {
                    ret = $element[v](args[0]);
                } else if (length == 2) {
                    ret = $element[v](args[0], args[1]);
                } else if (length == 3) {
                    ret = $element[v](args[0], args[1], args[2]);
                } else if (length == 4) {
                    ret = $element[v](args[0], args[1], args[2], args[3]);
                }
                // if the $element of an oxui object was returned
                // then return the oxui object instead
                // so we can do oxObj.jqFn().oxFn()
                //Ox.print("v", v, "arguments", arguments)
                if (ret.jquery) {
                    //Ox.print("ret", ret, "ret.data('id')", ret.data("ox"))
                }
                return ret.jquery && oxui.elements[id = ret.data("ox")] ?
                    oxui.elements[id] : ret;
            }
        });
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.theme()
        get theme
    Ox.theme("foo")
        set theme to "foo"
    ----------------------------------------------------------------------------
    */

    Ox.theme = function() {
        var length = arguments.length,
            classes = $body.attr("class").split(" "),
            arg, theme;
        $.each(classes, function(i, v) {
            if (Ox.startsWith(v, "OxTheme")) {
                theme = v.replace("OxTheme", "").toLowerCase();
                if (length == 1) {
                    $body.removeClass(v);
                }
                return false;
            }
        });
        if (length == 1) {
            arg = arguments[0]
            $body.addClass("OxTheme" + Ox.toTitleCase(arg));
            if (theme) {
                $("input[type=image]").each(function() {
                    var $this = $(this);
                    $this.attr({
                        src: $this.attr("src").replace(
                            "/ox.ui." + theme + "/", "/ox.ui." + arg + "/"
                        )
                    });
                });
            }
        }
        return theme;
    };

    /*
    ============================================================================
    Bars
    ============================================================================
    */

    Ox.Bar = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    orientation: "horizontal",
                    size: 16
                })
                .options(options || {})
                .addClass("OxBar Ox" + Ox.toTitleCase(self.options.orientation)),
            dimensions = oxui.getDimensions(self.options.orientation);
        that.css(dimensions[0], "100%")
            .css(dimensions[1], self.options.size + "px");
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Tabbar
    ----------------------------------------------------------------------------
    */

    Ox.Tabbar = function(options, self) {

        var self = self || {},
            that = new Ox.Bar({
                    size: 20
                }, self)
                .defaults({
                    selected: 0,
                    tabs: []
                })
                .options(options || {})
                .addClass("OxTabbar");

        Ox.ButtonGroup({
            buttons: self.options.tabs,
            group: true,
            selectable: true,
            selected: self.options.selected,
            size: "medium",
            style: "tab",
        }).appendTo(that);

        return that;

    };

    Ox.Toolbar = function(options, self) {
        var self = self || {},
            that = new Ox.Bar({
                size: oxui.getBarSize(options.size)
            }, self);
        return that;
    };

    /*
    ============================================================================
    Ox.Dialog
    ============================================================================
    */

    Ox.Dialog = function(options, self) {
        // fixme: this was just pasted from previous version ... update
        // fixme: dialog should be derived from a generic draggable
        var self = self || {},
            options = $.extend({
                title: "",
                buttons: [],
                width: 384,
                height: 128
            }, options),
            that = new Ox.Element()
                .addClass("OxDialog")
                .css({
                    left: (($(document).width() - options.width) / 2) + "px",
                    top: (($(document).height() - options.height - 92) / 2) + "px",
                    width: options.width + "px",
                    height: (options.height + 92) + "px"
                });
        that.$titlebar = new Ox.Bar({
                size: "medium"
            })
            .addClass("OxTitleBar")
            //.html(options.title)
            .mousedown(function(e) {
                var offset = that.offset(),
                    //maxLeft = $(document).width() - that.width(),
                    //maxTop = $(document).height() - that.height(),
                    x = e.clientX,
                    y = e.clientY,
                    documentWidth = $(document).width();
                    documentHeight = $(document).height();
                $(window).mousemove(function(e) {
                    $("*").css({
                        WebkitUserSelect: "none"
                    });
                    var left = Ox.limit(offset.left - x + e.clientX, 24 - options.width, documentWidth - 24),
                        top = Ox.limit(offset.top - y + e.clientY, 24, documentHeight - 24);
                    that.css({
                        left: left + "px",
                        top: top + "px"
                    });
                });
                $(window).one("mouseup", function() {
                    $(window).unbind("mousemove");
                    $("*").css({
                        WebkitUserSelect: "auto"
                    });
                });
            })
            .appendTo(that);
        that.$title = new Ox.Element()
            .addClass("OxTitle")
            .html(options.title)
            .appendTo(that.$titlebar);
        that.$content = new Ox.Container()
            .addClass("OxContent")
            .css({
                height: options.height + "px"
            })
            .appendTo(that);
        that.$buttonsbar = new Ox.Element()
            .addClass("OxButtonsBar")
            .appendTo(that);
        that.$buttons = [];
        $.each(options.buttons, function(i, button) {
            that.$buttons[i] = new Ox.Button({
                size: "medium",
                value: button.value
            }).click(button.click).appendTo(that.$buttonsbar);
        });
        that.$buttons[0].focus();
        that.$layer = $(".OxLayer"); // fixme: lazy loading of layer is fine, but save in var, dont look up
        self.onChange = function(key, value) {
            if (key == "title") {
                that.$title.html(value);
            }
        }
        that.append = function($element) {
            that.$content.append($element);
            return that;
        }
        that.close = function(callback) {
            callback = callback || function() {};
            that.animate({
                opacity: 0
            }, 200, function() {
                that.remove();
                that.$layer.remove();
                callback();
            })
        }
        that.disableButtons = function() {
            // to be used on submit of form, like login
        };
        that.open = function() {
            if (!that.$layer.length) {
                that.$layer = new Ox.Element()
                    .addClass("OxLayer")
                    .appendTo($body);
            }
            that.css({
                opacity: 0
            }).appendTo(that.$layer).animate({
                opacity: 1
            }, 200);
            return that;
        }
        return that;
    }

    /*
    ============================================================================
    Forms
    ============================================================================
    */

    Ox.Form = function(options, self) {

        var self = self || {},
            that = new Ox.Element("div", self)
                .defaults()
                .options();

    };

    /*
    ----------------------------------------------------------------------------
    Ox.Button
    ----------------------------------------------------------------------------
    */

    Ox.Button = function(options, self) {
        /*
        events:
        click       non-selectable button was clicked
        deselect    selectable button was deselected
        select      selectable button was selected
        */
        var self = self || {},
            that = new Ox.Element("input", self)
                .defaults({
                    disabled: false,
                    group: null,
                    id: "",
                    selectable: false,
                    selected: false,
                    size: "medium",
                    style: "default", // can be default, symbol or tab
                    type: "text",
                    value: "",
                    values: []
                })
                .options($.extend(options, {
                    value: $.isArray(options.value) ?
                        options.value[0] : options.value,
                    values: $.makeArray(options.value)
                }))
                .attr({
                    disabled: self.options.disabled ? "disabled" : "",
                    type: self.options.type == "text" ? "button" : "image"
                })
                .addClass("OxButton Ox" + Ox.toTitleCase(self.options.size) +
                    (self.options.style ? " Ox" + Ox.toTitleCase(self.options.style) : "") +
                    (self.options.disabled ? " OxDisabled": "") +
                    (self.options.selected ? " OxSelected": ""))
                .mousedown(mousedown)
                .click(click);
        //Ox.print(self.options.value, self.options.disabled)
        /*
        that.bind("OxElement" + that.id + "SetOptions", function(e, data) {
            if (typeof data.selected != "undefined") {
                if (data.selected != that.hasClass("OxSelected")) {
                    that.toggleClass("OxSelected");
                }
            }
            if (typeof data.value != "undefined") {
                if (self.options.type == "image") {
                    that.attr({
                        src: oxui.path + "png/" + Ox.theme() +
                            "/button" + Ox.toTitleCase(options.value) + ".png"
                    });
                } else {
                    that.val(self.options.value);
                }
            }
        })
        */
        function mousedown(e) {
            if (self.options.type == "image" && $.browser.safari) {
                // keep image from being draggable
                e.preventDefault();
            }
        }
        function click() {
            if (!self.options.selectable) {
                that.triggerEvent("click");
            } else if (!self.options.group || !self.options.selected) {
                if (self.options.group) {
                    that.triggerEvent("select");
                } else {
                    that.toggleSelected();
                }
            }
            if (self.options.values.length == 2) {
                Ox.print("2 values")
                that.options({
                    value: self.options.value == self.options.values[0] ?
                        self.options.values[1] : self.options.values[0]
                }); 
            }
            //self.options.click();
        }
        self.onChange = function(key, value) {
            //Ox.print("setOption", option, value)
            Ox.print("OxButton onChange", key, value)
            if (key == "selected") {
                if (value != that.hasClass("OxSelected")) { // fixme: neccessary?
                    that.toggleClass("OxSelected");
                }
                that.triggerEvent("change");
            } else if (key == "value") {
                Ox.print("OxButton onChange value", value)
                if (self.options.type == "image") {
                    that.attr({
                        src: oxui.path + "png/ox.ui." + Ox.theme() +
                            "/button" + Ox.toTitleCase(value) + ".png"
                    });
                } else {
                    that.val(value);
                }
            }
        }
        that.toggleDisabled = function() {
            that.options({
                enabled: !self.options.disabled
            });
        }
        that.toggleSelected = function() {
            that.options({
                selected: !self.options.selected
            });
        }
        that.options("value", self.options.value);
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.ButtonGroup
    ----------------------------------------------------------------------------
    */

    Ox.ButtonGroup = function(options, self) {

        /*
        events:
        change  {id, value}     selection within a group changed
        */

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    buttons: [],
                    group: false,
                    selectable: false,
                    selected: -1,
                    size: "medium",
                    style: "",
                    type: "text",
                })
                .options(options || {})
                .addClass("OxButtonGroup");
        self.position = {};

        that.$buttons = [];
        $.each(self.options.buttons, function(position, button) {
            that.$buttons[position] = Ox.Button({
                disabled: button.disabled,
                group: self.options.group ? that : null,
                id: button.id,
                selectable: self.options.selectable,
                selected: position == self.options.selected,
                size: self.options.size,
                style: self.options.style,
                type: self.options.type,
                value: button.value
            }).appendTo(that);
            self.position[that.$buttons.id] = position;
            that.bindEvent("select_" + that.$buttons[position].options("id"), function() {
                selectButton(position);
            });
        });

        function onChange(event, data) {
            console.log("event", event, "data", data)
            var id = event.split("_")[1];
            Ox.print("OK")
            if (self.options.selected > -1) {
                that.$buttons[self.options.selected].toggleSelected();
            }
            self.options.selected = self.position[id];
            that.triggerEvent("change", {
                value: id
            });
        }

        function selectButton(position) {
            that.$buttons[self.options.selected].toggleSelected();
            self.options.selected = position;
            that.$buttons[self.options.selected].toggleSelected();
        };

        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Input
    ----------------------------------------------------------------------------
    */

    Ox.Input = function(options, self) {
        var self = self || {},
            that = new Ox.Element(
                    options.type == "textarea" ? "textarea" : "input", self
                )
                .defaults({
                    autocomplete: null,
                    id: "",
                    placeholder: "", // can be [], will result in select
                    size: "medium",
                    type: "text"
                })
                .options(options || {})
                .attr({
                    placeholder: self.options.placeholder
                })
                .addClass(
                    "OxInput Ox" + Ox.toTitleCase(self.options.size) +
                    " OxPlaceholder"
                )
                .focus(focus)
                .blur(blur)
                .change(change);
        if (options.autocomplete) {
            self.element = that.$element[0];
            self.menuId = self.options.id + "_menu"; // fixme: we do this in other places ... are we doing it the same way? var name?
            self.menu = new Ox.Menu({
                element: that,
                id: self.menuId,
                offset: {
                    left: 4,
                    top: 0
                },
                size: self.options.size
            });
            that.bindEvent("click_" + self.menuId, onClick);
            //that.bindEvent("deselect_" + self.menuId, onDeselect);
            //that.bindEvent("select_" + self.menuId, onSelect);
        }
        if (options.type != "textarea") {
            that.attr({
                type: self.options.type
            });
        }
        function autocomplete(items) {
            var selected = 0;
            if (items.length) {
                items = $.map(items, function(title, position) {
                    if (that.val().toLowerCase() == Ox.stripTags(title.toLowerCase())) {
                        selected = position;
                    }
                    return {
                        id: title.toLowerCase(), // fixme: need function to do lowercase, underscores etc?
                        title: title
                    };
                });
                self.menu.options({
                    items: items,
                    selected: selected
                }).showMenu();
            } else {
                self.menu.hideMenu();
            }
        }
        function change() {
            
        }
        function blur() {
            if (that.val() === "") {
                that.addClass("OxPlaceholder").val(that.attr("placeholder"));
            }
            if (self.options.autocomplete) {
                $document.unbind("keydown", keypress);
                $document.unbind("keypress", keypress);
            }
        }
        function focus() {
            if (that.is(".OxPlaceholder")) {
                that.val("").removeClass("OxPlaceholder");
            }
            if (self.options.autocomplete) {
                // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
                $document.bind("keydown", keypress);
                $document.bind("keypress", keypress);
                self.options.autocomplete(that.val(), autocomplete);
            }
        }
        function keypress(event) {
            if (event.keyCode != 13) {
                setTimeout(function() {
                    var value = that.val();
                    if (self.options.autocomplete && value != self.value) {
                        self.value = value;
                        self.options.autocomplete(value, autocomplete);
                    }
                }, 25);
            }
        }
        function onClick(event, data) {
            Ox.print("onClick", data)
            that.focus().val(Ox.stripTags(data.title));
            self.menu.hideMenu();
        }
        function selection() {
            var start, end;
            if (arguments.length == 0) {
                return [self.element.selectionStart, self.element.selectionEnd];
            } else {
                start = arguments[0];
                end = arguments[1] || start;
                self.element.setSelectionRange(start, end);
            }
        }
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Range

    options:
        animate     boolean         if true, animate thumb
        arrows      boolean         if true, show arrows
        arrowImages array           arrow symbols, like ["minus", "plus"]
        arrowStep   number          step when clicking arrows
        max         number          maximum value
        min         number          minimum value
        orientation string          "horizontal" or "vertical"
        step        number          step between values
        size        number          width or height, in px
        thumbSize   number          minimum width or height of thumb, in px
        thumbValue  boolean         if true, display value on thumb
        trackImages string or array one or multiple track background image URLs          
        trackStep   number          0 (scroll here) or step when clicking track
        value       number          initial value
    ----------------------------------------------------------------------------
    */

    Ox.Range = function(options, self) {

        /*
            init
        */
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    animate: false,
                    arrows: false,
                    arrowImages: ["previous", "next"],
                    arrowStep: 1,
                    max: 100,
                    min: 0,
                    orientation: "horizontal",
                    step: 1,
                    size: 128,
                    thumbSize: 16,
                    thumbValue: false,
                    trackImages: [],
                    trackStep: 0,
                    value: 0
                })
                .options($.extend(options, {
                    arrowStep: options.arrowStep ?
                        options.arrowStep : options.step,
                    trackImages: $.makeArray(options.trackImages || [])
                }))
                .addClass("OxRange");

        // fixme: self. ... ?
        var trackImages = self.options.trackImages.length,
            values = (self.options.max - self.options.min + self.options.step) /
                    self.options.step;

        /*
            construct
        */
        that.$element
            .css({
                width: self.options.size + "px"
            });
        if (self.options.arrows) {
            var $arrowDec = Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: self.options.arrowImages[0]
                })
                .addClass("OxArrow")
                .mousedown(mousedownArrow)
                .click(clickArrowDec)
                .appendTo(that.$element);
        }
        var $track = new Ox.Element()
                .addClass("OxTrack")
                .mousedown(clickTrack)
                .appendTo(that.$element); // fixme: make that work

        if (trackImages) {
            var width = parseFloat(screen.width / trackImages),
                $image = $("<canvas/>")
                    .attr({
                        width: width * trackImages,
                        height: 14
                    })
                    .addClass("OxImage")
                    .appendTo($track.$element), // fixme: make that work
                c = $image[0].getContext('2d');
                c.mozImageSmoothingEnabled = false; // we may want to remove this later
            $.each(self.options.trackImages, function(i, v) {
                //Ox.print(v)
                $("<img/>")
                    .attr({
                        src: v
                    })
                    .load(function() {
                        c.drawImage(this, i * width, 0, width, 14);
                    });
            });
        }
        var $thumb = Ox.Button({})
            .addClass("OxThumb")
            .appendTo($track);
        if (self.options.arrows) {
            var $arrowInc = Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: self.options.arrowImages[1]
                })
                .addClass("OxArrow")
                .mousedown(mousedownArrow)
                .click(clickArrowInc)
                .appendTo(that.$element);
        }
        var rangeWidth, trackWidth, imageWidth, thumbWidth;
        setWidth(self.options.size);

        /*
            private functions
        */

        function clickArrowDec() {
            that.removeClass("OxActive");
            setValue(self.options.value - self.options.arrowStep, 200)
        }
        function clickArrowInc() {
            that.removeClass("OxActive");
            setValue(self.options.value + self.options.arrowStep, 200);
        }
        function clickTrack(e) {
            Ox.Focus.focus();
            var left = $track.offset().left,
                offset = $(e.target).hasClass("OxThumb") ?
                    e.clientX - $thumb.offset().left - thumbWidth / 2 - 2 : 0;
            function val(e) {
                return getVal(e.clientX - left - offset);
            }
            setValue(val(e), 200);
            $window.mousemove(function(e) {
                setValue(val(e));
            });
            $window.one("mouseup", function() {
                $window.unbind("mousemove");
            });
        }
        function getPx(val) {
            var pxPerVal = (trackWidth - thumbWidth - 2) /
                (self.options.max - self.options.min);
            return Math.ceil((val - self.options.min) * pxPerVal + 1);
        }
        function getVal(px) {
            var px = trackWidth / values >= 16 ? px : px - 8,
                valPerPx = (self.options.max - self.options.min) /
                        (trackWidth - thumbWidth);    
            return Ox.limit(self.options.min +
                Math.floor(px * valPerPx / self.options.step) * self.options.step,
                self.options.min, self.options.max);
        }
        function mousedownArrow() {
            that.addClass("OxActive");
        }
        function setThumb(animate) {
            var animate = typeof animate != "undefined" ? animate : 0;
            $thumb.animate({
                marginLeft: (getPx(self.options.value) - 2) + "px",
                width: thumbWidth + "px"
            }, self.options.animate ? animate : 0, function() {
                if (self.options.thumbValue) {
                    $thumb.options({
                        value: self.options.value
                    });
                }
            });
        }
        function setValue(val, animate) {
            val = Ox.limit(val, self.options.min, self.options.max);
            if (val != self.options.value) {
                that.options({
                    value: val
                });
                setThumb(animate);
                //Ox.print("triggering OxRange" + that.id + "Change")
                that.triggerEvent("change", { value: val });
            }
        }
        function setWidth(width) {
            trackWidth = width - self.options.arrows * 32;
            thumbWidth = Math.max(trackWidth / values - 2, self.options.thumbSize - 2);
            that.$element.css({
                width: (width - 2) + "px"
            });
            $track.css({
                width: (trackWidth - 2) + "px"
            });
            if (trackImages) {
                $image.css({
                    width: (trackWidth - 2) + "px"
                });
            }
            $thumb.css({
                width: (thumbWidth - 2) + "px",
                padding: 0
            });
            setThumb();
        }

        /*
            shared functions
        */

        self.onChange = function(option, value) {
            
        }

        return that;

    };

    /*
    ----------------------------------------------------------------------------
    Ox.Select
    ----------------------------------------------------------------------------
    */

    Ox.Select = function(options, self) {

        var self = self || {},
            that = new Ox.Element("div", self)
                .defaults({
                    id: "",
                    items: [],
                    size: "medium"
                })
                .options(options)
                .addClass("OxSelect Ox" + Ox.toTitleCase(self.options.size));
        self.buttonId = self.options.id + "_button"
        self.groupId = self.options.id + "_group"
        self.menuId = self.options.id + "_menu",

        $.each(self.options.items, function(i, item) {
            self.options.items[i] = $.extend(self.options.items[i], {
                checked: item.checked || false,
                group: self.groupId
            });
            if (item.checked) {
                self.selected = i;
            }
        });

        that.$button = new Ox.Button($.extend(self.options, {
                id: self.buttonId,
                type: "text", // fixme: this shouldn't be necessary
                value: self.options.items[self.selected].title
            }), {})
            .click(clickButton)
            .appendTo(that);

        that.$symbol = $("<div>", {
                "class": "OxSymbol",
                html: oxui.symbols.select
            })
            .click(function() {
                that.$button.trigger("click");
            })
            .appendTo(that.$element);

        that.$menu = new Ox.Menu({
            element: that.$button,
            id: self.menuId,
            items: self.options.items,
            offset: {
                left: 8,
                top: 0
            },
            side: "bottom",
            size: self.options.size
        });
        
        that.bindEvent("change_" + self.groupId, clickMenu);

        function clickButton() {
            that.$menu.toggleMenu();
        }

        function clickMenu(event, data) {
            Ox.print("clickMenu", event, data)
            that.$button.options({
                value: data.value
            });
            that.triggerEvent("change", data.value);
        }

        self.onChange = function(key, value) {
            
        };

        that.width = function(val) {
            // fixme: silly hack, and won't work for css()
            that.$element.width(val);
            that.$button.width(val);
            that.$symbol.width(val);
            return that;
        };

        return that;

    }

    /*
    ============================================================================
    Lists
    ============================================================================
    */

    Ox.List = function(options, self) {

        var self = self || {},
            that = new Ox.Container({}, self);

        return that;

    };

    Ox.ListItem = function(options, self) {
        
    };

    /*
    ============================================================================
    Menus
    ============================================================================
    */

    Ox.MainMenu = function(options, self) {

        var self = self || {},
            that = new Ox.Bar({}, self)
                .defaults({
                    menus: [],
                    size: "medium"
                })
                .options(options || {})
                .addClass("OxMainMenu Ox" + Ox.toTitleCase(self.options.size)) // fixme: bar should accept small/medium/large
                .click(click)
                .mousemove(mousemove);

        self.focused = false;
        self.selected = -1;
        that.menus = [];
        that.titles = [];
        that.layer = $("<div>").addClass("OxLayer");

        $.each(options.menus, function(position, menu) {
            var event = 
            that.titles[position] = $("<div>")
                .addClass("OxTitle")
                .html(menu.title)
                .data("position", position)
                .appendTo(that.$element);
            that.menus[position] = new Ox.Menu($.extend(menu, {
                element: that.titles[position],
                mainmenu: that,
                size: self.options.size
            }));
            that.bindEvent("hide_" + that.menus[position].options("id"), onHideMenu);
        });

        function click(event) {
            var $target = $(event.target),
                position = typeof $target.data("position") != "undefined" ?
                        $target.data("position") : -1;
            clickTitle(position);
        }

        function clickTitle(position) {
            var selected = self.selected;
            if (self.selected > -1) {
                that.menus[self.selected].hideMenu();
            }
            if (position > -1) {
                Ox.print("position", position, "self.selected", self.selected)
                if (position != selected) {
                    self.focused = true;
                    self.selected = position;
                    Ox.print("position", position, "setting self.selected", self.selected)
                    that.titles[self.selected].addClass("OxSelected");
                    that.menus[self.selected].showMenu();
                }
            }
        }

        function mousemove(event) {
            var $target = $(event.target),
                focused,
                position = typeof $target.data("position") != "undefined" ?
                        $target.data("position") : -1;
            if (self.focused && position != self.selected) {
                if (position > -1) {
                    clickTitle(position);                        
                } else {
                    focused = self.focused;
                    that.menus[self.selected].hideMenu();
                    self.focused = focused;
                }
            }
        }

        function onHideMenu() {
            Ox.print("hideMenu self.selected", self.selected)
            if (self.selected > -1) {
                that.titles[self.selected].removeClass("OxSelected");
                self.selected = -1;
            }
            self.focused = false;
        }

        self.onChange = function(key, value) {

        };

        that.addMenuAfter = function() {
            
        };

        that.addMenuBefore = function() {
            
        };

        that.disableItem = function(id) {
            
        };

        that.enableItem = function(id) {

        };

        that.removeMenu = function() {
            
        };

        that.selectNextMenu = function() {
            if (self.selected < self.options.menus.length - 1) {
                clickTitle(self.selected + 1);
            }
        };

        that.selectPreviousMenu = function() {
            if (self.selected) {
                clickTitle(self.selected - 1);
            }
        };

        return that;

    };

    Ox.Menu = function(options, self) {

        /*

        options:
        element         the element the menu is attached to
        id              the menu id
        items           array of menu items
        mainmenu        the main menu this menu is part of, if any
        offset          offset of the menu, in px
        parent          the supermenu, if any
        selected        the position of the selected item
        side            open to "bottom" or "right"
        size            "large", "medium" or "small"

        events:
        change_groupId  {id, value}     checked item of a group has changed
        click_itemId                    item not belonging to a group was clicked
        click_menuId    {id, value}     item not belonging to a group was clicked
        deselect_menuId {id, value}     item was deselected                             not needed, not implemented
        hide_menuId                     menu was hidden
        select_menuId   {id, value}     item was selected                               not needed, not implemented

        */

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    element: null,
                    id: "",
                    items: [],
                    mainmenu: null,
                    offset: {
                        left: 0,
                        top: 0
                    },
                    parent: null,
                    selected: -1,
                    side: "bottom",
                    size: "medium",
                })
                .options(options)
                .addClass(
                    "OxMenu Ox" + Ox.toTitleCase(self.options.side) +
                    " Ox" + Ox.toTitleCase(self.options.size)
                )
                .click(click)
                .mouseenter(mouseenter)
                .mouseleave(mouseleave)
                .mousemove(mousemove),
            itemHeight = self.options.size == "small" ? 12 : (self.options.size == "medium" ? 16 : 20),
            // menuHeight,
            scrollSpeed = 1,
            $item; // fixme: used?
            // fixme: attach all private vars to self?

        // construct
        that.items = [];
        that.submenus = {};
        that.$scrollbars = [];
        that.$top = $("<div>")
            .addClass("OxTop")
            .appendTo(that.$element);
        that.$scrollbars.up = constructScrollbar("up")
            .appendTo(that.$element);
        that.$container = $("<div>")
            .addClass("OxContainer")
            .appendTo(that.$element);
        that.$content = $("<table>")
            .addClass("OxContent")
            .appendTo(that.$container);
        constructItems(self.options.items);
        that.$scrollbars.down = constructScrollbar("down")
            .appendTo(that.$element);
        that.$bottom = $("<div>")
            .addClass("OxBottom")
            .appendTo(that.$element);
        that.$layer = $("<div>")
            .addClass(self.options.mainmenu ? "OxMainMenuLayer" : "OxLayer");

        function click(event) {
            var item,
                position,
                $target = $(event.target);
            if ($target.is(".OxCell")) {
                position = $target.parent().data("position");
                item = that.items[position];
                if (!item.options("disabled")) {
                    clickItem(position);
                } else {
                    that.hideMenu();
                }
            } else {
                that.hideMenu();
            }
        }

        function clickItem(position) {
            var item = that.items[position];
            Ox.print("clickItem", position, item.options("items").length)
            if (!item.options("items").length) {
                if (that.options("parent")) {
                    that.options("parent").hideMenu().triggerEvent("click");
                }
                if (item.options("checked") !== null && (!item.options("group") || !item.options("checked"))) {
                    item.options({
                        checked: !item.options("checked")
                    });
                    Ox.Event.trigger("change_" + item.options("group"), {
                        id: item.options("id"),
                        value: item.options("title")[0] // fixme: value or title?
                    });
                } else {
                    Ox.Event.trigger("click_" + self.options.id, {
                        id: item.options("id"),
                        title: item.options("title")[0]
                    });
                    Ox.Event.trigger("click_" + item.options("id"));
                }
                if (item.options("title").length == 2) {
                    item.toggleTitle();
                }
            }
            that.hideMenu();
        }

        function clickSelectedItem() {
            // called on key.enter
            if (self.options.selected > -1) {
                clickItem(self.options.selected);
            } else {
                that.hideMenu();
            }
        }

        function constructItems(items) {
            that.items = [];
            that.$content.empty();
            scrollMenuUp();
            $.each(items, function(i, item) {
                var position;
                if (item.id) {
                    that.items.push(new Ox.MenuItem($.extend(item, {
                        menu: that,
                        position: position = that.items.length
                    })).data("position", position).appendTo(that.$content)); // fixme: jquery bug when passing {position: position}? does not return the object?;
                    if (item.items) {
                        that.submenus[item.id] = new Ox.Menu({
                            element: that.items[position],
                            id: Ox.toCamelCase(self.options.id + "/" + item.id),
                            items: item.items,
                            mainmenu: self.options.mainmenu,
                            offset: {
                                left: 0,
                                top: -4
                            },
                            parent: that,
                            side: "right",
                            size: self.options.size,
                        });
                    }
                } else {
                    that.$content.append(constructSpace());
                    that.$content.append(constructLine());
                    that.$content.append(constructSpace());
                }
            });
            if (!that.is(":hidden")) {
                Ox.print("hide&show")
                that.hideMenu();
                that.showMenu();
            }
        }

        function constructLine() {
            return $("<tr>").append(
                $("<td>", {
                    "class": "OxLine",
                    colspan: 5
                })
            );
        }

        function constructScrollbar(direction) {
            var interval,
                speed = direction == "up" ? -1 : 1;
            return $("<div/>", {
                "class": "OxScrollbar Ox" + Ox.toTitleCase(direction),
                html: oxui.symbols["triangle_" + direction],
                click: function() { // fixme: do we need to listen to click event?
                    return false;
                },
                mousedown: function() {
                    scrollSpeed = 2;
                    return false;
                },
                mouseenter: function() {
                    var $otherScrollbar = that.$scrollbars[direction == "up" ? "down" : "up"];
                    $(this).addClass("OxSelected");
                    if ($otherScrollbar.is(":hidden")) {
                        $otherScrollbar.show();
                        that.$container.height(that.$container.height() - itemHeight);
                        if (direction == "down") {
                            that.$content.css({
                                top: -itemHeight + "px"
                            });
                        }
                    }
                    scrollMenu(speed);
                    interval = setInterval(function() {
                        scrollMenu(speed);
                    }, 100);
                },
                mouseleave: function() {
                    $(this).removeClass("OxSelected");
                    clearInterval(interval);
                },
                mouseup: function() {
                    scrollSpeed = 1;
                    return false;
                }
            });
        }

        function constructSpace() {
            return $("<tr>").append(
                $("<td>", {
                    "class": "OxSpace",
                    colspan: 5
                })
            );
        }

        function getElement(id) {
            // fixme: needed?
            return $("#" + Ox.toCamelCase(options.id + "/" + id));
        }

        function isFirstEnabledItem() {
            var ret = true;
            $.each(that.items, function(i, item) {
                if (i < self.options.selected && !item.options("disabled")) {
                    return ret = false;
                }
            });
            return ret;
        }

        function isLastEnabledItem() {
            var ret = true;
            $.each(that.items, function(i, item) {
                if (i > self.options.selected && !item.options("disabled")) {
                    return ret = false;
                }
            });
            return ret;
        }

        function mouseenter() {
            that.gainFocus();
        }

        function mouseleave() {
            if (self.options.selected > -1 && !that.items[self.options.selected].options("items").length) {
                selectItem(-1);
            }
        }

        function mousemove(event) {
            var item,
                position,
                $target = $(event.target);
            if ($target.is(".OxCell")) {
                position = $target.parent().data("position");
                item = that.items[position];
                if (!item.options("disabled") && position != self.options.selected) {
                    selectItem(position);
                }
            } else {
                mouseleave();
            }
        }

        function scrollMenu(speed) {
            var containerHeight = that.$container.height(),
                contentHeight = that.$content.height(),
                top = parseInt(that.$content.css("top")) || 0,
                min = containerHeight - contentHeight + itemHeight,
                max = 0;
            top += speed * scrollSpeed * -itemHeight;
            if (top <= min) {
                top = min;
                that.$scrollbars.down.hide().trigger("mouseleave");
                that.$container.height(containerHeight + itemHeight);
                that.items[that.items.length - 1].trigger("mouseover");
            } else if (top >= max - itemHeight) {
                top = max;
                that.$scrollbars.up.hide().trigger("mouseleave");
                that.$container.height(containerHeight + itemHeight);
                that.items[0].trigger("mouseover");
            }
            that.$content.css({
                top: top + "px"
            });
        }

        function scrollMenuUp() {
            if (that.$scrollbars.up.is(":visible")) {
                that.$content.css({
                    top: "0px"
                });
                that.$scrollbars.up.hide();
                if (that.$scrollbars.down.is(":hidden")) {
                    that.$scrollbars.down.show();
                } else {
                    that.$container.height(that.$container.height() + itemHeight);
                }
            }
        }

        function selectItem(position) {
            var item;
            if (self.options.selected > -1) {
                item = that.items[self.options.selected]
                item.removeClass("OxSelected");
            }
            if (position > -1) {
                item = that.items[position];
                $.each(that.submenus, function(id, submenu) {
                    if (!submenu.is(":hidden")) {
                        submenu.hideMenu();
                        return false;
                    }
                });
                item.options("items").length && that.submenus[item.options("id")].showMenu(); // fixme: do we want to switch to this style?
                item.addClass("OxSelected");
            }
            self.options.selected = position;
        }

        function selectNextItem() {
            var offset,
                selected = self.options.selected;
            if (!isLastEnabledItem()) {
                if (selected == -1) {
                    scrollMenuUp();
                } else {
                    that.items[selected].removeClass("OxSelected");
                }
                do {
                    selected++;                    
                } while (that.items[selected].options("disabled"))
                selectItem(selected);
                offset = that.items[selected].offset().top + itemHeight -
                        that.$container.offset().top - that.$container.height();
                if (offset > 0) {
                    if (that.$scrollbars.up.is(":hidden")) {
                        that.$scrollbars.up.show();
                        that.$container.height(that.$container.height() - itemHeight);
                        offset += itemHeight;
                    }
                    if (selected == that.items.length - 1) {
                        that.$scrollbars.down.hide();
                        that.$container.height(that.$container.height() + itemHeight);
                    } else {
                        that.$content.css({
                            top: ((parseInt(that.$content.css("top")) || 0) - offset) + "px"
                        });
                    }
                }
            }            
        }

        function selectPreviousItem() {
            var offset,
                selected = self.options.selected;
            if (selected > - 1) {
                if (!isFirstEnabledItem()) {
                    that.items[selected].removeClass("OxSelected");
                    do {
                        selected--;                    
                    } while (that.items[selected].options("disabled"))
                    selectItem(selected);
                }
                offset = that.items[selected].offset().top - that.$container.offset().top;
                Ox.print(offset);
                if (offset < 0) {
                    if (that.$scrollbars.down.is(":hidden")) {
                        that.$scrollbars.down.show();
                        that.$container.height(that.$container.height() - itemHeight);
                    }
                    if (selected == 0) {
                        that.$scrollbars.up.hide();
                        that.$container.height(that.$container.height() + itemHeight);
                    }
                    that.$content.css({
                        top: ((parseInt(that.$content.css("top")) || 0) - offset) + "px"
                    });
                }
            }
        }

        function selectSubmenu() {
            if (self.options.selected > -1) {
                var submenu = that.submenus[that.items[self.options.selected].options("id")];
                if (submenu && submenu.hasEnabledItems()) {
                    submenu.gainFocus();
                    submenu.selectFirstItem();
                } else if (self.options.mainmenu) {
                    self.options.mainmenu.selectNextMenu();
                }
            } else if (self.options.mainmenu) {
                self.options.mainmenu.selectNextMenu();
            }
        }

        function selectSupermenu() {
            if (self.options.parent) {
                that.items[self.options.selected].trigger("mouseleave");
                self.options.parent.gainFocus();
            } else if (self.options.mainmenu) {
                Ox.print("previousMenu")
                self.options.mainmenu.selectPreviousMenu();
            }
        }

        self.onChange = function(key, value) {
            if (key == "items") {
                constructItems(value);
            } else if (key == "selected") {
                selectItem(value);
            }
        }

        that.addItemAfter = function(item) {

        };

        that.addItemBefore = function(item) {

        };

        that.getItem = function(id) {
            
        };

        that.hasEnabledItems = function() {
            var ret = false;
            $.each(that.items, function(i, item) {
                if (!item.options("disabled")) {
                    return ret = true;
                }
            });
            return ret;
        };

        that.hideMenu = function() {
            Ox.print("hideMenu")
            $.each(that.submenus, function(i, submenu) {
                if (submenu.is(":visible")) {
                    submenu.hideMenu();
                    return false;
                }
            });
            selectItem(-1);
            scrollMenuUp();
            that.$scrollbars.up.is(":visible") && that.$scrollbars.up.hide();
            that.$scrollbars.down.is(":visible") && that.$scrollbars.down.hide();
            //that.$scrollbars.down.hide();
            if (self.options.parent) {
                self.options.element.removeClass("OxSelected");
            }
            that.hide()
                .loseFocus()
                .unbindEvent({
                    key_up: selectPreviousItem,
                    key_down: selectNextItem,
                    key_left: selectSupermenu,
                    key_right: selectSubmenu,
                    key_escape: that.hideMenu,
                    key_enter: clickItem
                })
                .triggerEvent("hide");
            that.$layer.hide();
            $document.unbind("click", click);
            return that;
            //that.triggerEvent("hide");
        };

        that.removeItem = function() {

        };

        that.selectFirstItem = function() {
            selectNextItem();
        };

        that.showMenu = function() {
            Ox.print("showMenu");
            if (!self.options.parent && !that.$layer.parent().length) {
                that.$layer.appendTo($body);
            }
            that.parent().length || that.appendTo($body);
            that.css({
                left: "-1000px",
                top: "-1000px",
            }).show();
            var offset = self.options.element.offset(),
                width = self.options.element.outerWidth(),
                height = self.options.element.outerHeight(),
                left = offset.left + self.options.offset.left + (self.options.side == "bottom" ? 0 : width),
                top = offset.top + self.options.offset.top + (self.options.side == "bottom" ? height : 0),
                menuHeight = that.$content.outerHeight(); // fixme: why is outerHeight 0 when hidden?
                menuMaxHeight = Math.floor($window.height() - top - 16),
            Ox.print("menuHeight", menuHeight, "menuMaxHeight", menuMaxHeight, that.items.length);
            if (self.options.parent) {
                if (menuHeight > menuMaxHeight) {
                    top = Ox.limit(top - menuHeight + menuMaxHeight, self.options.parent.offset().top, top); 
                    menuMaxHeight = Math.floor($window.height() - top - 16);
                }
            }
            that.css({
                left: left + "px",
                top: top + "px"
            });
            if (menuHeight > menuMaxHeight) {
                that.$container.height(menuMaxHeight - itemHeight - 8); // margin
                that.$scrollbars.down.show();
            } else {
                that.$container.height(menuHeight);
            }
            !self.options.parent && that.gainFocus();
            that.bindEvent({
                key_up: selectPreviousItem,
                key_down: selectNextItem,
                key_left: selectSupermenu,
                key_right: selectSubmenu,
                key_escape: that.hideMenu,
                key_enter: clickSelectedItem
            });
            setTimeout(function() {
                $document.bind("click", click);
            }, 100);
            return that;
            //that.triggerEvent("show");
        };

        that.toggleMenu = function() {
            Ox.print("toggleMenu")
            that.is(":hidden") ? that.showMenu() : that.hideMenu();
        };

        return that;

    };

    Ox.MenuItem = function(options, self) {

        var self = self || {},
            that = new Ox.Element("tr", self)
                .defaults({
                    bind: [],
                    checked: null,
                    disabled: false,
                    group: "",
                    icon: "",
                    id: "",
                    items: [],
                    keyboard: "",
                    menu: null, // fixme: is passing the menu to 100s of menu items really memory-neutral?
                    position: 0,
                    title: [],
                })
                .options($.extend(options, {
                    keyboard: parseKeyboard(options.keyboard || self.defaults.keyboard),
                    title: Ox.makeArray(options.title || self.defaults.title)
                }))
                .addClass("OxItem" + (self.options.disabled ? " OxDisabled" : ""))
                .attr({
                    id: Ox.toCamelCase(self.options.menu.options("id") + "/" + self.options.id)
                })
                .data("group", self.options.group); // fixme: why?

        // construct
        that.append(
                that.$status = $("<td>", {
                    "class": "OxCell OxStatus",
                    html: self.options.checked ? oxui.symbols.check : ""
                })
            )
            .append(
                that.$icon = $("<td>", {
                    "class": "OxCell OxIcon"
                })
                .append(self.options.icon ?
                    $("<img>", {
                        src: self.options.icon
                    }) : null
                )
            )
            .append(
                that.$title = $("<td>", {
                    "class": "OxCell OxTitle",
                    html: self.options.title[0]
                })
            )
            .append(
                $("<td>", {
                    "class": "OxCell OxModifiers",
                    html: $.map(self.options.keyboard.modifiers, function(modifier) {
                        return oxui.symbols[modifier];
                    }).join("")
                })
            )
            .append(
                $("<td>", {
                    "class": "OxCell Ox" + (self.options.items.length ? "Submenu" : "Key"),
                    html: self.options.items.length ? oxui.symbols.triangle_right :
                            oxui.symbols[self.options.keyboard.key] ||
                            self.options.keyboard.key.toUpperCase()
                })
            );            

        function parseKeyboard(str) {
            var modifiers = str.split(" "),
                key = modifiers.pop();
            return {
                modifiers: modifiers,
                key: key
            }
        }

        self.onChange = function(key, value) {
            Ox.print("MenuItem", self.options.id, "onChange", key, value);
            if (key == "checked") {
                if (value && self.options.group) {
                    $.each(self.options.menu.items, function(i, item) {
                        if (
                            item.options("id") != self.options.id &&
                            item.options("group") == self.options.group &&
                            item.options("checked")
                        ) {
                            item.options({
                                checked: false
                            });
                            return false;
                        }
                    });
                }
                that.$status.html(value ? oxui.symbols.check : "")
            } else if (key == "disabled") {
                that.toggleClass("disabled"); // fixme: this will only work if onChange is only invoked on actual change
            } else if (key == "title") {
                
            }
        }

        that.toggle = function() {
            // toggle id and title
        };

        that.toggleChecked = function() {

        };

        that.toggleDisabled = function() {

        };

        that.toggleTitle = function() {
            that.options({
                title: that.$title.html() == self.options.title[0] ?
                        self.options.title[1] : self.options.title[0]
            });
        };

        return that;

    };

    /*
    ============================================================================
    Panels
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.CollapsePanel
    ----------------------------------------------------------------------------
    */

    Ox.CollapsePanel = function(options, self) {
        var self = self || {},
            that = new Ox.Panel({}, self)
                .defaults({
                    collapsed: false,
                    size: 20,
                    title: ""
                })
                .options(options)
                .addClass("OxCollapsePanel"),
            value = self.options.collapsed ?
                ["expand", "collapsed"] : ["collapse", "expand"],
            $titlebar = new Ox.Bar({
                    orientation: "horizontal",
                    size: self.options.size,
                })
                .dblclick(dblclickTitlebar)
                .appendTo(that),
            $switch = new Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: value,
                })
                .click(toggleCollapsed)
                .appendTo($titlebar),
            $title = new Ox.Element()
                .addClass("OxTitle")
                .html(self.options.title/*.toUpperCase()*/)
                .appendTo($titlebar);
        that.$content = new Ox.Element()
            .addClass("OxContent")
            .appendTo(that);
        // fixme: doesn't work, content still empty
        // need to hide it if collapsed
        if (self.options.collapsed) {
            that.$content.css({
                marginTop: -that.$content.height() + "px"
            });
        }
        function dblclickTitlebar(e) {
            if (!$(e.target).hasClass("OxButton")) {
                toggleCollapsed();
            }
        }
        function toggleCollapsed() {
            that.options({
                collapsed: !self.options.collapsed
            });
            var top = self.options.collapsed ?
                -that.$content.height() : 0;
            that.$content.animate({
                marginTop: top + "px"
            }, 200);
        }
        self.onChange = function(option, value) {
            if (option == "collapsed") {
                $switch.options({
                    value: value ? "expand" : "collapse"
                });
            } else if (option == "title") {
                $title.html(self.options.title);
            }
        };
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Panel
    ----------------------------------------------------------------------------
    */

    Ox.Panel = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .addClass("OxPanel");
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.SplitPanel
    options:
        orientation: ""     "horizontal" or "vertical"
        elements: [{
            element,            Ox Element
            size: 0,            size in px
            resizable: false    resizable or not
        }]
        
    ----------------------------------------------------------------------------
    */

    Ox.SplitPanel = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    elements: [],
                    orientation: "horizontal"
                })
                .options(options || {})
                .addClass("OxSplitPanel"),
            length = self.options.elements.length,
            dimensions = oxui.getDimensions(self.options.orientation),
            edges = oxui.getEdges(self.options.orientation);
        $.each(self.options.elements, function(i, v) {
            Ox.print("v", v);
            var element = v.element
                .css({
                    position: "absolute" // fixme: this can go into a class
                })
                .css(edges[2], 0)
                .css(edges[3], 0);
            if (v.size != undefined) {
                element.css(dimensions[0], v.size + "px");
            }
            if (i == 0) {
                element.css(edges[0], 0);
                if (v.size == undefined) {
                    element.css(
                        edges[1],
                        (self.options.elements[1].size + (length == 3 ? self.options.elements[2].size : 0)) + "px"
                    );
                }
            } else if (i == 1) {
                if (self.options.elements[0].size != undefined) {
                    element.css(edges[0], self.options.elements[0].size + "px");
                }
                if (self.options.elements[0].size == undefined || v.size == undefined) {
                    element.css(
                        edges[1],
                        (length == 3 ? self.options.elements[2].size : 0) + "px"
                    );
                }
            } else {
                element.css(edges[1], 0);
                if (v.size == undefined) {
                    element.css(
                        edges[0],
                        (self.options.elements[0].size + self.options.elements[1].size) + "px"
                    );
                }
            }
            element.appendTo(that);
            //that.append(element)
        });
        return that;
    };

    Ox.TabPanel = function(options, self) {
        
    };

})();

/*
============================================================================
Requests
============================================================================
*/

/*
----------------------------------------------------------------------------
Ox.Progressbar
----------------------------------------------------------------------------
*/

/*
----------------------------------------------------------------------------
Ox.Spinner
----------------------------------------------------------------------------
*/

