// todo: check http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/

var Ox = {
    version: '0.1.2'
};

/*
================================================================================
Constants
================================================================================
*/

Ox.AMPM = ['AM', 'PM'];
//Ox.DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
Ox.DURATIONS = ['year', 'month', 'day', 'minute', 'second'];
Ox.EARTH_RADIUS = 6378137;
Ox.EARTH_CIRCUMFERENCE = Ox.EARTH_RADIUS * 2 * Math.PI;
Ox.HTML_ENTITIES = {
    '"': '&quot;', '&': '&amp;', "'": '&apos;', '<': '&lt;', '>': '&gt;'
};
Ox.KEYS = {
    SECTION: 0, BACKSPACE: 8, TAB: 9, CLEAR: 12, ENTER: 13,
    SHIFT: 16, CONTROL: 17, OPTION: 18, PAUSE: 19, CAPSLOCK: 20,
    ESCAPE: 27, SPACE: 32, PAGEUP: 33, PAGEDOWN: 34, END: 35, HOME: 36,
    LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, INSERT: 45, DELETE: 46, HELP: 47,
    0: 48, 1: 49, 2: 50, 3: 51, 4: 52, 5: 53, 6: 54, 7: 55, 8: 56, 9: 57,
    A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74,
    K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84,
    U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90,
    META_LEFT: 91, META_RIGHT: 92, SELECT: 93,
    '0_NUMPAD': 96, '1_NUMPAD': 97, '2_NUMPAD': 98, '3_NUMPAD': 99,
    '4_NUMPAD': 100, '5_NUMPAD': 101, '6_NUMPAD': 102, '7_NUMPAD': 103,
    '8_NUMPAD': 104, '9_NUMPAD': 105, '*_NUMPAD': 106, '+_NUMPAD': 107,
    '\n_NUMPAD': 108, '-_NUMPAD': 109, '._NUMPAD': 110, '/_NUMPAD': 111,
    F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118,
    F8: 110, F9: 120, F10: 121, F11: 122, F12: 123, F13: 124, F14: 125,
    F15: 126, F16: 127, NUMLOCK: 144, SCROLLLOCK: 145,
    ';': 186, '=': 187, ',': 188, '-': 189, '.': 190, '/': 191, '`': 192,
    '(': 219, '\\': 220, ')': 221, '\'': 222 
};
//Ox.MAX_LATITUDE = Ox.deg(Math.atan(Ox.sinh(Math.PI)));
//Ox.MIN_LATITUDE = -Ox.MAX_LATITUDE;
Ox.MONTHS = [
    'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December'
];
Ox.PREFIXES = ['K', 'M', 'G', 'T', 'P'];
Ox.SYMBOLS = {
    DOLLAR: '\u0024', 
    CENT: '\u00A2', POUND: '\u00A3', CURRENCY: '\u00A4', YEN: '\u00A5',
    BULLET: '\u2022', ELLIPSIS: '\u2026', PERMILLE: '\u2030',
    COLON: '\u20A1', CRUZEIRO: '\u20A2', FRANC: '\u20A3', LIRA: '\u20A4',
    NAIRA: '\u20A6',  PESETA: '\u20A7', WON: '\u20A9', SHEQEL: '\u20AA',
    DONG: '\u20AB', EURO: '\u20AC', KIP: '\u20AD', TUGRIK: '\u20AE',
    DRACHMA: '\u20AF', PESO: '\u20B1', GUARANI: '\u20B2', AUSTRAL: '\u20B3',
    HRYVNIA: '\u20B4', CEDI: '\u20B5', TENGE: '\u20B8', RUPEE: '\u20B9',
    CELSIUS: '\u2103', FAHRENHEIT: '\u2109', POUNDS: '\u2114', OUNCE: '\u2125',
    OHM: '\u2126', KELVIN: '\u212A', ANGSTROM: '\u212B', INFO: '\u2139',
    LEFT: '\u2190', UP: '\u2191', RIGHT: '\u2192', DOWN: '\u2193',
    HOME: '\u2196', END: '\u2198', RETURN: '\u21A9',
    REDO: '\u21BA', UNDO: '\u21BB', PAGEUP: '\u21DE', PAGEDOWN: '\u21DF',
    CAPSLOCK: '\u21EA', TAB: '\u21E5', SHIFT: '\u21E7', INFINITY: '\u221E',
    CONTROL: '\u2303', COMMAND: '\u2318', ENTER: '\u2324', ALT: '\u2325',
    DELETE: '\u2326', CLEAR:'\u2327',BACKSPACE: '\u232B', OPTION: '\u2387',
    NAVIGATE: '\u2388', ESCAPE: '\u238B', EJECT: '\u23CF',
    SPACE: '\u2423', DIAMOND: '\u25C6',
    STAR: '\u2605', SOUND: '\u266B', TRASH: '\u267A', FLAG: '\u2691',
    ANCHOR: '\u2693', GEAR: '\u2699', ATOM: '\u269B', WARNING: '\u26A0',
    CUT: '\u2702', BACKUP: '\u2707', FLY: '\u2708', CHECK: '\u2713',
    CLOSE: '\u2715', BALLOT: '\u2717', WINDOWS: '\u2756',
    EDIT: '\uF802', CLICK: '\uF803', APPLE: '\uF8FF'
};
Ox.TYPES = [
    'Arguments', 'Array', 'Boolean', 'Date', 'Element', 'Function', 'Infinity',
    'NaN', 'Null', 'Number', 'Object', 'RegExp', 'String', 'Undefined'
];
Ox.WEEKDAYS = [
    'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
];

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

Ox.getset = function(obj, args, callback, context) {
    /***
    Generic getter and setter function

    can be implemented like this:

    that.options = function() {
        return Ox.getset(options, arguments, setOption(key, val), that);
    }

    Ox.getset(obj, [])                                      returns obj
    Ox.getset(obj, [key])                                   returns obj.key
    Ox.getset(obj, [key, val], callback, context)
    Ox.getset(obj, [{key: val, ...}], callback, context)    sets obj.key to val,
                                                            calls callback(key, val)
                                                            for any changed value,
                                                            returns context
                                                            (for chaining)

    >>> o = new function() { var o = {}, s = function() {}, t = this; t.o = function() { return Ox['getset'](o, arguments, s, t); }; return t; }
    true
    >>> Ox.getset({}, []) && o.o('key', 'val').o('key')
    'val'
    >>> Ox.getset({}, []) && o.o({key: 'val', foo: 'bar'}).o().foo
    'bar'
    >>> Ox.getset({}, []) && typeof o.o({foo: undefined}).o('foo') == 'undefined'
    true
    >>> delete o
    true
    ***/
    var obj_ = Ox.clone(obj), ret;
    if (args.length == 0) {
        // getset([])
        ret = obj;
    } else if (args.length == 1 && !Ox.isObject(args[0])) {
        // getset([key])
        ret = obj[args[0]]
    } else {
        // getset([key, val]) or getset([{key: val, ...}])
        args = Ox.makeObject(Ox.isObject(args[0]) ? args[0] : args);
        obj = Ox.extend(obj, args);
        Ox.forEach(args, function(val, key) {
            if (!obj_ || !Ox.isEqual(obj_[key], val)) {
                callback && callback(key, val);
            }
        });
        ret = context;                            
    }
    return ret;
}

Ox.print = function() {
    /*
    */
    if (window.console) {
        var args = Ox.makeArray(arguments),
            date = new Date;
        args.unshift(Ox.formatDate(date, '%H:%M:%S') + '.' +
            (Ox.pad(+date % 1000, 3)));
        window.console.log.apply(window.console, args);
    }
}

Ox.uid = (function() {
    /***
    returns a unique id
    >>> Ox.uid() != Ox.uid()
    true
    ***/
    var uid = 0;
    return function() {
        return uid++;
    };
}());

Ox.user = function() {
    // fixme: move to ox.ui
    $.get("http://www.maxmind.com/app/locate_my_ip", function(data) {
        var arr = data.split("tblProduct1"),
            re = />(.+?)<\/td>\n<td class=output align="center">\n(.*?)\n/,
            results = {};
        arr.shift();
        Ox.forEach(arr, function(v) {
            var result = re(v);
            results[result[1].replace(/Your |\*/, "")] = result[2];
        });
        Ox.print(results)
    });
    return {
        document: {
            referrer: document.referrer
        },
        history: {
            length: history.length
        },
        navigator: navigator,
        innerHeight: innerHeight,
        innerWidth: innerWidth,
        screen: screen,
        outerHeight: outerHeight,
        outerWidth: outerWidth
    }
};

/*
================================================================================
Array and Object functions
================================================================================
*/

Ox.avg = function(obj) {
    /***
    returns the average of an array's values, or an object's properties
    >>> Ox.avg([-1, 0, 1])
    0
    >>> Ox.avg({a: 1, b: 2, c: 3})
    2
    ***/
    return Ox.sum(obj) / Ox.length(obj);
};

Ox.clone = function(obj) {
    /*
    >>> (function() { a = ['val']; b = Ox.clone(a); a[0] = null; return b[0]; }())
    'val'
    >>> (function() { a = {key: 'val'}; b = Ox.clone(a); a.key = null; return b.key; }())
    'val'
    */
    return Ox.isArray(obj) ? obj.slice() : Ox.extend({}, obj);
};

Ox.count = function(arr) {
    /*
    Ox.count(['foo', 'bar', 'foo']).foo
    2
    */
    var obj = {};
    arr.forEach(function(v) {
        obj[v] = (obj[v] || 0) + 1;
    });
    return obj;
};

Ox.each = function(obj, fn) {
    // fixme: deprecate!
    /*
    Ox.each() works for arrays, objects and strings,
    like $.each(), unlike [].forEach()
    >>> Ox.each([0, 1, 2], function(i, v) {})
    [0, 1, 2]
    >>> Ox.each({a: 1, b: 2, c: 3}, function(k, v) {}).a
    1
    >>> Ox.each('foo', function(i, v) {})
    'foo'
    */
    var i, isArray = Ox.isArray(obj);
    for (i in obj) {
        i = isArray ? parseInt(i) : i;
        // fixme: should be (v, k), like [].forEach()
        if (fn(i, obj[i]) === false) {
            break;
        }
    }
    return obj;
};

