// vim: et:ts=4:sw=4:sts=4:ft=js
/*@
Ox.Input <f:Ox.Element> Input Element
    () ->              <f> Input Element
    (options) ->       <f> Input Element
    (options, self) -> <f> Input Element
    options <o> Options object
        arrows                      <b> if true, and type is 'float' or 'integer', display arrows
        arrowStep                   <n> step when clicking arrows
        autocomplete                <a> array of possible values, or
                                    <f> function(key, value, callback), returns one or more values
        autocompleteReplace         <b> if true, value is replaced
        autocompleteReplaceCorrect  <b> if true, only valid values can be entered
        autocompleteSelect          <b> if true, menu is displayed
        autocompleteSelectHighlight <b> if true, value in menu is highlighted
        autocompleteSelectSubmit    <b> if true, submit input on menu selection
        autocorrect                 <s|r|f|null> ('email', 'float', 'integer', 'phone', 'url'), or
                                    <r> regexp(value), or
                                    <f> function(key, value, blur, callback), returns value
        autovalidate                <f> --remote validation--
        clear                       <b> if true, has clear button
        changeOnKeypress            <b> if true, fire change event while typing
        disabled                    <b> if true, is disabled
        height                      <n> px (for type='textarea' and type='range' with orientation='horizontal')
        id                          <s> element id
        key                         <s> to be passed to autocomplete and autovalidate functions
        max                         <n> max value if type is 'integer' or 'float'
        min                         <n> min value if type is 'integer' or 'float'
        name                        <s> will be displayed by autovalidate function ('invalid ' + name)
        overlap                     <s> '', 'left' or 'right', will cause padding and negative margin
        picker                  <o> picker object
        rangeOptions          <o> range options
            arrows              <b>boolean, if true, display arrows
            //arrowStep         <n>  number, step when clicking arrows
            //arrowSymbols      <a> array of two strings
            max                 <n> number, maximum value
            min                 <n> number, minimum value
            orientation         <s> 'horizontal' or 'vertical'
            step                <n> number, step
            thumbValue          <b> boolean, if true, value is displayed on thumb, or
                                <a> array of strings per value, or
                                <f> function(value), returns string
            thumbSize           <n> integer, px
            trackGradient       <s> string, css gradient for track
            trackImage          <s> string, image url, or
                                <a> array of image urls
            //trackStep         <n>   number, 0 for 'scroll here', positive for step
            trackValues         <b> boolean
        serialize                   <f> function used to serialize value in submit
        textAlign                   <s> 'left', 'center' or 'right'
        type                        <s> 'float', 'integer', 'password', 'text', 'textarea'
        value                       <s> string
        validate                    <f> remote validation
        width                       <n> px

    change <!> input changed event
    submit <!> input submit event
@*/

