/*
################################################################################
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", ""),
            stack: []
        },
        $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); 
            };

            return that;

        };

    }();

    /*
    ----------------------------------------------------------------------------
    Ox.Cache
    ----------------------------------------------------------------------------
    */

    // currently part of Ox.Request

    /*
    ----------------------------------------------------------------------------
    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 events = {};
        return {
            // make these bind, trigger, unbind
            publish: function(event, data) {
                console.log("publish", event, data);
                if (events[event]) {
                    $.each(events[event], function(i, v) {
                        setTimeout(function() {
                            v(data);                                
                        }, 0);
                    });
                }
            },
            subscribe: function(event, callback) {
                console.log("subscribe", event, callback);
                if (events[event]) {
                    events[event].push(callback);
                } else {
                    events[event] = [callback];
                }
            },
            unsubscribe: function(event, callback) {
                console.log("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 (index > -1) {
                    oxui.stack.splice(i, 1);
                }
                oxui.stack.push(id);
            },
            blur: function(id) {
                oxui.stack.pop();
            }
        };
    }();

    /*
    ----------------------------------------------------------------------------
    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"
            };
            
        return function() {
            document.keydown(keydown);
            function keydown(e) {
                var key = [],
                    ret = true,
                    time;
                $.each(modifierNames, function(k, v) {
                    if (e[k]) {
                        key.push(v);
                    }
                });
                // avoid pushing modifier twice
                if (keyNames[e.keyCode] && keys.indexOf(keyNames[e.keyCode]) == -1) {
                    key.push(keyNames[e.keyCode]);
                }
                key = key.join(" ");
                if (key.match(/^[\w\d-]$|SPACE/)) {
                    time = Ox.time();
                    if (time - bufferTime > bufferTimeout) {
                        buffer = "";
                    }
                    buffer += key == "SPACE" ? " " : key;
                    bufferTime = time;                    
                }
                $.each(stack, function(i, v) {
                    // fixme: we dont get the return value!
                    ret = Ox.event.publish(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]]
                }
            },

            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, callback) {
                    delete requests[options.id];
                    callback(data);
                }

                function error(request, status, error) {
                    var $dialog = new Ox.Dialog({
                                title: "Error: Remote request failed.",
                                buttons: [
                                    new Ox.Button({
                                        value: "Details",
                                        click: function() {
                                            var $iframe = $("<iframe>"),
                                            iframe = $iframe[0].contentWindow;
                                            iframe.document.open();
                                            iframe.document.write(xhr.responseText);
                                            iframe.document.close();
                                            $dialog.close();
                                            $dialog = new Ox.Dialog({
                                                    title: "Error: Remote request failed.",
                                                    buttons: [
                                                        new Ox.Button({
                                                            value: "Close",
                                                            click: function() {
                                                                $dialog.close();
                                                            }
                                                        })
                                                    ],
                                                    width: 800,
                                                    height: 400
                                                })
                                                .append($iframe)
                                                .open();
                                        }
                                    }),
                                    new Ox.Button({
                                        value: "Close",
                                        click: function() {
                                            $dialog.close();
                                        }
                                    })
                                ]
                            })
                            .append(request.status + " " + request.statusText)
                            .open();
                    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(error) {

                    }
                    cache[req] = {
                        data: data,
                        time: Ox.getTime()
                    };
                    callback(data, options.callback);
                }

                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(options.id, cache[req].data, callback);
                        }, 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, v) {
                    that[v] = function() {
                        var args = arguments,
                            length = args.length,
                            id, ret;
                        $.each(args, function(i, v) {
                            // if an ox object was passed
                            // then pass its $element instead
                            // so we can do oxObj.jqFn(oxObj)
                            if (v.ox) {
                                args[i] = v.$element;
                            }
                        });
                        // why does this not work?
                        // ret = that.$element[v].apply(this, arguments);
                        if (length == 0) {
                            ret = that.$element[v]();
                        } else if (length == 1) {
                            ret = that.$element[v](args[0]);
                        } else if (length == 2) {
                            ret = that.$element[v](args[0], args[1]);
                        } else if (length == 3) {
                            ret = that.$element[v](args[0], args[1], args[2]);
                        } else if (length == 4) {
                            ret = that.$element[v](args[0], args[1], args[2], args[3]);
                        }
                        // 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(option, value)
                // is called when an option changes
                // (to be implemented by widget)
            };

            // public
            that.defaults = function(defaults) {
                /*
                    that.defaults({foo: x})         sets self.defaults
                */
                self.defaults = defaults;
                return that;
            }
            that.gain = function() {
                Ox.Focus.gain(that.id);
            }
            that.lose = function() {
                Ox.Focus.lose(that.id);
            }
            that.options = function() {
                /*
                    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, 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
                    self.options = $.extend(
                        self.options || self.defaults, args);
                    $.each(args, function(k, v) {
                        self.onChange(k, v);
                    });
                    ret = that;                            
                }
                return ret;
            }
            that.publish = function() {
                arguments[0] = arguments[0] + "." + that.id;
                Ox.Event.publish.apply(that, arguments);
                // or, maybe better:
                $(".OxWidget").trigger.apply(that, arguments);
                return that;
            }
            that.remove = function() {
                that.$element.remove();
                delete elements[that.ox];
            }
            that.subscribe = function() {
                Ox.Event.subscribe.apply(that, arguments);
                // or
                that.$element.bind.apply(that, arguments);
                return that;
            }
            that.unsubscribe = function() {
                Ox.Event.unsubscribe.apply(that, arguments);
                return that;
            }

            // return
            return that;

        }

    }();

    Ox._Element = function(element) {
        var that = this;
        that.def = {};
        that.opt = {};
        that.ox = Ox.version;
        that.id = Ox.uid();
        //console.log("that.id", that.id)
        that.$element = $("<" + (element || "div") + "/>")
            //.addClass("OxElement")
            .data("ox", that.id);
        oxui.elements[that.id] = that;
        // console.log("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) {
                //console.log("getting all options", options);
                ret = that.opt;
            } else if (length == 1 && typeof arguments[0] == "string") {
                //console.log("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);
                    //console.log("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()
                //console.log("v", v, "arguments", arguments)
                if (ret.jquery) {
                    //console.log("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 || {}),
            dimensions = oxui.getDimensions(self.options.orientation);
        that.addClass("OxBar Ox" + Ox.toTitleCase(self.options.orientation))
            .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,
                    values: []
                })
                .options(options || {})
                .addClass("OxTabbar");

        (function() {
            Ox.ButtonGroup({
                selectable: true,
                selected: self.options.selected,
                size: "small",
                style: "tab",
                values: self.options.values
            }).appendTo(that);
        })();

        return that;

    };

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

    /*
    ============================================================================
    Dialog
    ============================================================================
    */

    Ox.Dialog = function(options, self) {
        var self = self || {},
            options = $.extend({
                title: "",
                buttons: [],
                width: 256,
                height: 128
            }, options),
            that = new Ox.Element()
                .addClass("OxDialog")
                .css({
                    left: (($(document).width() - options.width) / 2) + "px",
                    top: (($(document).height() - options.height - 48) / 2) + "px",
                    width: options.width + "px",
                    height: (options.height + 48) + "px"
                });
        that.$titlebar = new Ox.Element()
            .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) {
                    var left = Ox.constrain(offset.left - x + e.clientX, 24 - options.width, documentWidth - 24),
                        top = Ox.constrain(offset.top - y + e.clientY, -24 - options.height, documentHeight - 24);
                    that.css({
                        left: left + "px",
                        top: top + "px"
                    });
                });
                $(window).one("mouseup", function() {
                    $(window).unbind("mousemove");
                });
            })
            .appendTo(that);
        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, v) {
            v.appendTo(that.$buttonsbar);
        });
        options.buttons[0].focus();
        that.$layer = $(".OxLayer"); // fixme: lazy loading of layer is fine, but save in var, dont look up
        that.append = function($element) {
            that.$content.append($element);
            return that;
        }
        that.close = function() {
            that.animate({
                opacity: 0
            }, 200, function() {
                that.remove();
                that.$layer.remove();
            })
        }
        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.Button
    ----------------------------------------------------------------------------
    */

    Ox.Button = function(options, self) {
        var self = self || {},
            that = new Ox.Element("input", self)
                .defaults({
                    buttonId: null,
                    disabled: false,
                    groupId: null,
                    selectable: false,
                    selected: false,
                    size: "small",
                    style: "", // can be symbol or tab
                    type: "text",
                    value: "",
                    values: []
                })
                .options($.extend(options, {
                    value: $.isArray(options.value) ?
                        options.value[0] : options.value,
                    values: $.makeArray(options.value)
                }));
        that.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);
        //console.log(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 && !(self.options.groupId !== null && self.options.selected)) {
                that.toggleSelected();
            }
            if (self.options.values.length == 2) {
                console.log("2 values")
                that.options({
                    value: self.options.value == self.options.values[0] ?
                        self.options.values[1] : self.options.values[0]
                }); 
            }
        }
        self.onChange = function(option, value) {
            //console.log("setOption", option, value)
            if (option == "selected") {
                if (value != that.hasClass("OxSelected")) {
                    that.toggleClass("OxSelected");
                }
            }
            if (option == "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.toggleSelected = function() {
            that.options({
                selected: !self.options.selected
            });
            that.trigger("OxButtonToggle", self.options);
        }
        that.options("value", self.options.value);
        return that;
    };

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

    Ox.ButtonGroup = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    groupId: Ox.uid(),
                    selectable: false,
                    selected: -1,
                    size: "small",
                    style: "",
                    type: "text",
                    values: []
                })
                .options(options || {})
                .addClass("OxButtonGroup");
        (function() {
            self.$buttons = [];
            $.each(self.options.values, function(i, v) {
                self.$buttons[i] = Ox.Button({
                    buttonId: i,
                    groupId: self.options.groupId,
                    selectable: self.options.selectable,
                    selected: i == self.options.selected,
                    size: self.options.size,
                    style: self.options.style,
                    type: self.options.type,
                    value: v
                }).appendTo(that);
            });
            that.$element.bind("OxButtonToggle", function(e, data) {
                console.log("Data", data, self.options)
                if (data.groupId = self.options.groupId) {
                    if (data.selected) {
                        if (self.options.selected > -1) {
                            self.$buttons[self.options.selected].toggleSelected();
                        }
                        self.options.selected = data.buttonId;
                    }
                }
            });
        })();
        return that;
    };

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

    Ox.Input = function(options, self) {
        var self = self || {},
            that = new Ox.Element("input", self)
                .defaults({
                    placeholder: "",
                    size: "small",
                    type: "text"
                })
                .options(options || {});
        that.attr({
                type: self.options.type,
                placeholder: self.options.placeholder
            })
            .addClass("OxInput Ox" +
                    Ox.toTitleCase(self.options.size) + " OxPlaceholder")
            //.change(change)
            .focus(focus)
            .blur(blur);
        /* doesn't work yet
        function change() {
            console.log("change", that.val(), that.hasClass("OxPlaceholder"))
            if ((that.val() !== "") != that.hasClass("OxPlaceholder")) {
                that.toggleClass("OxPlaceholder");
            }
        }
        */
        function focus() {
            console.log("focus", that.val(), that.attr("class"))
            if (that.hasClass("OxPlaceholder")) {
                that.val("").removeClass("OxPlaceholder");
            }
        }
        function blur() {
            console.log("blur", that.val(), that.attr("class"))
            if (that.val() === "") {
                that.addClass("OxPlaceholder").val(that.attr("placeholder"));
            }
        }
        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) {
                //console.log(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);
                //console.log("triggering OxRange" + that.id + "Change")
                that.publish("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;

    };

    /*
    ============================================================================
    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({
                    size: "small",
                    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) {
            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;
    };

})();

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

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

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