Ox.every = function(obj, fn) {
    /*
    Ox.every() works for arrays, objects and strings, unlike [].every()
    >>> Ox.every([0, 1, 2], function(v, i) { return i == v; })
    true
    >>> Ox.every({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
    false
    >>> Ox.every("foo", function(v) { return v == 'f'; })
    false
    >>> Ox.every([true, true, true])
    true
    */
    return Ox.filter(Ox.values(obj), fn || function(v) {
        return v;
    }).length == Ox.length(obj);
};

Ox.extend = function() {
    /*
    >>> Ox.extend({a: 1, b: 1, c: 1}, {b: 2, c: 2}, {c: 3}).c
    3
    */
    var obj = arguments[0];
    Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg, i) {
        Ox.forEach(arg, function(val, key) {
            obj[key] = val;
        });
    });
    return obj;
};

Ox.filter = function(arr, fn) {
    /*
    Ox.filter works for arrays and strings, like $.grep(), unlike [].filter()
    >>> Ox.filter([2, 1, 0], function(v, i) { return v == i; })
    [1]
    >>> Ox.filter('210', function(v, i) { return v == i; })
    ['1']
    */
    var i, len = arr.length, ret = [];
    for (i = 0; i < len; i++) {
        if (fn(arr[i], i)) {
            ret.push(arr[i]);
        }
    }
    return ret;
};

Ox.flatten = function(arr) {
    /*
    >>> Ox.flatten([1, [2, [3], 4], 5])
    [1, 2, 3, 4, 5]
    */
    var ret = [];
    arr.forEach(function(v) {
        if (Ox.isArray(v)) {
            Ox.flatten(v).forEach(function(v) {
                ret.push(v);
            });
        } else {
            ret.push(v);
        }
    });
    return ret;
}

Ox.find = function(arr, str) {
    /*
    returns an array with two arrays as elements:
    an array of elements of arr that begin with str,
    and an array of elements of arr that contain,
    but do not begin with str
    >>> Ox.find(["foo", "bar", "foobar", "barfoo"], "foo")
    [["foo", "foobar"], ["barfoo"]]
    */
    var arrLowerCase = arr.map(function(v) {
            return v.toLowerCase();
        }),
        ret = [[], []];
    str && arrLowerCase.forEach(function(v, i) {
        var index = v.indexOf(str.toLowerCase());
        index > -1 && ret[index == 0 ? 0 : 1].push(arr[i]);
    });
    return ret;
}

Ox.forEach = function(obj, fn) {
    /*
    Ox.forEach() works for arrays, objects and strings,
    like $.each(), unlike [].forEach()
    The arguments of the iterator function are (value, key),
    like [].forEach(), unlike $.each()
    >>> Ox.forEach([0, 1, 2], function(v, i) {})
    [0, 1, 2]
    >>> Ox.forEach({a: 1, b: 2, c: 3}, function(v, k) {}).a
    1
    >>> Ox.forEach('foo', function(v, i) {})
    'foo'
    */
    var key, isArray = Ox.isArray(obj);
    for (key in obj) {
        key = isArray ? parseInt(key) : key;
        if (fn(obj[key], key) === false) {
            break;
        }
    }
    return obj;
};

Ox.getObjectById = function(arr, id) {
    /***
    >>> Ox.getObjectById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo").title
    "Foo"
    ***/
    var ret = null;
    Ox.forEach(arr, function(v) {
        if (v.id == id) {
            ret = v;
            return false;
        }
    });
    return ret;
};

Ox.getPositionById = function(arr, id) {
    /***
    >>> Ox.getPositionById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "bar")
    1
    ***/
    var ret = -1;
    Ox.forEach(arr, function(v, i) {
        if (v.id == id) {
            ret = i;
            return false;
        }
    });
    return ret;
};

Ox.isEmpty = function(val) {
    return Ox.length(val) == 0;
};

Ox.isEqual = function(obj0, obj1) {
    /*
    >>> Ox.isEqual(false, false)
    true
    >>> Ox.isEqual(0, 0)
    true
    >>> Ox.isEqual(NaN, NaN)
    false
    >>> Ox.isEqual([1, 2, 3], [1, 2, 3])
    true
    >>> Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}})
    true
    >>> Ox.isEqual(function() { return; }, function() { return; });
    true
    */
    var ret = false;
    if (obj0 === obj1) {
        ret = true;
    } else if (typeof(obj0) == typeof(obj1)) {
        if (obj0 == obj1) {
            ret = true;
        } else if (Ox.isArray(obj0) && obj0.length == obj1.length) {
            Ox.forEach(obj0, function(v, i) {
                ret = Ox.isEqual(v, obj1[i]);
                return ret;
            });
        } else if (Ox.isDate(obj0)) {
            ret = obj0.getTime() == obj1.getTime();
        } else if (Ox.isObject(obj0)) {
            ret = Ox.isEqual(Ox.keys(obj0), Ox.keys(obj1)) &&
                Ox.isEqual(Ox.values(obj0), Ox.values(obj1));
        } else if (Ox.isFunction(obj0)) {
            ret = obj0.toString() == obj1.toString();
        }
    }
    return ret;
}

Ox.keys = function(obj) {
    /*
    >>> Ox.keys({a: 1, b: 2, c: 3})
    ["a", "b", "c"]
    */
    var keys = [];
    Ox.forEach(obj, function(v, k) {
        keys.push(k);
    });
    return keys;
};

Ox.length = function(obj) {
    /*
    >>> Ox.length([1, 2, 3])
    3
    >>> Ox.length({"a": 1, "b": 2, "c": 3})
    3
    */
    var length = 0;
    Ox.forEach(obj, function() {
        length++;
    });
    return length;
};

Ox.makeArray = function(arg) {
    /*
    like $.makeArray()
    >>> Ox.makeArray('foo', 'bar')
    ['foo', 'bar']
    >>> (function() { return Ox.makeArray(arguments); }('foo', 'bar'))
    ['foo', 'bar']
    */
    return Array.prototype.slice.call(
        Ox.isArguments(arg) ? arg : arguments
    );
};

Ox.makeObject = function() {
    /*
    >>> Ox.makeObject("foo", "bar").foo
    "bar"
    >>> Ox.makeObject(["foo", "bar"]).foo
    "bar"
    >>> Ox.makeObject({foo: "bar"}).foo
    "bar"
    >>> (function() { return Ox.makeObject(arguments); }("foo", "bar")).foo
    "bar"
    */
    var arg = arguments, obj = {};
    if (arg.length == 1) {
        if (Ox.isObject(arg[0])) {
            // ({foo: 'bar'})
            obj = arg[0];
        } else {
            // (['foo', 'bar'])
            obj[arg[0][0]] = arg[0][1];
        }
    } else {
        // ('foo', 'bar')
        obj[arg[0]] = arg[1];
    }
    return obj;
};

Ox.map = function(arr, fn) {
    /*
    Ox.map() works for arrays and strings, like $.map(), unlike [].map()
    >>> Ox.map([1, 1, 1], function(v, i) { return v == i; })
    [false, true, false]
    >>> Ox.map("111", function(v, i) { return v == i; })
    [false, true, false]
    >>> Ox.map(new Array(3), function(v, i) { return i; })
    [0, 1, 2]
    */
    var i, len = arr.length, val, ret = [];
    for (i = 0; i < len; i++) {
        if ((val = fn(arr[i], i)) !== null) {
            ret.push(val);
        }
    }
    return ret;
};

Ox.max = function(obj) {
    /*
    >>> Ox.max([-1, 0, 1])
    1
    >>> Ox.max({a: 1, b: 2, c: 3})
    3
    */
    return Math.max.apply(Math, Ox.values(obj));
};

Ox.min = function(obj) {
    /*
    >>> Ox.min([-1, 0, 1])
    -1
    >>> Ox.min({a: 1, b: 2, c: 3})
    1
    */
    return Math.min.apply(Math, Ox.values(obj));
};

Ox.merge = function(arr) {
    /*
    >>> Ox.merge(['foo'], ['bar'], ['baz'])
    ['foo', 'bar', 'baz']
    */
    Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg) {
        Ox.forEach(arg, function(val) {
            arr.push(val);
        });
    });
    return arr;
};

Ox.range = function(start, stop, step) {
    /*
    >>> Ox.range(3)
    [0, 1, 2]
    >>> Ox.range(3, 0)
    [3, 2, 1]
    >>> Ox.range(1, 2, 0.5)
    [1, 1.5]
    */
    stop = arguments.length > 1 ? stop : arguments[0];
    start = arguments.length > 1 ? start : 0;
    step = step || (start <= stop ? 1 : -1);
    var arr = [], i;
    for (i = start; step > 0 ? i < stop : i > stop; i += step) {
        arr.push(i);
    }
    return arr;
};

Ox.serialize = function(obj) {
    /*
    >>> Ox.serialize({a: 1, b: 2, c: 3})
    'a=1&b=2&c=3'
    */
    var arr = [];
    Ox.forEach(obj, function(val, key) {
        val !== '' && arr.push(key + '=' + val);
    });
    return arr.join('&');
};

Ox.setPropertyOnce = function(arr, str) {
    /*
    >>> Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected')
    0
    >>> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected')
    1
    >>> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected')
    0
    */
    var pos = -1;
    Ox.forEach(arr, function(v, i) {
        if (pos == -1 && arr[i][str]) {
            pos = i;
        } else if (pos > -1 && arr[i][str]) {
            delete arr[i][str];
        }
    });
    if (pos == -1) {
        arr[0][str] = true;
        pos = 0;
    }
    return pos;
}

Ox.shuffle = function(arr) {
    /*
    >>> Ox.shuffle([1, 2, 3]).length
    3
    */
    var shuffle = arr;
    return shuffle.sort(function() {
        return Math.random() - 0.5;
    });
};