Ox.Input = function(options, self) {

    var self = self || {},
        that = new Ox.Element({}, self)
            .defaults({
                arrows: false,
                arrowStep: 1,
                autocomplete: null,
                autocompleteReplace: false,
                autocompleteReplaceCorrect: false,
                autocompleteSelect: false,
                autocompleteSelectHighlight: false,
                autocompleteSelectSubmit: false,
                autovalidate: null,
                changeOnKeypress: false,
                clear: false,
				disabled: false,
                key: '',
                min: 0,
                max: 100,
                label: '',
                labelWidth: 64,
                overlap: 'none',
                placeholder: '',
                serialize: null,
                style: 'rounded',
                textAlign: 'left',
                type: 'text',
                validate: null,
                value: '',
                width: 128
            })
            .options(options)
            .addClass(
                'OxInput OxMedium Ox' + Ox.toTitleCase(self.options.style) /*+ (
                    self.options.overlap != 'none' ?
                    ' OxOverlap' + Ox.toTitleCase(self.options.overlap) : ''
                )*/
            )
            .bindEvent($.extend(self.options.type == 'textarea' ? {} : {
                key_enter: submit
            }, {
                key_control_v: paste,
                key_escape: cancel
            }));

    if (
        Ox.isArray(self.options.autocomplete) &&
        self.options.autocompleteReplace &&
        self.options.autocompleteReplaceCorrect &&
        self.options.value === ''
    ) {
        self.options.value = self.options.autocomplete[0]
    }

    // fixme: set to min, not 0
    if (self.options.type == 'float') {
        $.extend(self.options, {
            autovalidate: 'float',
            textAlign: 'right',
            value: self.options.value || '0.0'
        });
    } else if (self.options.type == 'integer') {
        $.extend(self.options, {
            autovalidate: 'integer',
            textAlign: 'right',
            value: self.options.value || '0'
        });
    }

    if (self.options.label) {
        self.$label = new Ox.Label({
            overlap: 'right',
            textAlign: 'right',
            title: self.options.label,
            width: self.options.labelWidth
        })
        .css({
            float: 'left', // fixme: use css rule
        })
        .click(function() {
            // fixme: ???
            // that.focus();
        })
        .appendTo(that);
    }

    if (self.options.arrows) {
        self.arrows = [];
        self.arrows[0] = [
            new Ox.Button({
                    overlap: 'right',
                    title: 'left',
                    type: 'image'
                })
                .css({
                    float: 'left'
                })
                .click(function() {
                    clickArrow(0);
                })
                .appendTo(that),
            new Ox.Button({
                    overlap: 'left',
                    title: 'right',
                    type: 'image'
                })
                .css({
                    float: 'right'
                })
                .click(function() {
                    clickArrow(1);
                })
                .appendTo(that)
        ]
    }

    $.extend(self, {
        bindKeyboard: self.options.autocomplete || self.options.autovalidate || self.options.changeOnKeypress,
        hasPasswordPlaceholder: self.options.type == 'password' && self.options.placeholder,
        inputWidth: getInputWidth()
    });

    if (self.options.clear) {
        self.$button = new Ox.Button({
                overlap: 'left',
                title: 'close',
                type: 'image'
            })
            .css({
                float: 'right' // fixme: use css rule
            })
            .click(clear)
            .appendTo(that);
    }

    self.$input = $(self.options.type == 'textarea' ? '<textarea>' : '<input>')
        .addClass('OxInput OxMedium Ox' + Ox.toTitleCase(self.options.style))
        .attr({
            disabled: self.options.disabled ? 'disabled' : '',
            type: self.options.type == 'password' ? 'password' : 'text'
        })
        .css($.extend({
            width: self.inputWidth + 'px',
            textAlign: self.options.textAlign
        }, self.options.type == 'textarea' ? {
            height: self.options.height + 'px',
        } : {}))
        .val(self.options.value)
        .blur(blur)
        .change(change)
        .focus(focus)
        .appendTo(that.$element);

    // fixme: is there a better way than this one?
    // should at least go into ox.ui.theme.foo.js
    // probably better: divs in the background
    if (self.options.type == 'textarea') {
        $.extend(self, {
            colors: Ox.Theme() == 'classic' ?
                [208, 232, 244] :
                //[0, 16, 32],
                [32, 48, 64],
            colorstops: [8 / self.options.height, self.options.height - 8 / self.options.height]
        });
        self.$input.css({
            background: '-moz-linear-gradient(top, rgb(' +
                [self.colors[0], self.colors[0], self.colors[0]].join(', ') + '), rgb(' +
                [self.colors[1], self.colors[1], self.colors[1]].join(', ') + ') ' + 
                Math.round(self.colorstops[0] * 100) + '%, rgb(' +
                [self.colors[1], self.colors[1], self.colors[1]].join(', ') + ') ' + 
                Math.round(self.colorstops[1] * 100) + '%, rgb(' +
                [self.colors[2], self.colors[2], self.colors[2]].join(', ') + '))' 
        });
        self.$input.css({
            background: '-webkit-linear-gradient(top, rgb(' +
                [self.colors[0], self.colors[0], self.colors[0]].join(', ') + '), rgb(' +
                [self.colors[1], self.colors[1], self.colors[1]].join(', ') + ') ' + 
                Math.round(self.colorstops[0] * 100) + '%, rgb(' +
                [self.colors[1], self.colors[1], self.colors[1]].join(', ') + ') ' + 
                Math.round(self.colorstops[1] * 100) + '%, rgb(' +
                [self.colors[2], self.colors[2], self.colors[2]].join(', ') + '))' 
        });
    }

    if (self.hasPasswordPlaceholder) {
        self.$input.hide();
        self.$placeholder = $('<input>')
            .addClass('OxInput OxMedium Ox' +
                Ox.toTitleCase(self.options.style) +
                ' OxPlaceholder')
            .attr({
                type: 'text'
            })
            .css({
                //float: 'left',
                width: self.inputWidth + 'px'
            })
            .val(self.options.placeholder)
            .focus(focus)
            .appendTo(that.$element);
    }

    if (self.options.autocomplete && self.options.autocompleteSelect) {
        self.$autocompleteMenu = constructAutocompleteMenu();
    }

    self.options.placeholder && setPlaceholder();

    function autocomplete(oldValue, oldCursor) {

        oldValue = Ox.isUndefined(oldValue) ? self.options.value : oldValue;
        oldCursor = Ox.isUndefined(oldCursor) ? cursor : oldCursor;

        Ox.print('autocomplete', oldValue, oldCursor)

        if (self.options.value || self.options.autocompleteReplaceCorrect) {
            if(Ox.isFunction(self.options.autocomplete)) {
                if(self.options.key) {
                    self.options.autocomplete(self.options.key,
                                              self.options.value,
                                              autocompleteCallback)
                } else {
                    self.options.autocomplete(self.options.value,
                                              autocompleteCallback)
                }
            } else {
                autocompleteCallback(autocompleteFunction(self.options.value));
            }
        }
        if (!self.options.value) {
            self.options.autocompleteSelect && self.$autocompleteMenu.hideMenu();
        }

        function autocompleteFunction() {
            var values = Ox.find(self.options.autocomplete, self.options.value);
            return self.options.autocompleteReplace ? values[0] :
                    $.merge(values[0], values[1]);
        }

        function autocompleteCallback(values) {

            //Ox.print('autocompleteCallback', values[0], self.options.value, self.options.value.length, oldValue, oldCursor)

            var length = self.options.value.length,
                deleted = length <= oldValue.length - (oldCursor[1] - oldCursor[0]),
                newValue = values[0] ?
                        ((self.options.autocompleteReplaceCorrect || !deleted) ?
                        values[0] : self.options.value) :
                        (self.options.autocompleteReplaceCorrect ? oldValue : self.options.value),
                newLength = newValue.length,
                pos = cursor(),
                selected = -1,
                selectEnd = length == 0 || (values[0] && values[0].length),
                value;

            //Ox.print('selectEnd', selectEnd)

            if (self.options.autocompleteReplace) {
                self.options.value = newValue;
                self.$input.val(self.options.value);
                if (selectEnd) {
                    cursor(length, newLength);
                } else if (self.options.autocompleteReplaceCorrect) {
                    cursor(oldCursor);
                } else {
                    cursor(pos);
                }
                selected = 0;
            }

            if (self.options.autocompleteSelect) {
                value = self.options.value.toLowerCase();
                if (values.length) {
                    self.oldCursor = cursor();
                    self.oldValue = self.options.value;
                    self.$autocompleteMenu.options({
                        items: $.map(values, function(v, i) {
                            if (value == v.toLowerCase()) {
                                selected = i;
                            }
                            return {
                                id: v.toLowerCase().replace(/ /g, '_'), // fixme: need function to do lowercase, underscores etc?
                                title: self.options.autocompleteSelectHighlight ? v.replace(
                                    new RegExp('(' + value + ')', 'ig'),
                                    '<span class="OxHighlight">$1</span>'
                                ) : v
                            };
                        }),
                        selected: selected
                    }).showMenu();
                } else {
                    self.$autocompleteMenu.hideMenu();
                }
            }

            that.triggerEvent('autocomplete', {
                value: newValue
            });

        }

    }

    function constructAutocompleteMenu() {
        var menu = new Ox.Menu({
                element: self.$input,
                id: self.options.id + 'Menu', // fixme: we do this in other places ... are we doing it the same way? var name?,
                offset: {
                    left: 4,
                    top: 0
                }
                //size: self.options.size
            })
            .bindEvent('click', clickMenu);
        if (self.options.autocompleteReplace) {
            menu.bindEvent({
                deselect: deselectMenu,
                select: selectMenu,
            });
        }
        return menu;
    }

    function autovalidate() {

        var blur, oldCursor, oldValue;

        if (arguments.length == 1) {
            blur = arguments[0];
        } else {
            blur = false;
            oldValue = arguments[0];
            oldCursor = arguments[1];
        }

        if (Ox.isFunction(self.options.autovalidate)) {
            if (self.options.key) {
                self.options.autovalidate(
                    self.options.key, self.options.value, blur, autovalidateCallback
                );
            } else {
                self.options.autovalidate(
                    self.options.value, blur, autovalidateCallback
                );
            }
        } else if (Ox.isRegExp(self.options.autovalidate)) {
                autovalidateCallback(autovalidateFunction(self.options.value));
        } else {
            autovalidateTypeFunction(self.options.type, self.options.value);
        }

        function autovalidateFunction(value) {
            var regexp = new RegExp(self.options.autovalidate);
            return $.map(value.split(''), function(v) {
                return regexp.test(v) ? v : null;
            }).join('');
        }

        function autovalidateTypeFunction(type, value) {
            // fixme: remove trailing zeroes on blur
            var cursor,
                regexp = type == 'float' ? /[\d\.]/ : /\d/;
            if (type == 'float') {
                if (value.indexOf('.') != value.lastIndexOf('.')) {
                    value = oldValue;
                } else {
                    if (self.autovalidateFloatFlag) {
                        if (Ox.endsWith(value, '.')) {
                            value = value.substr(0, value.length - 1);
                        }
                        self.autovalidateFloatFlag = false;
                    }
                    while (value[0] == '0' && value[1] != '.') {
                        value = value.substr(1);
                    }
                    while (Ox.startsWith(value, '.')) {
                        if (Ox.startsWith(value, '..')) {
                            value = value.substr(1);
                        } else {
                            value = '0' + value;
                        }
                    }
                    if (Ox.endsWith(value, '.')) {
                        value += '0';
                        cursor = [value.length - 1, value.length];
                        self.autovalidateFloatFlag = true;
                    }
                }
            }
            value = $.map(value.split(''), function(v) {
                return regexp(v) ? v : null;
            }).join('');
            if (type == 'integer') {
                while (value.length > 1 && Ox.startsWith(value, '0')) {
                    value = value.substr(1);
                }
            }
            if (value === '') {
                value = type == 'float' ? '0.0' : '0';
                cursor = [0, value.length];
            } else if (value > self.options.max) {
                value = oldValue;
            }
            autovalidateCallback(value, cursor);
        }

        function autovalidateCallback(newValue, newCursor) {
            //Ox.print('autovalidateCallback', newValue, oldCursor)
            self.options.value = newValue;
            self.$input.val(self.options.value);
            !blur && cursor(
                newCursor || (oldCursor[1] + newValue.length - oldValue.length)
            );
            that.triggerEvent('autovalidate', {
                value: self.options.value
            });
        }

    }

    /*

    function autovalidate(blur) {
        Ox.print('autovalidate', self.options.value, blur || false)
        self.autocorrectBlur = blur || false;
        self.autocorrectCursor = cursor();
        Ox.isFunction(self.options.autocorrect) ?
            (self.options.key ? self.options.autocorrect(
                self.options.key,
                self.options.value,
                self.autocorrectBlur,
                autocorrectCallback
            ) : self.options.autocorrect(
                self.options.value,
                self.autocorrectBlur,
                autocorrectCallback
            )) : autocorrectCallback(autocorrect(self.options.value));
    }

    function autovalidateFunction(value) {
        var length = value.length;
        return $.map(value.toLowerCase().split(''), function(v, i) {
            if (new RegExp(self.options.autocorrect).test(v)) {
                return v;
            } else {
                return null;
            }
        }).join('');
    }

    */

    function blur() {
        that.loseFocus();
        //that.removeClass('OxFocus');
        self.options.value = self.$input.val();
        self.options.autovalidate && autovalidate(true);
        self.options.placeholder && setPlaceholder();
        self.options.validate && validate();
        if (self.bindKeyboard) {
            Ox.UI.$document.unbind('keydown', keypress);
            Ox.UI.$document.unbind('keypress', keypress);
        }
        that.triggerEvent('blur');
    }

    function cancel() {
        self.$input.blur();
    }

    function change() {
        self.options.value = self.$input.val();
        that.triggerEvent('change', {
            value: self.options.value
        });
    }

    function clear() {
        // fixme: set to min, not zero
        // fixme: make this work for password
        var value = '';
        if (self.options.type == 'float') {
            value = '0.0';
        } else if (self.options.type == 'integer') {
            value = '0'
        }
        self.$input.val(value);
        cursor(0, value.length);
    }

    function clickArrow(i) {
        self.options.value = Ox.limit(
            parseFloat(self.options.value) + (i == 0 ? -1 : 1) * self.options.arrowStep,
            self.options.min,
            self.options.max
        ).toString();
        self.$input.val(self.options.value);//.focus();
    }

    function clickMenu(event, data) {
        //Ox.print('clickMenu', data);
        self.options.value = data.title;
        self.$input.val(self.options.value).focus();
        that.gainFocus();
        self.options.autocompleteSelectSubmit && submit();
    }

    function cursor(start, end) {
        /*
        cursor()                returns [start, end]
        cursor(start)           sets start
        cursor([start, end])    sets start and end
        cursor(start, end)      sets start and end
        */
        var isArray = Ox.isArray(start);
        if (arguments.length == 0) {
            return [self.$input[0].selectionStart, self.$input[0].selectionEnd];
        } else {
            end = isArray ? start[1] : (end ? end : start);
            start = isArray ? start[0] : start;
            self.$input[0].setSelectionRange(start, end);
        }
    }

    function deselectMenu() {
        self.options.value = self.oldValue;
        self.$input.val(self.options.value);
        cursor(self.oldCursor);
    }

    function focus() {
        //Ox.print('focus()')
        if (
            that.hasClass('OxFocus') || // fixme: this is just a workaround, since for some reason, focus() gets called twice on focus
            (self.$autocompleteMenu && self.$autocompleteMenu.is(':visible')) ||
            (self.hasPasswordPlaceholder && self.$input.is(':visible'))
        ) {
            return;
        }
        that.gainFocus();
        that.is('.OxError') && that.removeClass('OxError');
        self.options.placeholder && setPlaceholder();
        if (self.bindKeyboard) {
            //Ox.print('binding...')
            // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
            Ox.UI.$document.keydown(keypress);
            Ox.UI.$document.keypress(keypress);
            self.options.autocompleteSelect && setTimeout(autocomplete, 0); // fixme: why is the timeout needed?
        }
        that.triggerEvent('focus');
    }

    function getInputWidth() {
        return self.options.width -
            (self.options.arrows ? 32 : 0) -
            (self.options.clear ? 16 : 0) -
            (self.options.label ? self.options.labelWidth : 0) -
            (self.options.style == 'rounded' ? 14 : 6);
    }

    function keypress(event) {
        var oldCursor = cursor(),
            oldValue = self.options.value,
            newValue = oldValue.substr(0, oldCursor[0] - 1),
            hasDeletedSelectedEnd = (event.keyCode == 8 || event.keyCode == 46) &&
                oldCursor[0] < oldCursor[1] && oldCursor[1] == oldValue.length;
        //Ox.print('keypress', event.keyCode)
        if (event.keyCode != 9 && (self.options.type == 'textarea' || event.keyCode != 13) && event.keyCode != 27) { // fixme: can't 13 and 27 return false?
            setTimeout(function() { // wait for val to be set
                var value = self.$input.val();
                if (self.options.autocompleteReplaceCorrect && hasDeletedSelectedEnd) {
                    //Ox.print(value, '->', newValue);
                    value = newValue; // value.substr(0, value.length - 1);
                    self.$input.val(value);
                }
                if (value != self.options.value) {
                    self.options.value = value;
                    self.options.autocomplete && autocomplete(oldValue, oldCursor);
                    self.options.autovalidate && autovalidate(oldValue, oldCursor);
                    self.options.changeOnKeypress && that.triggerEvent({
                        change: {
                            value: self.options.value
                        }
                    });
                }
            }, 0);
        }
        if ((event.keyCode == 38 || event.keyCode == 40) && self.options.autocompleteSelect && self.$autocompleteMenu.is(':visible')) {
            return false;
        }
    }

    function paste() {
        var data = Ox.Clipboard.paste();
        data.text && self.$input.val(data.text);
    }

    function selectMenu(event, data) {
        var pos = cursor();
        //Ox.print('selectMenu', pos)
        self.options.value = data.title
        self.$input.val(self.options.value);
        cursor(pos[0], self.options.value.length)
    }

    function setPlaceholder() {
        if (self.options.placeholder) {
            if (that.hasClass('OxFocus')) {
                if (self.options.value === '') {
                    if (self.options.type == 'password') {
                        self.$placeholder.hide();
                        self.$input.show().focusInput();
                    } else {
                        self.$input
                            .removeClass('OxPlaceholder')
                            .val('');
                    }
                }
            } else {
                if (self.options.value === '') {
                    if (self.options.type == 'password') {
                        self.$input.hide();
                        self.$placeholder.show();
                    } else {
                        self.$input
                            .addClass('OxPlaceholder')
                            .val(self.options.placeholder)
                    }
                } else {
                    self.$input
                        .removeClass('OxPlaceholder')
                        .val(self.options.value)
                }
            }
        }
    }

    function setWidth() {
        
    }

    function submit() {
        self.$input.blur();
        that.triggerEvent('submit', {
            value: self.options.value
        });
    }

    function validate() {
        self.options.validate(self.options.value, function(data) {
            that.triggerEvent('validate', data);
        });
    }

    self.setOption = function(key, value) {
        var inputWidth, val;
        if (['autocomplete', 'autocompleteReplace', 'autocompleteSelect', 'autovalidate'].indexOf(key) > -1) {
            if (self.options.autocomplete && self.options.autocompleteSelect) {
                self.$autocompleteMenu = constructAutocompleteMenu();
            }
            self.bindKeyboard = self.options.autocomplete || self.options.autovalidate;
        } else if (key == 'disabled') {
			self.$input.attr({
				disabled: value ? 'disabled' : ''
			});
		} else if (key == 'height') {
		    that.css({
		        height: value + 'px'
		    });
		    self.$input.css({
		        height: value + 'px'
		    });
		} else if (key == 'placeholder') {
            setPlaceholder();
        } else if (key == 'value') {
            val = self.$input.val(); // fixme: ??
            self.$input.val(value);
            setPlaceholder();
        } else if (key == 'width') {
            inputWidth = getInputWidth();
            self.$input.css({
                width: inputWidth + 'px'
            });
            self.hasPasswordPlaceholder && self.$placeholder.css({
                width: inputWidth + 'px'
            });
        }
    };

    that.blurInput = function() {
        self.$input.blur();
        return that;
    };

    that.clearInput = function() {
        clear();
        return that;
    }

    that.focusInput = function(select) {
        select = Ox.isUndefined(select) ? true : select;
        self.$input.focus();
        if (select) {
            cursor(0, self.$input.val().length);
        } else {
            cursor(self.$input.val().length);
        }
        return that;
    };

    // fixme: deprecate, options are enough
    that.value = function() {
        return self.$input.hasClass('OxPlaceholder') ? '' : self.$input.val();
    };

    return that;

};