Ox.some = function(obj, fn) {
    /*
    Ox.some() works for arrays, objects and strings, unlike [].some()
    >>> Ox.some([2, 1, 0], function(i, v) { return i == v; })
    true
    >>> Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
    true
    >>> Ox.some("foo", function(v) { return v == 'f'; })
    true
    */
    return Ox.filter(Ox.values(obj), fn).length > 0;
};

Ox.sum = function(obj) {
    /*
    >>> Ox.sum([-1, 0, 1])
    0
    >>> Ox.sum({a: 1, b: 2, c: 3})
    6
    */
    var sum = 0;
    Ox.forEach(obj, function(val) {
        sum += val;
    });
    return sum;
};

Ox.toArray = function(obj) {
    /*
    >>> Ox.toArray('foo')
    ['foo']
    >>> Ox.toArray(['foo'])
    ['foo']
    */
    var arr;
    if (Ox.isArray(obj)) {
        arr = obj;
    } else if (Ox.isArguments(obj)) {
        arr = Ox.makeArray(obj);
    } else {
        arr = [obj];
    }
    return arr;
};

Ox.unique = function(arr) {
    /*
    >>> Ox.unique([1, 2, 3, 1])
    [1, 2, 3]
    */
    var unique = [];
    Ox.forEach(arr, function(val) {
        unique.indexOf(val) == -1 && unique.push(val);
    });
    return unique;
};

Ox.unserialize = function(str) {
    /*
    >>> Ox.unserialize('a=1&b=2&c=3').c
    '3'
    */
    var arr, obj = {};
    Ox.forEach(str.split('&'), function(val) {
        arr = val.split('=');
        obj[arr[0]] = arr[1];
    });
    return obj;
};

Ox.values = function(obj) {
    /*
    >>> Ox.values({a: 1, b: 2, c: 3})
    [1, 2, 3]
    >>> Ox.values([1, 2, 3])
    [1, 2, 3]
    */
    var values = [];
    Ox.forEach(obj, function(val) {
        values.push(val);
    });
    return values;
};

Ox.zip = function() {
    /*
    >>> Ox.zip([[0, 1], [2, 3], [4, 5]])
    [[0, 2, 4], [1, 3, 5]]
    >>> Ox.zip([0, 1, 2], [3, 4, 5])
    [[0, 3], [1, 4], [2, 5]]
    */
    var args = arguments.length == 1 ? arguments[0] : Ox.makeArray(arguments),
        arr = [];
    args[0].forEach(function(v, i) {
        arr[i] = [];
        args.forEach(function(v_, i_) {
            arr[i].push(v_[i]);
        });
    });
    return arr;
};

/*
================================================================================
Color functions
================================================================================
*/

Ox.hsl = function(rgb) {
    /*
    >>> Ox.hsl([0, 0, 0])
    [0, 0, 0]
    >>> Ox.hsl([255, 255, 255])
    [0, 0, 1]
    >>> Ox.hsl([0, 255, 0])
    [120, 1, 0.5]
    */
    rgb = rgb.map(function(v) {
        return v / 255;
    });
    var max = Ox.max(rgb),
        min = Ox.min(rgb),
        hsl = [0, 0, 0];
    hsl[2] = 0.5 * (max + min);
    if (max == min) {
        hsl[0] = 0;
        hsl[1] = 0;
    } else {
        if (max == rgb[0]) {
            hsl[0] = (60 * (rgb[1] - rgb[2]) / (max - min) + 360) % 360;
        } else if (max == rgb[1]) {
            hsl[0] = 60 * (rgb[2] - rgb[0]) / (max - min) + 120;
        } else if (max == rgb[2]) {
            hsl[0] = 60 * (rgb[0] - rgb[1]) / (max - min) + 240;
        }
        if (hsl[2] <= 0.5) {
            hsl[1] = (max - min) / (2 * hsl[2]);
        } else {
            hsl[1] = (max - min) / (2 - 2 * hsl[2]);
        }
    }
    return hsl;
};

Ox.rgb = function(hsl) {
    /*
    >>> Ox.rgb([0, 0, 0])
    [0, 0, 0]
    >>> Ox.rgb([0, 0, 1])
    [255, 255, 255]
    >>> Ox.rgb([120, 1, 0.5])
    [0, 255, 0]
    */
    hsl[0] /= 360;
    var rgb = [0, 0, 0],
        v1, v2, v3;
    if (hsl[1] == 0) {
        rgb = [hsl[2], hsl[2], hsl[2]];
    } else {
        if (hsl[2] < 0.5) {
            v2 = hsl[2] * (1 + hsl[1]); 
        } else {
            v2 = hsl[1] + hsl[2] - (hsl[1] * hsl[2]);
        }
        v1 = 2 * hsl[2] - v2;
        rgb.forEach(function(v, i) {
            v3 = hsl[0] + (1 - i) * 1/3;
            if (v3 < 0) {
                v3++;
            } else if (v3 > 1) {
                v3--;
            }
            if (v3 < 1/6) {
                rgb[i] = v1 + ((v2 - v1) * 6 * v3);
            } else if (v3 < 0.5) {
                rgb[i] = v2;
            } else if (v3 < 2/3) {
                rgb[i] = v1 + ((v2 - v1) * 6 * (2/3 - v3));
            } else {
                rgb[i] = v1;
            }
        });
    }
    return rgb.map(function(v) {
        return v * 255;
    });
};

/*
================================================================================
Date functions
================================================================================
*/

Ox.getDateInWeek = function(date, weekday) {
    /*
    >>> Ox.formatDate(Ox.getDateInWeek(new Date("January 1 2000"), "Sunday"), "%A, %B %e, %Y")
    "Sunday, January  2, 2000"
    >>> Ox.formatDate(Ox.getDateInWeek(new Date("Jan 1 2000"), "Fri"), "%A, %B %e, %Y")
    "Friday, December 31, 1999"
    >>> Ox.formatDate(Ox.getDateInWeek(new Date("1/1/2000"), 1), "%A, %B %e, %Y")
    "Monday, December 27, 1999"
    */
    var date = date || new Date(),
        sourceWeekday = Ox.formatDate(date, "%u");
        targetWeekday = Ox.isNumber(weekday) ? weekday :
            Ox.map(Ox.WEEKDAYS, function(v, i) {
                return v.substr(0, 3) == weekday.substr(0, 3) ? i + 1 : null;
            })[0];
    date.setDate(date.getDate() - sourceWeekday + targetWeekday);
    return date;
}

Ox.getDayOfTheYear = function(date) {
    /*
    >>> Ox.getDayOfTheYear(new Date("12/31/2000"))
    366
    >>> Ox.getDayOfTheYear(new Date("12/31/2002"))
    365
    >>> Ox.getDayOfTheYear(new Date("12/31/2004"))
    366
    */
    return function(date) {
        date = date || new Date();
        var month = date.getMonth(),
            year = date.getFullYear();
        return Ox.sum(Ox.map(Ox.range(month), function(i) {
            return Ox.getDaysInMonth(year, i + 1);
        })) + date.getDate();
        /*
        var day = date.getDate(),
            month = date.getMonth();
            i;
        for (i = 0; i < month; i++) {
            day += Ox.DAYS[i];
        }
        if (month >= 2 && Ox.isLeapYear(date.getFullYear())) {
            day++;
        }
        return day;
        */
    };
}();

Ox.getDaysInMonth = function(year, month) {
    /*
    >>> Ox.getDaysInMonth(2000, 2)
    29
    >>> Ox.getDaysInMonth("2002", "Feb")
    28
    >>> Ox.getDaysInMonth("2004", "February")
    29
    */
    var year = parseInt(year),
        month = Ox.isNumber(month) ? month :
            Ox.map(Ox.MONTHS, function(v, i) {
                return v.substr(0, 3) == month.substr(0, 3) ? i + 1 : null;
            })[0];
    return new Date(year, month, 0).getDate()
    //return Ox.DAYS[month - 1] + (month == 2 && Ox.isLeapYear(year));
}

Ox.getFirstDayOfTheYear = function(date) {
    /*
    Decimal weekday of January 1 (0-6, Sunday as first day)
    >>> Ox.getFirstDayOfTheYear(new Date("01/01/00"))
    6
    */
    var date_ = date ? new Date(date.valueOf()) : new Date();
    date_.setMonth(0);
    date_.setDate(1);
    return date_.getDay();
};

Ox.getISODate = function(date) {
    /*
    >>> Ox.getISODate(new Date("01/01/2000"))
    "2000-01-01T00:00:00Z"
    */
    return Ox.formatDate(date || new Date(), '%FT%TZ');
};

Ox.getISODay = function(date) {
    /*
    Decimal weekday (1-7, Monday as first day)
    >>> Ox.getISODay(new Date("01/01/2000"))
    6
    >>> Ox.getISODay(new Date("01/02/2000"))
    7
    >>> Ox.getISODay(new Date("01/03/2000"))
    1
    */
    return (date || new Date()).getDay() || 7;
};

Ox.getISOWeek = function(date) {
    /*
    see http://en.wikipedia.org/wiki/ISO_8601
    >>> Ox.getISOWeek(new Date("01/01/2000"))
    52
    >>> Ox.getISOWeek(new Date("01/02/2000"))
    52
    >>> Ox.getISOWeek(new Date("01/03/2000"))
    1
    */
    date = date || new Date();
    var date_ = new Date(date.valueOf());
    // set date to Thursday of the same week
    date_.setDate(date.getDate() - Ox.getISODay(date) + 4);
    return Math.floor((Ox.getDayOfTheYear(date_) - 1) / 7) + 1;
};

Ox.getISOYear = function(date) {
    /*
    see http://en.wikipedia.org/wiki/ISO_8601
    >>> Ox.getISOYear(new Date("01/01/2000"))
    1999
    >>> Ox.getISOYear(new Date("01/02/2000"))
    1999
    >>> Ox.getISOYear(new Date("01/03/2000"))
    2000
    */
    date = date || new Date();
    var date_ = new Date(date.valueOf());
    // set date to Thursday of the same week
    date_.setDate(date.getDate() - Ox.getISODay(date) + 4);
    return date_.getFullYear();
};