/**
delete below
*/

Ox.AutocorrectIntFunction = function(min, max, pad, year) {
    var pad = pad || false,
        year = year || false,
        maxLength = max.toString().length,
        ret = null,
        values = [];
        Ox.range(min, max + 1).forEach(function(v) {
            values.push(v + '');
            pad && v.toString().length < maxLength && values.push(Ox.pad(v, maxLength));
        });
    return function(value, blur, callback) {
        var results;
        if (year && value == '1') {
            value = '1900';
        } else {
            results = Ox.find(values, value);
            value = results[0].length == 1 && results[0][0].length < maxLength ?
                (pad ? Ox.pad(results[0][0], maxLength) : results[0][0]) :
                (results[0].length ? results[0][0] : null);
        }
        callback(value);
    };
};

Ox.Input_ = function(options, self) {

    /*
    options:
        clear                   boolean, clear button, or not
        disabled                boolean, disabled, or not
        height                  height (px), if type is 'textarea'
        id
        label                   string, or
                                array [{ id, title, checked }] (selectable label) or
                                array [{ id, label: [{ id, title, checked }], width }] (multiple selectable labels)
                                label and placeholder are mutually exclusive
        labelWidth              integer (px)
        placeholder             string, or
                                array [{ id, title, checked }] (selectable placeholder)
                                label and placeholder are mutually exclusive
        separator               string, or
                                array of strings
                                to separate multiple values
        separatorWidth          integer (px), or
                                array of integers
        serialize               function
        size                    'large', 'medium' or 'small'
        type                    'password', 'select' or 'text'
        unit                    string, or
                                array [{ id, title, checked }] (selectable unit)
        unitWidth               integer (px)
        value                   string, or
                                array [{ id, value, width }] (multiple values)
        width                   integer (px)
    methods:
    events:
    */

    var self = self || {},
        that = new Ox.Element({}, self)
            .defaults({
                autocomplete: null,
                autocorrect: null,
                autosuggest: null,
                autosuggestHighlight: false,
                autosuggestSubmit: false,
                autovalidate: null,
                autovalidateName: 'Value',
                clear: false,
                disabled: false,
                height: 128,
                id: '',
                key: '',
                label: '',
                labelWidth: 64,
                placeholder: '',
                separator: '',
                separatorWidth: 16,
                serialize: null,
                size: 'medium',
                type: 'text',
                unit: '',
                unitWidth: 64,
                value: '',
                width: 128
            })
            .options(options || {})
            .addClass('OxInput Ox' + Ox.toTitleCase(self.options.size))
            .css({
                width: self.options.width + 'px'
            });

    $.extend(self, {
        clearWidth: 16,
        hasMultipleKeys: Ox.isArray(self.options.label) && 'label' in self.options.label[0],
        hasMultipleValues: Ox.isArray(self.options.value) &&
                (self.options.type != 'select' || 'items' in self.options.value[0]),
        hasSelectableKeys: Ox.isArray(self.options.label) || Ox.isArray(self.options.placeholder),
        hasSelectableUnits: Ox.isArray(self.options.unit),
        keyName: self.options.label ? 'label' : (self.options.placeholder ? 'placeholder' : ''),
        placeholderWidth: 16,
        selectedKey: [0], // fixme: only set on demand?
        selectedValue: 0,
        selectedUnit: 0,
        /* valid: autovalidateCall(true) */
    });

    ['autocomplete', 'autocorrect', 'autosuggest', 'autovalidate'].forEach(function(v) {
        //if (!Ox.isFunction(self.options[v])) {
            self.options[v] = {
                '': self.options[v]
            };
        //}
    });

    if (self.keyName && !self.hasMultipleKeys) {
        self.options[self.keyName] = [$.extend({
            id: '',
            label: self.options[self.keyName],
        }, self.keyName == 'label' ? {
            id: '',
            width: self.options.labelWidth
        } : {})];
        if (!self.hasSelectableKeys) {
            self.options[self.keyName][0].label = [{
                id: '',
                title: self.options[self.keyName][0].label
            }];
        }
    }
    if (self.hasSelectableKeys) {
        self.options[self.keyName].forEach(function(key, keyPos) {
            if (key.width) {
                self.options.labelWidth = (keyPos == 0 ? 0 : self.options.labelWidth) + key.width;
            }
            self.selectedKey[keyPos] = 0;
            Ox.forEach(key, function(value, valuePos) {
                if (value.checked) {
                    self.selectedKey[keyPos] = valuePos;
                    return false;
                }
            });
        });
    }
    self.valueWidth = self.options.width -
            (self.options.label ? self.options.labelWidth : 0) -
            ((self.options.placeholder && self.options.placeholder[0].label.length > 1) ? self.placeholderWidth : 0) -
            (self.options.unit ? self.options.unitWidth : 0) -
            (self.options.clear ? self.clearWidth : 0);
    /*
    if (self.hasMultipleValues) {
        self.valueWidth -= Ox.isArray(self.options.separatorWidth) ?
                Ox.sum(self.options.separatorWidth) :
                (self.options.value.length - 1) * self.options.separatorWidth;
    }
    */
    //Ox.print('self.hasMulVal', self.hasMultipleValues);
    //Ox.print('self.options.value', self.options.value)
    if (!self.hasMultipleValues) {
        if (self.options.type == 'select') {
            self.options.value = [{
                id: '',
                items: self.options.value,
                width: self.valueWidth
            }];
        } else if (self.options.type == 'range') {
            self.options.value = [$.extend({
                id: '',
                size: self.valueWidth
            }, self.options.value)];
        } else {
            self.options.value = [{
                id: '',
                value: self.options.value,
                width: self.valueWidth
            }]
        }
    }
    //Ox.print('self.options.value', self.options.value)
    self.values = self.options.value.length;
    //Ox.print(self.options.id, 'self.values', self.values)

    if (Ox.isString(self.options.separator)) {
        self.options.separator = $.map(new Array(self.values - 1), function(v, i) {
            return self.options.separator;
        });
    }
    if (Ox.isNumber(self.options.separatorWidth)) {
        self.options.separatorWidth = $.map(new Array(self.values - 1), function(v, i) {
            return self.options.separatorWidth;
        });
    }

    if (self.options.unit) {
        if (self.hasSelectableUnits) {
            Ox.forEach(self.options.unit, function(unit, pos) {
                if (unit.checked) {
                    self.selectedUnit = pos;
                    return false;
                }
            });
        } else {
            self.options.unit = [{
                id: '',
                title: self.options.unit
            }];
        }
    }

    //Ox.print('self', self);

    if (self.keyName) {
        that.$key = [];
        self.options[self.keyName].forEach(function(key, keyPos) {
            //Ox.print('keyPos key', keyPos, key)
            if (self.keyName == 'label' && key.label.length == 1) {
                that.$key[keyPos] = new Ox.Label({
                        overlap: 'right',
                        title: key.label[0].title,
                        width: self.options.labelWidth
                    })
                    .css({
                        float: 'left'
                    })
                    .click(function() {
                        that.$input[0].focusInput();
                    })
                    .appendTo(that);
            } else if (key.label.length > 1) {
                //Ox.print('key.length > 1')
                self.selectKeyId = self.options.id + Ox.toTitleCase(self.keyName) +
                        (self.options[self.keyName].length == 1 ? '' : keyPos);
                //Ox.print('three', self.selectedKey, keyPos, self.selectedKey[keyPos]);
                that.$key[keyPos] = new Ox.Select({
                        id: self.selectKeyId,
                        items: $.map(key.label, function(value, valuePos) {
                            return {
                                checked: valuePos == self.selectedKey[keyPos],
                                id: value.id,
                                group: self.selectKeyId, // fixme: same id, works here, but should be different
                                title: value.title
                            };
                        }),
                        overlap: 'right',
                        type: self.options.label ? 'text' : 'image',
                        width: self.options.label ? (self.options.label.length == 1 ? self.options.labelWidth : key.width) : self.placeholderWidth
                    })
                    .css({
                        float: 'left'
                    })
                    .appendTo(that);
                that.bindEvent('change_' + self.selectKeyId, changeKey);
            }
        });
    }

    if (self.options.clear) {
        that.$clear = new Ox.Button({
                overlap: 'left',
                title: 'close',
                type: 'image'
            })
            .css({
                float: 'right'
            })
            .click(clear)
            .appendTo(that);
    }

    if (self.options.unit.length == 1) {
        that.$unit = new Ox.Label({
                overlap: 'left',
                title: self.options.unit[0].title,
                width: self.options.unitWidth
            })
            .css({
                float: 'right'
            })
            .click(function() {
                that.$input[0].focusInput();
            })
            .appendTo(that);
    } else if (self.options.unit.length > 1) {
        self.selectUnitId = self.options.id + 'Unit';
        that.$unit = new Ox.Select({
                id: self.selectUnitId,
                items: $.map(self.options.unit, function(unit, i) {
                    //Ox.print('unit', unit)
                    return {
                        checked: i == 0,
                        id: unit.id,
                        group: self.selectUnitId, // fixme: same id, works here, but should be different
                        title: unit.title
                    };
                }),
                overlap: 'left',
                size: self.options.size,
                width: self.options.unitWidth
            })
            .css({
                float: 'right'
            })
            .appendTo(that);
    }

    if (self.values) {
        that.$separator = [];
        self.options.value.forEach(function(v, i) {
            if (i < self.values - 1) {
                that.$separator[i] = new Ox.Label({
                        textAlign: 'center',
                        title: self.options.separator[i],
                        width: self.options.separatorWidth[i] + 32
                    })
                    .css({
                        float: 'left',
                        marginLeft: (v.width - (i == 0 ? 16 : 32)) + 'px'
                    })
                    .click(function() {
                        that.$input[0].focusInput();
                    })
                    .appendTo(that);
            }
        });
    }
    that.$input = [];
    //self.margin = 0;
    self.options.value.forEach(function(v, i) {
        //Ox.print('o k i', self.options, self.keyName, i);
        var id = self.keyName ? $.map(self.selectedKey, function(v, i) {
                return self.options[self.keyName][i].id;
            }).join('.') : '';
        //self.margin -= (i == 0 ? 16 : self.options.value[i - 1].width)
        //Ox.print('v:', v, 'id:', id)
        if (self.options.type == 'select') {
            that.$input[i] = new Ox.Select({
                    id: v.id,
                    items: v.items,
                    width: v.width
                }).
                css({
                    float: 'left'
                });
        } else if (self.options.type == 'range') {
            that.$input[i] = new Ox.Range(v)
                .css({
                    float: 'left'
                });
        } else {
            that.$input[i] = new Ox.InputElement({
                autocomplete: self.options.autocomplete[id],
                autocorrect: self.options.autocorrect[id],
                autosuggest: self.options.autosuggest[id],
                autosuggestHighlight: self.options.autosuggestHighlight,
                autosuggestSubmit: self.options.autosuggestSubmit,
                autovalidate: self.options.autovalidate[id],
                autovalidateName: self.options.autovalidateName,
                disabled: self.options.disabled,
                height: self.options.height,
                id: self.options.id + 'Input' + Ox.toTitleCase(v.id),
                key: self.hasSelectableKeys ? self.options[self.keyName][0].label[self.selectedKey[0]].id : '',
                parent: that,
                placeholder: self.options.placeholder ? self.options.placeholder[0].label[0].title : '',
                size: self.options.size,
                type: self.options.type,
                value: v.value,
                width: v.width
            });
        }
        that.$input[i]
            .css($.extend({}, self.options.value.length > 1 ? {
                float: 'left',
                marginLeft: -Ox.sum($.map(self.options.value, function(v_, i_) {
                    return i_ > i ? self.options.value[i_ - 1].width + self.options.separatorWidth[i_ - 1] : (i_ == i ? 16 : 0);
                }))
            } : {}))
            .appendTo(that);
    });

    //width(self.options.width);

    function changeKey(event, data) {
        //Ox.print('changeKey', data);
        if (data) { // fixme: necessary?
            self.key = {
                id: data.id,
                title: data.value // fixme: should be data.title
            };
            that.$input[0].options({
                key: data.id
            });
        }
        if (self.options.label) {
            //that.$label.html(self.option.title);
            that.$input[0].focusInput();
            //autocompleteCall();
        } else {
            that.$input[0].options({
                placeholder: data.value // fixme: should be data.title
            });
            /*
            if (that.$input.hasClass('OxPlaceholder')) {
                that.$input.val(self.key.title);
                //that.$input.focus();
            } else {
                that.$input.focus();
                self.options.autosuggest && autosuggestCall();
            }
            */
        }
    }

    function changeUnit() {
        that.$input[0].focusInput();
    }

    function clear() {
        that.$input.forEach(function(v, i) {
            v.val('');
        });
        that.$input[0].focusInput();
    }

    function height(value) {
        var stop = 8 / value;
        if (self.options.type == 'textarea') {
            that.$element
                .height(value)
                .css({
                    background: '-moz-linear-gradient(top, rgb(224, 224, 224), rgb(208, 208, 208) ' + (stop * 100) + '%, rgb(208, 208, 208) ' + (100 - stop * 100) + '%, rgb(192, 192, 192))'
                })
                .css({
                    background: '-webkit-gradient(linear, left top, left bottom, from(rgb(224, 224, 224)), color-stop(' + stop + ', rgb(208, 208, 208)), color-stop(' + (1 - stop) + ', rgb(208, 208, 208)), to(rgb(192, 192, 192)))'
                });
            that.$input
                .height(value)
                .css({
                    background: '-moz-linear-gradient(top, rgb(224, 224, 224), rgb(240, 240, 240) ' + (stop * 100) + '%, rgb(240, 240, 240) ' + (100 - stop * 100) + '%, rgb(255, 255, 255))'
                })
                .css({
                    background: '-webkit-gradient(linear, left top, left bottom, from(rgb(224, 224, 224)), color-stop(' + stop + ', rgb(240, 240, 240)), color-stop(' + (1 - stop) + ', rgb(240, 240, 240)), to(rgb(255, 255, 255)))'
                });
        }
    }

    function selectUnit() {
        self.$selectUnitMenu.show();
    }

    function submit() {
        //Ox.print('submit')
        var value = that.$input.val();
        that.$input.blur();
        that.triggerEvent('submit', self.options.key ? {
            key: self.options.key,
            value: value
        } : value);
    }

    function width(value) {
        that.$element.width(value);
        that.$input.width(
            value - (self.options.type == 'textarea' ? 0 : 12) -
                    (self.options.label ? self.options.labelWidth : 0) -
                    (self.options.placeholder.length > 1 ? 16 : 0) -
                    (self.options.unit ? self.options.unitWidth : 0) -
                    (self.options.clear ? 16 : 0)
        );
    }

    self.setOption = function(key, value) {
        if (key == 'height') {
            height(value);
        } else if (key == 'width') {
            width(value);
        }
    };

    that.changeLabel = function(id) {
        that.$key.html(Ox.getObjectById(self.options.label, id).title);
        self.selectMenu.checkItem(id);
    };

    return that;

}