Ox.getTime = function() {
    return +new Date();
}

Ox.getTimezoneOffsetString = function(date) {
    /*
    Time zone offset string ('-1200' - '+1200')
    >>> Ox.getTimezoneOffsetString(new Date('01/01/2000')).length
    5
    */
    var offset = (date || new Date()).getTimezoneOffset();
    return (offset < 0 ? '+' : '-') +
        Ox.pad(Math.floor(Math.abs(offset) / 60), 2) +
        Ox.pad(Math.abs(offset) % 60, 2);
};

Ox.getWeek = function(date) {
    /*
    Week of the year (0-53, Sunday as first day)
    >>> Ox.getWeek(new Date("01/01/2000"))
    0
    >>> Ox.getWeek(new Date("01/02/2000"))
    1
    >>> Ox.getWeek(new Date("01/03/2000"))
    1
    */
    date = date || new Date();
    return Math.floor((Ox.getDayOfTheYear(date) +
        Ox.getFirstDayOfTheYear(date) - 1) / 7);
};

Ox.isLeapYear = function(year) {
    /*
    >>> Ox.isLeapYear(1900)
    false
    >>> Ox.isLeapYear(2000)
    true
    >>> Ox.isLeapYear(2004)
    true
    */
    return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
};

/*
================================================================================
DOM functions
================================================================================
*/

Ox.canvas = function() {
    // Ox.canvas(img) or Ox.canvas(width, height)
    var c = {}, isImage = arguments.length == 1,
        image = isImage ? arguments[0] : {
            width: arguments[0], height: arguments[1]
        };
    c.context = (c.canvas = Ox.element('canvas').attr({
        width: image.width, height: image.height
    })[0]).getContext('2d');
    isImage && c.context.drawImage(image, 0, 0);
    c.data = (c.imageData = c.context.getImageData(
        0, 0, image.width, image.height
    )).data;
    return c;
};

Ox.element = function(str) {
    /*
    >>> Ox.element('div').attr({id: 'foo'}).attr('id')
    'foo'
    >>> Ox.element('div').html('foo').html()
    'foo'
    */
    return {
        0: str[0] == '#' ? document.getElementById(str.substr(1)) :
                document.createElement(str),
        attr: function() {
            var args, ret, that = this;
            if (arguments.length == 1 && Ox.isString(arguments[0])) {
                ret = this[0].getAttribute(arguments[0]);
            } else {
                Ox.forEach(Ox.makeObject.apply(this, arguments), function(v, k) {
                    that[0].setAttribute(k, v);
                });
                ret = this;
            }
            return ret;
        },
        html: function(str) {
            var ret;
            if (Ox.isUndefined(str)) {
                ret = this[0].innerHTML;
            } else {
                this[0].innerHTML = str;
                ret = this;
            }
            return ret;
        }
    }
};

/*
================================================================================
Encoding functions
================================================================================
*/

(function() {

    var aliases = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'},
        digits = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

    function cap(width, height) {
        // returns maximum encoding capacity of an image
        return parseInt(width * height * 3/8) - 4;
    }

    function seek(data, px) {
        // returns this, or the next, opaque pixel
        while (data[px * 4 + 3] < 255) {
            if (++px * 4 == data.length) {
                throwPNGError('de');
            }
        }
        return px;
    }

    function xor(byte) {
        // returns "1"-bits-in-byte % 2
        var xor = 0;
        Ox.range(8).forEach(function(i) {
            xor ^= byte >> i & 1;
        });
        return xor;
    }

    function throwPNGError(str) {
        throw new RangeError(
            'PNG codec can\'t ' +
            (str == 'en' ? 'encode data' : 'decode image')
        );
    }

    function throwUTF8Error(byte, pos) {
        throw new RangeError(
            'UTF-8 codec can\'t decode byte 0x' +
            byte.toString(16).toUpperCase() + ' at position ' + pos
        );
    }

    Ox.encodeBase32 = function(num) {
        // see http://www.crockford.com/wrmg/base32.html
        /*
        >>> Ox.encodeBase32(15360)
        'F00'
        >>> Ox.encodeBase32(33819)
        '110V'
        */
        return Ox.map(num.toString(32), function(char) {
            return digits[parseInt(char, 32)];
        }).join('');
    }

    Ox.decodeBase32 = function(str) {
        /*
        >>> Ox.decodeBase32('foo')
        15360
        >>> Ox.decodeBase32('ilou')
        33819
        >>> Ox.decodeBase32('?').toString()
        'NaN'
        */
        return parseInt(Ox.map(str.toUpperCase(), function(char) {
            var index = digits.indexOf(aliases[char] || char);
            return (index == -1 ? ' ' : index).toString(32);
        }).join(''), 32);
    }

    Ox.encodeBase64 = function(num) {
        /*
        >>> Ox.encodeBase64(32394)
        'foo'
        */
        return btoa(Ox.encodeBase256(num)).replace(/=/g, "");
    }

    Ox.decodeBase64 = function(str) {
        /*
        >>> Ox.decodeBase64('foo')
        32394
        */
        return Ox.decodeBase256(atob(str));
    }

    Ox.encodeBase128 = function(num) {
        /*
        >>> Ox.encodeBase128(1685487)
        'foo'
        */
        var str = '';
        while (num) {
            str = Ox.char(num & 127) + str;
            num >>= 7;
        }
        return str;
    }

    Ox.decodeBase128 = function(str) {
        /*
        >>> Ox.decodeBase128('foo')
        1685487
        */
        var num = 0, len = str.length;
        Ox.forEach(str, function(char, i) {
            num += char.charCodeAt(0) << (len - i - 1) * 7;
        });
        return num;
    }

    Ox.encodeBase256 = function(num) {
        /*
        >>> Ox.encodeBase256(6713199)
        'foo'
        */
        var str = '';
        while (num) {
            str = Ox.char(num & 255) + str;
            num >>= 8;
        }
        return str;
    }

    Ox.decodeBase256 = function(str) {
        /*
        >>> Ox.decodeBase256('foo')
        6713199
        */
        var num = 0, len = str.length;
        Ox.forEach(str, function(char, i) {
            num += char.charCodeAt(0) << (len - i - 1) * 8;
        });
        return num;
    }

    Ox.encodeDeflate = function(str) {
        // encodes string, using deflate
        /*
        in fact, the string is written to the rgb channels of a canvas element,
        then the dataURL is decoded from base64, and some head and tail cut off
        */
        str = Ox.encodeUTF8(str);
        var len = str.length, c = Ox.canvas(Math.ceil((4 + len) / 3), 1), data;
        str = Ox.pad(Ox.encodeBase256(len), 4, Ox.char(0)) + str +
                Ox.repeat('\u00FF', (4 - len % 4) % 4); // simpler? Ox.pad()?
        /* fixme: why does map not work here?
        c.data = $.map(c.data, function(v, i) {
            return i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255;
        });
        */
        for (i = 0; i < c.data.length; i += 1) {
            c.data[i] = i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255;
        }
        c.context.putImageData(c.imageData, 0, 0);
        Ox.print(c.canvas.toDataURL())
        data = atob(c.canvas.toDataURL().split(',')[1]);
        Ox.print('data', data);
        return data.substr(8, data.length - 20);
    }

    Ox.decodeDeflate = function(str) {
        var image = new Image();
        image.src = 'data:image/png;base64,' + btoa('\u0089PNG\r\n\u001A\n' +
                str + Ox.repeat('\u0000', 4) + 'IEND\u00AEB`\u0082');
        Ox.print(image.src);
        while (!image.width) {} // block until image data is available
        str = Ox.map(Ox.canvas(image).data, function(v, i) {
            return i % 4 < 3 ? Ox.char(v) : '';
        }).join('');
        return Ox.decodeUTF8(str.substr(4, Ox.decodeBase256(str.substr(0, 4))));
    }

    Ox.encodeHTML = function(str) {
        /*
        >>> Ox.encodeHTML('\'<"&">\'')
        '&apos;&lt;&quot;&amp;&quot;&gt;&apos;'
        >>> Ox.encodeHTML('äbçdê')
        '&#x00E4;b&#x00E7;d&#x00EA;'
        */
        return Ox.map(str, function(v) {
            var code = v.charCodeAt(0);
            return code < 128 ? (v in Ox.HTML_ENTITIES ? Ox.HTML_ENTITIES[v] : v) :
                '&#x' + Ox.pad(code.toString(16).toUpperCase(), 4) + ';';
        }).join('');
    };

    Ox.decodeHTML = function(str) {
        /*
        >>> Ox.decodeHTML('&apos;&lt;&quot;&amp;&quot;&gt;&apos;')
        '\'<"&">\''
        >>> Ox.decodeHTML('&#x0027;&#x003C;&#x0022;&#x0026;&#x0022;&#x003E;&#x0027;')
        '\'<"&">\''
        >>> Ox.decodeHTML('&auml;b&ccedil;d&ecirc;')
        'äbçdê'
        >>> Ox.decodeHTML('&#x00E4;b&#x00E7;d&#x00EA;')
        'äbçdê'
        */
        // relies on dom, but shorter than using this:
        // http://www.w3.org/TR/html5/named-character-references.html 
        return Ox.element('div').html(str)[0].childNodes[0].nodeValue;
        //return $('<div/>').html(str)[0].childNodes[0].nodeValue;
    };

    Ox.encodePNG = function(img, str) {
        // encodes string into image, returns new image url
        /*
        the message is compressed with deflate (by proxy of canvas),
        then the string (four bytes length) + (length bytes message)
        is encoded bitwise into the r/g/b bytes of all opaque pixels
        by flipping, if necessary, the least significant bit, so that
        (number of "1"-bits of the byte) % 2 is the bit of the string
        wishlist:
        - only use deflate if it actually shortens the message
        - in deflate, strip and later re-insert the chunk types
        - encode a decoy message into the least significant bit
          (and flip the second least significant bit, if at all)
        - write an extra png chunk containing some key
        */
        //str = Ox.encodeDeflate(str); currently broken
        str = Ox.encodeUTF8(str);
        var c = Ox.canvas(img), len = str.length, px = 0;
        if (len == 0 || len > cap(img.width, img.height)) {
            throwPNGError('en')
        }
        len = Ox.pad(Ox.encodeBase256(len), 4, Ox.char(0));
        Ox.forEach(Ox.map(len + str, function(byte) {
            return Ox.map(Ox.range(8), function(i) {
                return byte.charCodeAt(0) >> 7 - i & 1;
            }).join('');
        }).join(''), function(bit, i) {
            var index = parseInt((px = seek(c.data, px)) * 4 + i % 3),
                byte = c.data[index];
            c.data[index] = bit == xor(byte) ? byte :
                    byte & 254 | !(byte & 1);
            px += i % 3 == 2;
        });
        c.context.putImageData(c.imageData, 0, 0);
        return c.canvas.toDataURL();
    }

    Ox.decodePNG = function(img) {
        // decodes image, returns string
        var bits = '', data = Ox.canvas(img).data, flag = false, i = 0,
            len = 4, max = cap(img.width, img.height), px = 0, str = '';
        do {
            bits += xor(data[parseInt((px = seek(data, px)) * 4 + i % 3)]);
            px += i % 3 == 2;
            if (++i % 8 == 0) {
                str += Ox.char(parseInt(bits, 2));
                bits = '';
                len--;
                if (len == 0 && !flag) {
                    len = Ox.decodeBase256(str);
                    if (len <= 0 || len > max) {
                        Ox.print(len);
                        throwPNGError('de');
                    }
                    str = '';
                    flag = true;
                }
            }
        } while (len);
        try {
            //return Ox.decodeDeflate(str); currently broken
            return Ox.decodeUTF8(str);
        } catch(e) {
            Ox.print(e.toString());
            throwPNGError('de');
        }
    }

    Ox.encodeUTF8 = function(str) {
        /*
        see http://en.wikipedia.org/wiki/UTF-8
        >>> Ox.encodeUTF8('foo')
        'foo'
        >>> Ox.encodeUTF8('¥€$')
        '\u00C2\u00A5\u00E2\u0082\u00AC\u0024'
        */
        return Ox.map(Array.prototype.slice.call(str), function(chr) {
            var code = chr.charCodeAt(0),
                str = '';
            if (code < 128) {
                str = chr;
            } else if (code < 2048) {
                str = String.fromCharCode(code >> 6 | 192) +
                    String.fromCharCode(code & 63 | 128);
            } else {
                str = String.fromCharCode(code >> 12 | 224) +
                    String.fromCharCode(code >> 6 & 63 | 128) +
                    String.fromCharCode(code & 63 | 128);
            }
            return str;
        }).join('');
    }

    Ox.decodeUTF8 = function(str) {
        /*
        >>> Ox.decodeUTF8('foo')
        'foo'
        >>> Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024')
        '¥€$'
        */
        var bytes = Ox.map(str, function(v) {
                return v.charCodeAt(0);
            }),
            i = 0,
            len = str.length,
            str = '';
        while (i < len) {
            if (bytes[i] <= 128) {
                str += String.fromCharCode(bytes[i]);
                i++;
            } else if (
                bytes[i] >= 192 && bytes[i] < 240 &&
                i < len - (bytes[i] < 224 ? 1 : 2)
            ) {
                if (bytes[i + 1] >= 128 && bytes[i + 1] < 192) {
                    if (bytes[i] < 224) {
                        str += String.fromCharCode((bytes[i] & 31) << 6 |
                            bytes[i + 1] & 63);
                        i += 2;
                    } else if (bytes[i + 2] >= 128 && bytes[i + 2] < 192) {
                        str += String.fromCharCode((bytes[i] & 15) << 12 |
                            (bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63);
                        i += 3;
                    } else {
                        throwUTF8Error(bytes[i + 2], i + 2);
                    }
                } else {
                    throwUTF8Error(bytes[i + 1], i + 1);
                }
            } else {
                throwUTF8Error(bytes[i], i);
            }
        }
        return str;
    };

})();

/*
================================================================================
Format functions
================================================================================
*/

Ox.formatColor = function() {
    
};

Ox.formatCurrency = function(num, str, dec) {
    /*
    >>> Ox.formatCurrency(1000, '$', 2)
    '$1,000.00'
    */
    return str + Ox.formatNumber(num, dec);
};

Ox.formatDate = function() {
    /*
    See http://developer.apple.com/documentation/Darwin/Reference/ManPages/man3/strftime.3.html
    and http://en.wikipedia.org/wiki/ISO_8601
    >>> _date = new Date("01/02/05 00:03:04")
    "Sun Jan 02 2005 00:03:04 GMT+0100 (CET)"
    >>> Ox.formatDate(_date, "%A") // Full weekday
    "Sunday"
    >>> Ox.formatDate(_date, "%a") // Abbreviated weekday
    "Sun"
    >>> Ox.formatDate(_date, "%B") // Full month
    "January"
    >>> Ox.formatDate(_date, "%b") // Abbreviated month
    "Jan"
    >>> Ox.formatDate(_date, "%C") // Century
    "20"
    >>> Ox.formatDate(_date, "%c") // US time and date
    "01/02/05 12:03:04 AM"
    >>> Ox.formatDate(_date, "%D") // US date
    "01/02/05"
    >>> Ox.formatDate(_date, "%d") // Zero-padded day of the month
    "02"
    >>> Ox.formatDate(_date, "%e") // Space-padded day of the month
    " 2"
    >>> Ox.formatDate(_date, "%F") // Date
    "2005-01-02"
    >>> Ox.formatDate(_date, "%G") // Full ISO-8601 year
    "2004"
    >>> Ox.formatDate(_date, "%g") // Abbreviated ISO-8601 year
    "04"
    >>> Ox.formatDate(_date, "%H") // Zero-padded hour (24-hour clock)
    "00"
    >>> Ox.formatDate(_date, "%h") // Abbreviated month
    "Jan"
    >>> Ox.formatDate(_date, "%I") // Zero-padded hour (12-hour clock)
    "12"
    >>> Ox.formatDate(_date, "%j") // Zero-padded day of the year
    "002"
    >>> Ox.formatDate(_date, "%k") // Space-padded hour (24-hour clock)
    " 0"
    >>> Ox.formatDate(_date, "%l") // Space-padded hour (12-hour clock)
    "12"
    >>> Ox.formatDate(_date, "%M") // Zero-padded minute
    "03"
    >>> Ox.formatDate(_date, "%m") // Zero-padded month
    "01"
    >>> Ox.formatDate(_date, "%n") // Newline
    "\n"
    >>> Ox.formatDate(_date, "%p") // AM or PM
    "AM"
    >>> Ox.formatDate(_date, "%Q") // Quarter of the year
    "1"
    >>> Ox.formatDate(_date, "%R") // Zero-padded hour and minute
    "00:03"
    >>> Ox.formatDate(_date, "%r") // US time
    "12:03:04 AM"
    >>> Ox.formatDate(_date, "%S") // Zero-padded second
    "04"
    >/> Ox.formatDate(_date, "%s") // Number of seconds since the Epoch
    "1104620584"
    >>> Ox.formatDate(_date, "%T") // Time
    "00:03:04"
    >>> Ox.formatDate(_date, "%t") // Tab
    "\t"
    >>> Ox.formatDate(_date, "%U") // Zero-padded week of the year (00-53, Sunday as first day)
    "01"
    >>> Ox.formatDate(_date, "%u") // Decimal weekday (1-7, Monday as first day)
    "7"
    >>> Ox.formatDate(_date, "%V") // Zero-padded ISO-8601 week of the year
    "53"
    >>> Ox.formatDate(_date, "%v") // Formatted date
    " 2-Jan-2005"
    >>> Ox.formatDate(_date, "%W") // Zero-padded week of the year (00-53, Monday as first day)
    "00"
    >>> Ox.formatDate(_date, "%w") // Decimal weekday (0-6, Sunday as first day)
    "0"
    >>> Ox.formatDate(_date, "%X") // US time
    "12:03:04 AM"
    >>> Ox.formatDate(_date, "%x") // US date
    "01/02/05"
    >>> Ox.formatDate(_date, "%Y") // Full year
    "2005"
    >>> Ox.formatDate(_date, "%y") // Abbreviated year
    "05"
    >/> Ox.formatDate(_date, "%Z") // Time zone name
    "CET"
    >/> Ox.formatDate(_date, "%z") // Time zone offset
    "+0100"
    >/> Ox.formatDate(_date, "%+") // Formatted date and time
    "Sun Jan  2 00:03:04 CET 2005"
    >>> Ox.formatDate(_date, "%%")
    "%"
    >>> delete _date
    true
    >>> Ox.formatDate(new Date("01/01/2000"), "%W")
    "00"
    >>> Ox.formatDate(new Date("01/02/2000"), "%W")
    "00"
    >>> Ox.formatDate(new Date("01/03/2000"), "%W")
    "01"
    */
    var format = [
            ["%", function() {return "%{%}";}],
            ["c", function() {return "%x %X";}],
            ["X", function() {return "%r";}],
            ["x", function() {return "%D";}],
            ["D", function() {return "%m/%d/%y";}],
            ["F", function() {return "%Y-%m-%d";}],
            ["h", function() {return "%b";}],
            ["R", function() {return "%H:%M";}],
            ["r", function() {return "%I:%M:%S %p";}],
            ["T", function() {return "%H:%M:%S";}],
            ["v", function() {return "%e-%b-%Y";}],
            ["\\+", function() {return "%a %b %e %H:%M:%S %Z %Y";}],
            ["A", function(d) {return Ox.WEEKDAYS[(d.getDay() + 6) % 7];}],
            ["a", function(d) {return Ox.WEEKDAYS[(d.getDay() + 6) % 7].toString().substr(0, 3);}],
            ["B", function(d) {return Ox.MONTHS[d.getMonth()];}],
            ["b", function(d) {return Ox.MONTHS[d.getMonth()].toString().substr(0, 3);}],
            ["C", function(d) {return d.getFullYear().toString().substr(0, 2);}],
            ["d", function(d) {return Ox.pad(d.getDate(), 2);}],
            ["e", function(d) {return Ox.pad(d.getDate(), 2, " ");}],
            ["G", function(d) {return Ox.getISOYear(d);}],
            ["g", function(d) {return Ox.getISOYear(d).toString().substr(-2);}],
            ["H", function(d) {return Ox.pad(d.getHours(), 2);}],
            ["I", function(d) {return Ox.pad((d.getHours() + 11) % 12 + 1, 2);}],
            ["j", function(d) {return Ox.pad(Ox.getDayOfTheYear(d), 3);}],
            ["k", function(d) {return Ox.pad(d.getHours(), 2, " ");}],
            ["l", function(d) {return Ox.pad(((d.getHours() + 11) % 12 + 1), 2, " ");}],
            ["M", function(d) {return Ox.pad(d.getMinutes(), 2);}],
            ["m", function(d) {return Ox.pad((d.getMonth() + 1), 2);}],
            ["p", function(d) {return Ox.AMPM[Math.floor(d.getHours() / 12)];}],
            ["Q", function(d) {return Math.floor(d.getMonth() / 4) + 1;}],
            ["S", function(d) {return Ox.pad(d.getSeconds(), 2);}],
            ["s", function(d) {return Math.floor(d.getTime() / 1000);}],
            ["U", function(d) {return Ox.pad(Ox.getWeek(d), 2);}],
            ["u", function(d) {return Ox.getISODay(d);}],
            ["V", function(d) {return Ox.pad(Ox.getISOWeek(d), 2);}],
            ["W", function(d) {return Ox.pad(Math.floor((Ox.getDayOfTheYear(d) +
                (Ox.getFirstDayOfTheYear(d) || 7) - 2) / 7), 2);}],
            ["w", function(d) {return d.getDay();}],
            ["Y", function(d) {return d.getFullYear();}],
            ["y", function(d) {return d.getFullYear().toString().substr(-2);}],
            ["Z", function(d) {return d.toString().split("(")[1].replace(")", "");}],
            ["z", function(d) {return Ox.getTimezoneOffsetString(d);}],
            ["n", function() {return "\n";}],
            ["t", function() {return "\t";}],
            ["\\{%\\}", function() {return "%";}]
        ];
    format.forEach(function(v, i) {
        v[0] = new RegExp('%' + v[0] + '', 'g');
    });
    return function(date, str) {
        str = str || date;
        date = arguments.length == 2 ? date : new Date();
        var split;
        if (typeof date == 'string') {
            // support YYYY-MM-DD
            split = date.substr(0, 10).split('-');
            if (split.length == 3) {
                date = [split[1], split[2], split[0]].join('/') + date.substr(10);
            }
        }
        if (Ox.isNumber(date) || Ox.isString(date)) {
            date = new Date(date);
        }
        if (Ox.isDate(date) && date.toString() != 'Invalid Date') {
            Ox.forEach(format, function(v) {
                str = str.replace(v[0], v[1](date));
            });
        } else {
            str = '';
        }
        return str;
    };
}();

Ox.formatDuration = function(sec, dec, format) {
    /*
    >>> Ox.formatDuration(123456.789, 3)
    "1:10:17:36.789"
    >>> Ox.formatDuration(12345.6789)
    "03:25:46"
    >>> Ox.formatDuration(12345.6789, true)
    "0:03:25:46"
    >>> Ox.formatDuration(3599.999, 3)
    "00:59:59.999"
    >>> Ox.formatDuration(3599.999)
    "01:00:00"
    */
    var format = arguments.length == 3 ? format : (Ox.isString(dec) ? dec : "short"),
        dec = (arguments.length == 3 || Ox.isNumber(dec)) ? dec : 0,
        sec = dec ? sec : Math.round(sec),
        val = [
            Math.floor(sec / 31536000),
            Math.floor(sec % 31536000 / 86400),
            Math.floor(sec % 86400 / 3600),
            Math.floor(sec % 3600 / 60),
            format == "short" ? Ox.formatNumber(sec % 60, dec) : sec % 60
        ],
        str = {
            medium: ["y", "d", "h", "m", "s"],
            long: ["year", "day", "hour", "minute", "second"]
        },
        pad = [0, 3, 2, 2, dec ? dec + 3 : 2];
    while (!val[0] && val.length > (format == "short" ? 3 : 1)) {
        val.shift();
        str.medium.shift();
        str.long.shift();
        pad.shift();
    }
    while (format != "short" && !val[val.length - 1] && val.length > 1) {
        val.pop();
        str.medium.pop();
        str.long.pop();
    }
    return Ox.map(val, function(v, i) {
        return format == "short" ? Ox.pad(v, pad[i]) :
                v + (format == "long" ? " " : "") + str[format][i] +
                (format == "long" && v != 1 ? "s" : "");
    }).join(format == "short" ? ":" : " ");
};

Ox.formatNumber = function(num, dec) {
    /*
    >>> Ox.formatNumber(123456789, 3)
    "123,456,789.000"
    >>> Ox.formatNumber(-2000000 / 3, 3)
    "-666,666.667"
    >>> Ox.formatNumber(666666.666)
    "666,667"
    */
    var str = Math.abs(num).toFixed(dec || 0),
        spl = str.split('.'),
        arr = [];
    while (spl[0]) {
        arr.unshift(spl[0].substr(-3));
        spl[0] = spl[0].substr(0, spl[0].length - 3);
    }
    spl[0] = arr.join(',');
    return (num < 0 ? '-' : '') + spl.join('.');
};

Ox.formatPercent = function(num, total, dec) {
    /*
    >>> Ox.formatPercent(1, 1000, 2)
    "0.10%"
    */
    return Ox.formatNumber(num / total * 100, dec) + '%'
};

Ox.formatResolution = function(arr, str) {
    /*
    >>> Ox.formatResolution([1920, 1080], 'px')
    "1920 x 1080 px"
    */
    return arr[0] + ' x ' + arr[1] + (str ? ' ' + str : '');
}

Ox.formatString = function (str, obj) {
    /* 
    >>> Ox.formatString('{0}{1}', ['foo', 'bar'])
    "foobar"
    >>> Ox.formatString('{a}{b}', {a: 'foo', b: 'bar'})
    "foobar"
    */
    return str.replace(/\{([^}]+)\}/g, function(str, match) {
        return obj[match];
    });
}