Ox.InputElement_ = function(options, self) {

    var self = self || {},
        that = new Ox.Element(
                options.type == 'textarea' ? 'textarea' : 'input', self
            )
            .defaults({
                autocomplete: null,
                autocorrect: null,
                autosuggest: null,
                autosuggestHighlight: false,
                autosuggestSubmit: false,
                autovalidate: null,
                disabled: false,
                height: 128,
                id: '',
                key: '',
                parent: null,
                placeholder: '',
                size: 'medium',
                type: 'text',
                value: '',
                width: 128
            })
            .options(options || {})
            .addClass('OxInput Ox' + Ox.toTitleCase(self.options.size) + (
                (self.options.placeholder && self.options.value === '') ?
                ' OxPlaceholder' : ''
            ))
            .attr(self.options.type == 'textarea' ? {} : {
                type: self.options.type
            })
            .css({
                float: 'left',
                width: (self.options.width - 14) + 'px'
            })
            .val(
                (self.options.placeholder && self.options.value === '') ?
                self.options.placeholder : self.options.value
            )
            .blur(blur)
            .change(change)
            .focus(focus);

    //Ox.print('InputElement self.options', self.options)

    self.bindKeyboard = self.options.autocomplete || self.options.autocorrect ||
                    self.options.autosuggest || self.options.autovalidate;

    if (self.options.autosuggest) {
        self.autosuggestId = self.options.id + 'Menu'; // fixme: we do this in other places ... are we doing it the same way? var name?
        self.$autosuggestMenu = new Ox.Menu({
            element: that.$element,
            id: self.autosuggestId,
            offset: {
                left: 4,
                top: 0
            },
            size: self.options.size
        });
        that.bindEvent('click_' + self.autosuggestId, clickMenu);
    }

    that.bindEvent($.extend(self.options.type == 'textarea' ? {} : {
        key_enter: submit
    }, {
        key_escape: cancel
    }));

    function autocomplete(value) {
        var value = value.toLowerCase(),
            ret = '';
        if (value !== '') {
            Ox.forEach(self.options.autocomplete, function(v, i) {
                if (v.toLowerCase().indexOf(value) == 0) {
                    ret = v;
                    return false;
                }
            });
        }
        return ret;
    }

    function autocompleteCall() {
        var value = that.$element.val();
        Ox.isFunction(self.options.autocomplete) ?
            self.options.autocomplete(self.options.key ? {
                key: self.options.key,
                value: value
            } : value, autocompleteCallback) :
            autocompleteCallback(autocomplete(value));
    }

    function autocompleteCallback(value) {
        var pos = cursor()[0];
        if (value) {
            that.$element.val(value);
            cursor(pos, value.length);
        }
    }

    function autocorrect(value) {
        var length = value.length;
        return $.map(value.toLowerCase().split(''), function(v, i) {
            if (new RegExp(self.options.autocorrect).test(v)) {
                return v
            } else {
                return null;
            }
        }).join('');
    }

    function autocorrectCall(blur) {
        var blur = blur || false,
            value = that.$element.val(),
            pos = cursor()[0];
        Ox.isFunction(self.options.autocorrect) ?
            self.options.autocorrect(value, blur, autocorrectCallback) :
            autocorrectCallback(autocorrect(value), blue);
    }

    function autocorrectCallback(value, blur) {
        var length = that.$element.val().length;
        that.$element.val(self.options.value);
        !blur && cursor(pos + value.length - length);
    }

    function autosuggest(value) {
        var value = value.toLowerCase(),
            values = [[], []];
        if (value !== '') {
            (
                self.options.key ?
                self.options.autosuggest[self.options.key] : self.options.autosuggest
            ).forEach(function(v, i) {
                //Ox.print('v...', v)
                var index = v.toLowerCase().indexOf(value);
                index > -1 && values[index == 0 ? 0 : 1].push(v);
            });
        }
        return $.merge(values[0], values[1]);
    }

    function autosuggestCall() {
        var value = that.$element.val();
        Ox.isFunction(self.options.autosuggest) ?
            self.options.autosuggest(self.options.key ? {
                key: self.options.key,
                value: value
            } : value, autosuggestCallback) :
            autosuggestCallback(autosuggest(value));
    }

    function autosuggestCallback(values) {
        var values = values || [],
            selected = values.length == 1 ? 0 : -1,
            value = that.$element.val().toLowerCase();
        //Ox.print('values', values);
        if (values.length) {
            values = $.map(values, function(v, i) {
                if (value == v.toLowerCase()) {
                    selected = i;
                }
                return {
                    id: v.toLowerCase().replace(/ /g, '_'), // fixme: need function to do lowercase, underscores etc?
                    title: self.options.autosuggestHighlight ? v.replace(
                        new RegExp('(' + value + ')', 'ig'),
                        '<span class="OxHighlight">$1</span>'
                    ) : v
                };
            });
            // self.selectMenu && self.selectMenu.hideMenu(); // fixme: need event
            self.$autosuggestMenu.options({
                items: values,
                selected: selected
            }).showMenu();
        } else {
            self.$autosuggestMenu.hideMenu();
        }
    }

    function autovalidate(value) {
        return {
            valid: self.options.autovalidate(value) != null,
            message: 'Invalid ' + self.options.name
        };
    }

    function autovalidateCall(blur) {
        var blur = blur || false,
            value = that.$element.val();
        if (value !== '') {
            Ox.isFunction(self.options.autovalidate) ?
                self.options.autovalidate(value, autovalidateCallback) :
                autovalidateCallback(autovalidate(value), blur);
        } else {
            autovalidateCallback({
                blur: blur,
                valid: false,
                message: 'Empty ' + self.options.name
            });
        }
    }

    function autovalidateCallback(data, blur) {
        if (data.valid != self.valid) {
            self.valid = data.valid;
            that.triggerEvent('validate', $.extend(data, {
                blur: blur
            }));
        }
    }

    function blur() {
        if (!self.options.autosuggest || self.$autosuggestMenu.is(':hidden')) {
            //Ox.print('losing focus...')
            that.loseFocus();
            self.options.parent.removeClass('OxFocus');
            self.options.autocorrect && autocorrectCall(true);
            // self.options.autosuggest && self.$autosuggestMenu.hideMenu();
            self.options.autovalidate && autovalidateCall(true);
            if (self.options.placeholder && that.$element.val() === '') {
                that.$element.addClass('OxPlaceholder').val(self.options.placeholder);
            }
        }
        if (self.bindKeyboard) {
            Ox.UI.$document.unbind('keydown', keypress);
            Ox.UI.$document.unbind('keypress', keypress);
        }
    }

    function cancel() {
        that.$element.blur();
    }

    function change() {
        
    }

    function clear() {
        that.$element.val('').focus();
    }

    function clickMenu(event, data) {
        //Ox.print('clickMenu', data);
        that.$element.val(data.title);
        //self.$autosuggestMenu.hideMenu();
        self.options.autosuggestSubmit && submit();
    }

    function cursor(start, end) {
        /*
        cursor()                returns [start, end]
        cursor(start)           sets start
        cursor([start, end])    sets start and end
        cursor(start, end)      sets start and end
        */
        var isArray = Ox.isArray(start);
        if (arguments.length == 0) {
            return [that.$element[0].selectionStart, that.$element[0].selectionEnd];
        } else {
            start = isArray ? start[0] : start;
            end = isArray ? start[1] : (end ? end : start);
            that.$element[0].setSelectionRange(start, end);
        }
    }

    function focus() {
        var val = that.$element.val();
        that.gainFocus();
        self.options.parent.addClass('OxFocus');
        if (that.$element.hasClass('OxPlaceholder')) {
            that.$element.val('').removeClass('OxPlaceholder');
        }
        if (self.bindKeyboard) {
            // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
            Ox.UI.$document.keydown(keypress);
            Ox.UI.$document.keypress(keypress);
            //Ox.print('calling autosuggest...')
            self.options.autosuggest && setTimeout(autosuggestCall, 0); // fixme: why is the timeout needed?
        }
    }

    function keypress(event) {
        //Ox.print('keyCode', event.keyCode)
        if (event.keyCode != 9 && event.keyCode != 13 && event.keyCode != 27) { // fixme: can't 13 and 27 return false?
            setTimeout(function() { // fixme: document what this timeout is for
                var value = that.$element.val();
                if (value != self.options.value) {
                    self.options.value = value;
                    self.options.autocomplete && autocompleteCall();
                    self.options.autocorrect && autocorrectCall();
                    self.options.autosuggest && autosuggestCall();
                    self.options.autovalidate && autovalidateCall();
                }
            }, 25);
        }
    }

    function submit() {
        
    }

    self.setOption = function(key, value) {
        if (key == 'placeholder') {
            that.$element.hasClass('OxPlaceholder') && that.$element.val(value);
        } else if (key == 'value') {
            if (self.options.placeholder) {
                if (value === '') {
                    that.$element.addClass('OxPlaceholder').val(self.options.placeholder);
                } else {
                    that.$element.removeClass('OxPlaceholder');
                }
            }
            change(); // fixme: keypress too
        }
    }

    return that;

}