Ox.formatValue = function(num, str, bin) {
    /*
    >>> Ox.formatValue(0, "B")
    "0 KB"
    >>> Ox.formatValue(123456789, "B")
    "123.5 MB"
    >>> Ox.formatValue(1234567890, "B", true)
    "1.15 GiB"
    */
    var base = bin ? 1024 : 1000,
        len = Ox.PREFIXES.length,
        val;
    Ox.forEach(Ox.PREFIXES, function(chr, i) {
        if (num < Math.pow(base, i + 2) || i == len - 1) {
            val = Ox.formatNumber(num / Math.pow(base, i + 1), i) +
                ' ' + chr + (bin ? 'i' : '') + str;
            return false;
        }
    });
    return val;
};

Ox.formatUnit = function(num, str) {
    return num + ' ' + str;
};

/*
================================================================================
Geo functions
================================================================================
*/

Ox.Line = function(point0, point1) {

    var self = {
            points: [point0, point1]
        },
        that = this;

    function rad() {
        return self.points.map(function(point) {
            return {
                lat: Ox.rad(point.lat()),
                lng: Ox.rad(point.lng())
            };
        });
    }

    that.getArea = function() {
        
    };

    that.getBearing = function() {
        var points = rad(),
            x = Math.cos(point[0].lat) * Math.sin(point[1].lat) -
                Math.sin(point[0].lat) * Math.cos(point[1].lat) *
                Math.cos(point[1].lng - point[0].lng),
            y = Math.sin(point[1].lng - point[0].lng) *
                Math.cos(point[1].lat);
        return (Ox.deg(math.atan2(y, x)) + 360) % 360;
    };

    that.getDistance = function() {
        var points = rad();
        return Math.acos(
    	    Math.sin(point[0].lat) * Math.sin(point[1].lat) + 
    	    Math.cos(point[0].lat) * Math.cos(point[1].lat) *
    	    Math.cos(point[1].lng - point[0].lng)
    	) * Ox.EARTH_RADIUS; 
    };

    that.getMidpoint = function() {
        var points = rad(),
            x = Math.cos(point[1].lat) *
                Math.cos(point[1].lng - point[0].lng),
            y = Math.cos(point[1].lat) *
                Math.sin(point[1].lng - point[0].lng),
            d = Math.sqrt(
                Math.pow(Math.cos(point[0].lat) + x, 2) + Math.pow(y, 2)
            ),
            lat = Ox.deg(
                Math.atan2(Math.sin(points[0].lat) + Math.sin(points[1].lat), d)
            ),
            lng = Ox.deg(
                points[0].lng + Math.atan2(y, math.cos(points[0].lat) + x)
            );
        return new Point(lat, lng);
    };

    that.points = function() {
        
    };

    return that;

};

Ox.Point = function(lat, lng) {

    var self = {lat: lat, lng: lng},
        that = this;

    that.lat = function() {
        
    };

    that.latlng = function() {
        
    };

    that.lng = function() {
        
    };

    that.getMetersPerDegree = function() {
        return Math.cos(self.lat * Math.PI / 180) *
            Ox.EARTH_CIRCUMFERENCE / 360;
    }

    that.getXY = function() {
        return [
            getXY(Ox.rad(self.lng)),
            getXY(Ox.asinh(Math.tan(Ox.rad(-self.lat))))
        ];
    };

    return that;

};

Ox.Rectangle = function(point0, point1) {

    var self = {
            points: [
                new Point(
                    Math.min(point0.lat(), point1.lat()),
                    point0.lng()
                ),
                new Point(
                    Math.max(point0.lat(), point1.lat()),
                    point1.lng()
                )
            ]
        },
        that = this;

    that.contains = function(rectangle) {
        
    }

    that.crossesDateline = function() {
        return self.points[0].lng > self.points[1].lng;
    }

    that.getCenter = function() {
        return new Ox.Line(self.points[0], self.points[1]).getMidpoint();
    };

    that.intersects = function(rectangle) {
        
    };

    return that;

};


/*
================================================================================
HTML functions
================================================================================
*/

Ox.parseEmailAddresses = function(html) {
    return html.replace(
        /\b([0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6})\b/gi,
        '<a href="mailto:$1" title="mailto:$1">$1</a>'
    );
};

Ox.parseHTML = (function() {
    /*
    >>> Ox.parseHTML('http://foo.com, bar')
    '<a href="http://foo.com" title="http://foo.com">foo.com</a>, bar'
    >>> Ox.parseHTML('(see: www.foo.com)')
    '(see: <a href="http://www.foo.com" title="http://www.foo.com">www.foo.com</a>)'
    >>> Ox.parseHTML('foo@bar.com')
    '<a href="mailto:foo@bar.com" title="mailto:foo@bar.com">foo@bar.com</a>'
    >>> Ox.parseHTML('<a href="http://foo.com" onmouseover="alert()">foo</a>')
    '<a href="http://foo.com" title="http://foo.com">foo</a>'
    >>> Ox.parseHTML('<a href="javascript:alert()">foo</a>')
    '&lt;a href=&quot;javascript:alert()&quot;&gt;foo&lt;/a&gt;'
    >>> Ox.parseHTML('[http://foo.com foo]')
    '<a href="http://foo.com" title="http://foo.com">foo</a>'
    >>> Ox.parseHTML('<rtl>foo</rtl>')
    '<div style="direction: rtl">foo</div>'
    >>> Ox.parseHTML('<script>alert()</script>')
    '&lt;script&gt;alert()&lt;/script&gt;'
    */
    var defaultTags = [
            'a', 'b', 'blockquote', 'cite', 'code', 'del',
            'em', 'i', 'img', 'ins', 'li', 'ol', 'q', 'rtl',
            's', 'strong', 'sub', 'sup', 'ul', '[]'
        ],
        parse = {
            a: {
                '<a [^<>]*?href="(https?:\/\/.+?)".*?>': '<a href="{1}" title="{1}">',
                '<\/a>': '</a>'
            },
            img: {
                '<img [^<>]*?src="(https?:\/\/.+?)".*?>': '<img src="{1}">'
            },
            rtl: {
                '<rtl>': '<div style="direction: rtl">',
                '<\/rtl>': '</div>'
            },
            '*': function(tag) {
                var ret = {};
                ret['<(/?' + tag + ')>'] = '<{1}>';
                return ret;
            }
        },
        tab = '\t';

    return function(html, tags, wikilinks) {
        var matches = [],
            tags = tags || defaultTags;
        html = Ox.clean(html);
        if (tags.indexOf('[]') > -1) {
            html = html.replace(/\[(https?:\/\/.+?) (.+?)\]/gi, '<a href="$1">$2</a>');
            tags = tags.filter(function(tag) {
                return tag != '[]';
            });
        }
        tags.forEach(function(tag) {
            var p = parse[tag] || parse['*'](tag);
            Ox.forEach(p, function(replace, regexp) {
                html = html.replace(new RegExp(regexp, 'gi'), function() {
                    matches.push(Ox.formatString(replace, arguments));
                    return tab + (matches.length - 1) + tab; 
                });
            });
        });
        html = Ox.encodeHTML(html);
        html = Ox.parseURLs(html);
        html = Ox.parseEmailAddresses(html);
        matches.forEach(function(match, i) {
            html = html.replace(new RegExp(tab + i + tab, 'gi'), match);
        });
        html = html.replace(/\n/g, '<br/>\n')
        // close extra opening (and remove extra closing) tags
        // return $('<div>').html(html).html();
        // fixme: this converts '&quot;' to '"' 
        return Ox.element('div').html(html).html();
    }

}());

Ox.parseURL = (function() {
    // fixme: leak memory, like now, or create every time? ... benchmark??
    var a = document.createElement('a'),
        keys = ['hash', 'host', 'hostname', 'origin',
            'pathname', 'port', 'protocol', 'search'];
    return function(str) {
        var ret = {};
        a.href = str;
        keys.forEach(function(key) {
            ret[key] = a[key];
        });
        return ret;
    };
}());

Ox.parseURLs = function(html) {
    return html.replace(
        /\b((https?:\/\/|www\.).+?)([\.,:;!\?\)\]]*?(\s|$))/gi,
        function(str, url, pre, end) {
            url = (pre == 'www.' ? 'http://' : '' ) + url;
            return Ox.formatString(
                '<a href="{url}" title="{url}">{host}</a>{end}',
                {
                    end: end,
                    host: Ox.parseURL(url).hostname,
                    url: url
                }
            );
        }
    );
};

/*
================================================================================
Math functions
================================================================================
*/

Ox.asinh = function(x) {
    /*
    fixme: no test
    */
    return Math.log(x + Math.sqrt(x * x + 1));
};

Ox.deg = function(rad) {
    /*
    >>> Ox.deg(2 * Math.PI)
    360
    */
    return rad * 180 / Math.PI;
};

Ox.divideInt = function(num, by) {
    /*
    >>> Ox.divideInt(100, 3)
    [33, 33, 34]
    >>> Ox.divideInt(100, 6)
    [16, 16, 17, 17, 17, 17]
    */
    var arr = [],
        div = parseInt(num / by),
        mod = num % by,
        i;
    for (i = 0; i < by; i++) {
        arr[i] = div + (i > by - 1 - mod);
    }
    return arr;
}

Ox.limit = function(num, min, max) {
    /*
    >>> Ox.limit(1, 2, 3)
    2
    >>> Ox.limit(2, 1)
    1
    */
    var len = arguments.length;
    max = arguments[len - 1];
    min = len == 3 ? min : 0;
    return Math.min(Math.max(num, min), max);
};

Ox.log = function(x, base) {
    /*
    >>> Ox.log(100, 10)
    2
    >>> Ox.log(Math.E)
    1
    */
    return Math.log(x) / Math.log(base || Math.E);
};

Ox.rad = function(deg) {
    /*
    >>> Ox.rad(360)
    2 * Math.PI
    */
    return deg * Math.PI / 180;
};

Ox.random = function() {
    /*
    >>> Ox.random(3) in {0: 0, 1: 0, 2: 0, 3: 0}
    true
    >>> Ox.random(1, 2) in {1: 0, 2: 0}
    true
    */
    var len = arguments.length,
        min = len == 1 ? 0 : arguments[0],
        max = arguments[len - 1];
    return min + parseInt(Math.random() * (max - min + 1));
};

Ox.round = function(num, dec) {
    /*
    >>> Ox.round(2 / 3, 6)
    0.666667
    >>> Ox.round(1 / 2, 3)
    0.5
    */
    var pow = Math.pow(10, dec || 0);
    return Math.round(num * pow) / pow;
};

Ox.sinh = function(x) {
    /*
    fixme: no test
    */
    return (Math.exp(x) - Math.exp(-x)) / 2;
};

/*
================================================================================
RegExp functions
================================================================================
*/

Ox.regexp = {
    'accents': '¨´`ˆ˜',
    'letters': 'a-z¨´`ˆ˜äåáàâãæçëéèèñïíìîöóòôõøœßúùûÿ'
};

/*
================================================================================
String functions
================================================================================
*/

Ox.basename = function(str) {
    /*
    fixme: this should go into Path functions
    >>> Ox.basename("foo/bar/foo.bar")
    "foo.bar"
    >>> Ox.basename("foo.bar")
    "foo.bar"
    */
    return str.replace(/^.*[\/\\]/g, '');
};

Ox.char = String.fromCharCode;

Ox.clean = function(str) {
    /*
    >>> Ox.clean("foo  bar")
    "foo bar"
    >>> Ox.clean(" foo  bar ")
    "foo bar"
    >>> Ox.clean(" foo \n bar ")
    "foo\nbar"
    */
    return Ox.map(str.split('\n'), function(str) {
        return Ox.trim(str.replace(/\s+/g, ' '));
    }).join('\n');
};

Ox.contains = function(str, chr) {
    /*
    >>> Ox.contains("foo", "bar")
    false
    >>> Ox.contains("foobar", "bar")
    true
    */
    return str.indexOf(chr) > -1;
};

Ox.endsWith = function(str, sub) {
    /*
    >>> Ox.endsWith("foobar", "bar")
    true
    */
    return str.substr(-sub.length) === sub;
};

Ox.highlight = function(txt, str) {
    // fixme: move to ox.ui
    return str ? txt.replace(
        new RegExp('(' + str + ')', 'ig'),
        '<span class="OxHighlight">$1</span>'
    ) : txt;
};

Ox.isValidEmail = function(str) {
    /*
    >>> Ox.isValidEmail("foo@bar.com")
    true
    >>> Ox.isValidEmail("foo.bar@foobar.co.uk")
    true
    >>> Ox.isValidEmail("foo@bar")
    false
    */
    return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i(str);
}

Ox.pad = function(str, len, pad, pos) {
    /*
    >>> Ox.pad(1, 2)
    "01"
    >>> Ox.pad("abc", 6, ".", "right")
    "abc..."
    >>> Ox.pad("foobar", 3, ".", "right")
    "foo"
    >>> Ox.pad("abc", 6, "123456", "right")
    "abc123"
    >>> Ox.pad("abc", 6, "123456", "left")
    "456abc"
    */
    str = str.toString().substr(0, len);
    pad = Ox.repeat(pad || "0", len - str.length);
    pos = pos || "left";
    str = pos == "left" ? pad + str : str + pad;
    str = pos == "left" ?
        str.substr(str.length - len, str.length) :
        str.substr(0, len);
    return str;
};

Ox.repeat = function(str, num) {
    /*
    >>> Ox.repeat(1, 3)
    "111"
    >>> Ox.repeat("foo", 3)
    "foofoofoo"
    */
    return num >= 1 ? new Array(num + 1).join(str.toString()) : '';
};