Ox.Range_ = function(options, self) {

    /*
        init
    */
    var self = self || {},
        that = new Ox.Element({}, self)
            .defaults({
                animate: false,
                arrows: false,
                arrowStep: 1,
                arrowSymbols: ['left', 'right'],
                max: 100,
                min: 0,
                orientation: 'horizontal',
                step: 1,
                size: 128,
                thumbSize: 16,
                thumbValue: false,
                trackImages: [],
                trackStep: 0,
                value: 0,
                valueNames: null
            })
            .options($.extend(options, {
                arrowStep: options.arrowStep ?
                    options.arrowStep : options.step,
                trackImages: $.makeArray(options.trackImages || [])
            }))
            .addClass('OxRange')
            .css({
                width: self.options.size + 'px'
            });

    // 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.arrowSymbols[0]
            })
            .addClass('OxArrow')
            .mousedown(mousedownArrow)
            .click(clickArrowDec)
            .appendTo(that.$element);
    }
    var $track = new Ox.Element()
            .addClass('OxTrack')
            .mousedown(clickTrack)
            .appendTo(that.$element);

    if (trackImages) {
        var width = parseFloat(screen.width / trackImages),
            $image = $('<canvas>')
                .attr({
                    width: width * trackImages,
                    height: 14
                })
                .addClass('OxImage')
                .appendTo($track.$element),
            c = $image[0].getContext('2d');
            c.mozImageSmoothingEnabled = false; // we may want to remove this later
        self.options.trackImages.forEach(function(v, i) {
            var left = 0;
            $('<img/>')
                .attr({
                    src: v
                })
                .load(function() {
                    c.drawImage(this, left, 0, self.trackImageWidth[i], 14);
                });
            left += self.trackImageWidth[i];
        });
    }
    var $thumb = Ox.Button({})
        .addClass('OxThumb')
        .appendTo($track);
        //Ox.print('----')
    if (self.options.arrows) {
        var $arrowInc = Ox.Button({
                style: 'symbol',
                type: 'image',
                value: self.options.arrowSymbols[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);
        Ox.UI.$window.mousemove(function(e) {
            setValue(val(e));
        });
        Ox.UI.$window.one('mouseup', function() {
            Ox.UI.$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.valueNames ?
                            self.options.valueNames[self.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);
            that.triggerEvent('change', { value: val });
        }
    }
    function setWidth(width) {
        trackWidth = width - self.options.arrows * 32;
        thumbWidth = Math.max(trackWidth / values - 2, self.options.thumbSize - 2);
        that.$element.css({
            width: (width - 2) + 'px'
        });
        $track.css({
            width: (trackWidth - 2) + 'px'
        });
        if (trackImages) {
            $image.css({
                width: (trackWidth - 2) + 'px'
            });
        }
        $thumb.css({
            width: (thumbWidth - 2) + 'px',
            padding: 0
        });
        setThumb();
    }

    /*
        shared functions
    */

    self.setOption = function(key, value) {
        
    }

    return that;

};