Ox.reverse = function(str) {
    /*
    Ox.reverse("foo")
    oof
    */
    return str.split('').reverse().join('');
};

Ox.startsWith = function(str, sub) {
    /*
    >>> Ox.startsWith("foobar", "foo")
    true
    */
    return str.substr(0, sub.length) === sub;
};

Ox.stripTags = function(str) {
    /*
    >>> Ox.stripTags("f<span>o</span>o")
    "foo"
    */
    return str.replace(/(<.*?>)/gi, '');
};

Ox.toCamelCase = function(str) {
    /*
    >>> Ox.toCamelCase("foo-bar-baz")
    "fooBarBaz"
    >>> Ox.toCamelCase("foo/bar/baz")
    "fooBarBaz"
    >>> Ox.toCamelCase("foo_bar_baz")
    "fooBarBaz"
    */
    return str.replace(/[\-_\/][a-z]/g, function(str) {
        return str[1].toUpperCase();
    });
};

Ox.toDashes = function(str) {
    /*
    >>> Ox.toDashes("fooBarBaz")
    "foo-bar-baz"
    */
    return str.replace(/[A-Z]/g, function(str) {
        return '-' + str.toLowerCase();
    });
};

Ox.toSlashes = function(str) {
    /*
    >>> Ox.toSlashes("fooBarBaz")
    "foo/bar/baz"
    */
    return str.replace(/[A-Z]/g, function(str) {
        return '/' + str.toLowerCase();
    });
};

Ox.toTitleCase = function(str) {
    /*
    >>> Ox.toTitleCase("foo")
    "Foo"
    >>> Ox.toTitleCase("Apple releases iPhone, IBM stock plummets")
    "Apple Releases iPhone, IBM Stock Plummets"
    */
    return Ox.map(str.split(' '), function(v) {
        var sub = v.substr(1),
            low = sub.toLowerCase();
        if (sub == low) {
            v = v.substr(0, 1).toUpperCase() + low;
        }
        return v;
    }).join(" ");
};

Ox.toUnderscores = function(str) {
    /*
    >>> Ox.toUnderscores("fooBarBaz")
    "foo_bar_baz"
    */
    return str.replace(/[A-Z]/g, function(str) {
        return '_' + str.toLowerCase();
    });
};

Ox.trim = function(str) { // is in jQuery
    /*
    Ox.trim(" foo ")
    "foo"
    */
    return str.replace(/^\s+|\s+$/g, "");
};

Ox.truncate = function(str, len, pad, pos) {
    /*
    >>> Ox.truncate("anticonstitutionellement", 16, "...", "center")
    "anticon...lement"
    */
    var pad = pad || {},
        pos = pos || "right",
        strlen = str.length,
        padlen = pad.length,
        left, right;
    if (strlen > len) {
        if (pos == "left") {
            str = pad + str.substr(padlen + strlen - len);
        } else if (pos == "center") {
            left = Math.ceil((len - padlen) / 2);
            right = Math.floor((len - padlen) / 2);
            str = str.substr(0, left) + pad + str.substr(-right);
        } else if (pos == "right") {
            str = str.substr(0, len - padlen) + pad;
        }
    }
    return str;
};

Ox.words = function(str) {
    /*
    >>> Ox.words('He\'s referring to the "ill-conceived" AOL/TimeWarner merger--didn\'t you know?')
    ['he\'s', 'referring', 'to' , 'the' , 'ill-conceived' , 'aol', 'timewarner' , 'merger' , 'didn\'t', 'you', 'know']
    */
    var arr = str.toLowerCase().split(/\b/),
        chr = "-'",
        len = arr.length,
        startsWithWord = !!/\w/(arr[0]);
    arr.forEach(function(v, i) {
        // find single occurrences of chars in chr
        // that are not at the beginning or end of str
        // and join the surrounding words with them
        if (
            i > 0 && i < len - 1 &&
            v.length == 1 && chr.indexOf(v) > -1
        ) {
            arr[i + 1] = arr[i - 1] + arr[i] + arr[i + 1];
            arr[i - 1] = arr[i] = '';
        }
    });
    // remove elements that have been emptied above
    arr = arr.filter(function(v) {
        return v.length;
    });
    // return words, not spaces or punctuation
    return arr.filter(function(v, i) {
        return i % 2 == !startsWithWord;
    });
}

Ox.wordwrap = function(str, len, sep, bal, spa) {
    /*
    >>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 25, "<br/>")
    "Anticonstitutionellement, <br/>Paris s'eveille"
    >>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 16, "<br/>")
    "Anticonstitution<br/>ellement, Paris <br/>s'eveille"
    >>> Ox.wordwrap("These are short words", 16, "<br/>", true)
    "These are <br/>short words"
    */
    var str = str === null ? '' : str.toString(),
        len = len || 80,
        sep = sep || "<br/>",
        bal = bal || false,
        spa = Ox.isUndefined(spa) ? true : spa,
        words = str.split(" "),
        lines;
    if (bal) {
        // balance lines: test if same number of lines
        // can be achieved with a shorter line length
        lines = Ox.wordwrap(str, len, sep, false).split(sep);
        if (lines.length > 1) {
            // test shorter line, unless
            // that means cutting a word
            var max = Ox.max(Ox.map(words, function(word) {
                return word.length;
            }));
            while (len > max) {
                len--;
                if (Ox.wordwrap(str, len, sep, false).split(sep).length > lines.length) {
                    len++;
                    break;
                }
            }
        }
    }
    lines = [""];
    Ox.forEach(words, function(word) {
        if ((lines[lines.length - 1] + word + " ").length <= len + 1) {
            // word fits in current line
            lines[lines.length - 1] += word + " ";
        } else {
            if (word.length <= len) {
                // word fits in next line
                lines.push(word + " ");
            } else {
                // word is longer than line
                var chr = len - lines[lines.length - 1].length;
                lines[lines.length - 1] += word.substr(0, chr);
                for (var pos = chr; pos < word.length; pos += len) {
                    lines.push(word.substr(pos, len));
                }
                lines[lines.length - 1] += " ";
            }
        }
    });
    if (!spa) {
        lines = Ox.map(lines, function(line) {
            return Ox.trim(line);
        });
    }
    return Ox.trim(lines.join(sep));    
};

/*
================================================================================
Type functions
================================================================================
*/

Ox.isArguments = function(val) {
    /*
    >>> Ox.isArguments((function() { return arguments; }()))
    true
    */
    return !!(val && val.toString() == '[object Arguments]');
}

Ox.isArray = function(val) { // is in jQuery
    /*
    >>> Ox.isArray([])
    true
    */
    return val instanceof Array;
}

Ox.isBoolean = function(val) {
    /*
    >>> Ox.isBoolean(false)
    true
    */
    return typeof val == 'boolean';
};

Ox.isDate = function(val) {
    /*
    >>> Ox.isDate(new Date())
    true
    */
    return val instanceof Date;
};

Ox.isElement = function(val) {
    /*
    >>> Ox.isElement(document.createElement())
    true
    */
    return !!(val && val.nodeType == 1);
};

Ox.isFunction = function(val) { // is in jQuery
    /*
    >>> Ox.isFunction(function() {})
    true
    >>> Ox.isFunction(/ /)
    false
    */
    return typeof val == 'function' && !Ox.isRegExp(val);
};

Ox.isInfinity = function(val) {
    /*
    >>> Ox.isInfinity(Infinity)
    true
    >>> Ox.isInfinity(-Infinity)
    true
    >>> Ox.isInfinity(NaN)
    false
    */
    return typeof val == 'number' && !isFinite(val) && !Ox.isNaN(val);
};

Ox.isNaN = function(val) {
    /*
    >>> Ox.isNaN(NaN)
    true
    */
    return val !== val;
}

Ox.isNull = function(val) {
    /*
    >>> Ox.isNull(null)
    true
    */
    return val === null;
};

Ox.isNumber = function(val) {
    /*
    >>> Ox.isNumber(0)
    true
    >>> Ox.isNumber(Infinity)
    false
    >>> Ox.isNumber(-Infinity)
    false
    >>> Ox.isNumber(NaN)
    false
    */
    return typeof val == 'number' && isFinite(val);
};

Ox.isObject = function(val) {
    /*
    >>> Ox.isObject({})
    true
    >>> Ox.isObject([])
    false
    >>> Ox.isObject(new Date())
    false
    >>> Ox.isObject(null)
    false
    */
    return typeof val == 'object' && !Ox.isArguments(val) &&
        !Ox.isArray(val) && !Ox.isDate(val) && !Ox.isNull(val);
};

Ox.isRegExp = function(val) {
    /*
    >>> Ox.isRegExp(/ /)
    true
    */
    return val instanceof RegExp;
};

Ox.isString = function(val) {
    /*
    >>> Ox.isString('')
    true
    */
    return typeof val == 'string';
};

Ox.isUndefined = function(val) {
    /*
    >>> Ox.isUndefined()
    true
    */
    return typeof val == 'undefined';
};

Ox.typeOf = function(val) {
    /*
    >>> (function() { return Ox.typeOf(arguments); }())
    'arguments'
    >>> Ox.typeOf([])
    'array'
    >>> Ox.typeOf(false)
    'boolean'
    >>> Ox.typeOf(new Date())
    'date'
    >>> Ox.typeOf(document.createElement())
    'element'
    >>> Ox.typeOf(function() {})
    'function'
    >>> Ox.typeOf(Infinity)
    'infinity'
    >>> Ox.typeOf(NaN)
    'nan'
    >>> Ox.typeOf(null)
    'null'
    >>> Ox.typeOf(0)
    'number'
    >>> Ox.typeOf({})
    'object'
    >>> Ox.typeOf(/ /)
    'regexp'
    >>> Ox.typeOf('')
    'string'
    >>> Ox.typeOf()
    'undefined'
    */
    var ret;
    Ox.forEach(Ox.TYPES, function(type) {
        if (Ox['is' + type](val)) {
            ret = type.toLowerCase();
            return false;
        }
    });
    return ret;
};