'use strict';

/*@
Ox.TableList <f> TableList Widget
    ([options[, self]]) -> <o:Ox.Element> TableList Object
    options <o> Options object
        columns <[o]|[]> Columns
            # Fixme: There's probably more...
            editable <b> ...
            format <f> ...
            id <s> ...
            removable <b> ...
            map <f> function that maps values to sort values
            operator <s> default sort operator
            title <s> ...
            titleImage <s> ...
            unformat <f> Applied before editing
            unique <b> If true, this column acts as unique id (deprecated)
            visible <b> ...
            width <n> ...
        columnsMovable <b|false> If true, columns can be re-ordered
        columnsRemovable <b|false> If true, columns are removable
        columnsResizable <b|false> If true, columns are resizable
        columnsVisible <b|false> If true, columns are visible
        columnWidth <[n]|[40, 800]> Minimum and maximum column width
        draggable <b|false> If true, items can be dragged
        id <s|''> Id
        items <f|null> function() {} {sort, range, keys, callback} or array
        keys <[s]|[]> Additional keys (apart from keys of visible columns)
        max <n|-1> Maximum number of items that can be selected (-1 for all)
        min <n|0> Minimum number of items that must be selected
        pageLength <n|100> Number of items per page
        query <o> Query
        scrollbarVisible <b|false> If true, the scrollbar is always visible
        selectAsYouType <s|''> If set to a key, enables select-as-you-type
        selected <[s]|[]> Array of selected ids
        sort <[o]|[s]|[]> ['+foo', ...] or [{key: 'foo', operator: '+'}, ...]
        sortable <b|false> If true, elements can be re-ordered
        sums <[s]|[]> Sums to be included in totals
        unique <s|''> Key of the unique id
            This has precedence over a unique id specified via columns (which is
            deprecated).
        columnresize <!> columnresize
        columnchange <!> columnchange
    self    <o> shared private variable
@*/

// fixme: options.columnsMovable, but options.sortable ... pick one.

Ox.TableList = function(options, self) {

    // fixme: in columns, "operator" should be "sortOperator"

    self = self || {};
    var that = Ox.Element({}, self)
            .defaults({
                columns: [],
                columnsMovable: false,
                columnsRemovable: false,
                columnsResizable: false,
                columnsVisible: false,
                columnWidth: [40, 800],
                draggable: false,
                id: '',
                items: null,
                keys: [],
                max: -1,
                min: 0,
                pageLength: 100,
                query: {conditions: [], operator: '&'},
                scrollbarVisible: false,
                selected: [],
                sort: [],
                sortable: false,
                sums: [],
                unique: ''
            })
            .options(options || {})
            .update({
                items: function() {
                    that.$body.options({items: self.options.items});
                },
                paste: function() {
                    that.$body.options({paste: self.options.paste});
                },
                query: function() {
                    that.$body.options({query: self.options.query});
                },
                selected: function() {
                    that.$body.options({selected: self.options.selected});
                },
                sort: function() {
                    updateColumn();
                    that.$body.options({sort: self.options.sort});
                }
            })
            .addClass('OxTableList')
            .bindEvent({
                key_left: function() {
                    var $element = that.$body.$element,
                        scrollLeft = $element[0].scrollLeft - $element.width();
                    $element.animate({scrollLeft: scrollLeft}, 250);
                },
                key_right: function() {
                    var $element = that.$body.$element,
                        scrollLeft = $element[0].scrollLeft + $element.width();
                    $element.animate({scrollLeft: scrollLeft}, 250);
                }
            });

    self.options.columns.forEach(function(column) { // fixme: can this go into a generic ox.js function?
        // fixme: and can't these just remain undefined?
        if (Ox.isUndefined(column.align)) {
            column.align = 'left';
        }
        if (Ox.isUndefined(column.clickable)) {
            column.clickable = false;
        }
        if (Ox.isUndefined(column.editable)) {
            column.editable = false;
        }
        if (Ox.isUndefined(column.unique)) {
            column.unique = false;
        }
        if (Ox.isUndefined(column.visible)) {
            column.visible = false;
        }
        if (column.unique && !self.options.unique) {
            self.options.unique = column.id;
        }
    });

    if (Ox.isEmpty(self.options.sort)) {
        self.options.sort = [{
            key: self.options.unique,
            operator: Ox.getObjectById(self.options.columns, self.options.unique).operator
        }];
    } else{
        self.options.sort = self.options.sort.map(function(sort) {
            return Ox.isString(sort) ? {
                key: sort.replace(/^[\+\-]/, ''),
                operator: sort[0] == '-' ? '-' : '+'
            } : sort;
        });
    }

    Ox.extend(self, {
        columnPositions: [],
        defaultColumnWidths: self.options.columns.map(function(column) {
            return column.defaultWidth || column.width;
        }),
        hasItemsArray: Ox.isArray(self.options.items),
        itemHeight: 16,
        page: 0,
        pageLength: 100,
        scrollLeft: 0,
        selectedColumn: getColumnIndexById(self.options.sort[0].key),
        visibleColumns: self.options.columns.filter(function(column) {
            return column.visible;
        })
    });
    // fixme: there might be a better way than passing both visible and position
    self.options.columns.forEach(function(column) {
        if (!Ox.isUndefined(column.position)) {
            self.visibleColumns[column.position] = column;
        }
    });
    Ox.extend(self, {
        columnWidths: self.visibleColumns.map(function(column) {
            return column.width;
        }),
        pageHeight: self.options.pageLength * self.itemHeight
    });

    self.format = {};
    self.options.columns.forEach(function(column) {
        if (column.format) {
            self.format[column.id] = column.format;
        }
    });

    // Head

    if (self.options.columnsVisible) {
        that.$bar = Ox.Bar({
            orientation: 'horizontal',
            size: 16
        }).appendTo(that);
        that.$head = Ox.Container()
            .addClass('OxHead')
            .css({
                right: self.options.scrollbarVisible
                    ? Ox.UI.SCROLLBAR_SIZE + 'px' : 0
            })
            .appendTo(that.$bar);
        that.$head.$content.addClass('OxTitles');
        constructHead();
        if (self.options.columnsRemovable) {
            that.$select = Ox.Select({
                    id: self.options.id + 'SelectColumns',
                    items: self.options.columns.map(function(column) {
                        return {
                            disabled: column.removable === false,
                            id: column.id,
                            title: column.title
                        };
                    }),
                    max: -1,
                    min: 1,
                    type: 'image',
                    value: Ox.filter(self.options.columns, function(column) {
                        return column.visible;
                    }).map(function(column) {
                        return column.id;
                    })
                })
                .bindEvent('change', changeColumns)
                .appendTo(that.$bar.$element);
        }
    }

    // Body

    that.$body = Ox.List({
            construct: constructItem,
            draggable: self.options.draggable,
            id: self.options.id,
            itemHeight: 16,
            items: self.options.items,
            itemWidth: getItemWidth(),
            format: self.format, // fixme: not needed, happens in TableList
            keys: Ox.unique(
                (
                    self.hasItemsArray
                    ? self.options.columns
                    : self.visibleColumns
                ).map(function(column) {
                    return column.id;
                })
                .concat(self.options.unique)
                .concat(self.options.keys)
            ),
            max: self.options.max,
            min: self.options.min,
            orientation: 'vertical',
            pageLength: self.options.pageLength,
            paste: self.options.paste,
            query: self.options.query,
            selectAsYouType: self.options.selectAsYouType,
            selected: self.options.selected,
            sort: self.options.sort,
            sortable: self.options.sortable,
            sums: self.options.sums,
            type: 'text',
            unique: self.options.unique
        })
        .addClass('OxBody')
        .css({
            top: (self.options.columnsVisible ? 16 : 0) + 'px',
            overflowY: (self.options.scrollbarVisible ? 'scroll' : 'hidden')
        })
        .scroll(function() {
            var scrollLeft = $(this).scrollLeft();
            if (scrollLeft != self.scrollLeft) {
                self.scrollLeft = scrollLeft;
                that.$head && that.$head.scrollLeft(scrollLeft);
            }
        })
        .bindEvent(function(data, event) {
            if (event == 'cancel') {
                Ox.Log('List', 'cancel edit', data);
            } else if (event == 'edit') {
                that.editCell(data.id, data.key);
            } else if (event == 'select') {
                self.options.selected = data.ids;
            }
            that.triggerEvent(event, data);
        })
        .appendTo(that);

    that.$body.$content.css({
        width: getItemWidth() + 'px'
    });

    //Ox.Log('List', 's.vC', self.visibleColumns)

    function addColumn(id) {
        //Ox.Log('List', 'addColumn', id);
        var column, ids,
            index = 0;
        Ox.forEach(self.options.columns, function(v) {
            if (v.visible) {
                index++;
            } else if (v.id == id) {
                column = v;
                Ox.Break();
            }
        });
        column.visible = true;
        self.visibleColumns.splice(index, 0, column);
        self.columnWidths.splice(index, 0, column.width);
        that.$head.$content.empty();
        constructHead();
        !self.hasItemsArray && that.$body.options({
            keys: self.visibleColumns.map(function(column) {
                return column.id;
            }).concat(self.options.keys)
        });
        that.$body.reloadPages();
    }

    function changeColumns(data) {
        var add,
            ids = [];
        Ox.forEach(data.value, function(id) {
            var index = getColumnIndexById(id);
            if (!self.options.columns[index].visible) {
                addColumn(id);
                add = true;
                Ox.Break();
            }
            ids.push(id);
        });
        if (!add) {
            Ox.forEach(self.visibleColumns, function(column) {
                if (ids.indexOf(column.id) == -1) {
                    removeColumn(column.id);
                    Ox.Break();
                }
            });
        }
        triggerColumnChangeEvent();
    }

    function clickColumn(id) {
        Ox.Log('List', 'clickColumn', id);
        var i = getColumnIndexById(id),
            isSelected = self.options.sort[0].key == self.options.columns[i].id;
        self.options.sort = [{
            key: self.options.columns[i].id,
            operator: isSelected ?
                (self.options.sort[0].operator == '+' ? '-' : '+') :
                self.options.columns[i].operator,
            map: self.options.columns[i].map
        }];
        updateColumn();
        // fixme: strangely, sorting the list blocks updating the column,
        // so we use a timeout for now
        setTimeout(function() {
            that.$body.options({sort: self.options.sort});
        }, 10);
        that.gainFocus().triggerEvent('sort', {
            key: self.options.sort[0].key,
            operator: self.options.sort[0].operator
        });
    }

    function constructHead() {
        var pos;
        self.$heads = [];
        self.$titles = [];
        self.$orderButtons = [];
        self.visibleColumns.forEach(function(column, i) {
            var $resize;
            self.$heads[i] = Ox.Element()
                .addClass('OxHeadCell OxColumn' + Ox.toTitleCase(column.id))
                .css({width: self.columnWidths[i] - 5 + 'px'})
                .appendTo(that.$head.$content.$element);
            // if sort operator is set, bind click event
            if (column.operator) {
                self.$heads[i].bindEvent({
                    anyclick: function() {
                        clickColumn(column.id);
                    }
                });
            }
            // if columns are movable, bind drag events
            if (self.options.columnsMovable) {
                self.$heads[i].bindEvent({
                    dragstart: function(data) {
                        dragstartColumn(column.id, data);
                    },
                    drag: function(data) {
                        dragColumn(column.id, data);
                    },
                    dragpause: function(data) {
                        dragpauseColumn(column.id, data);
                    },
                    dragend: function(data) {
                        dragendColumn(column.id, data);
                    }
                });
            }
            self.$titles[i] = Ox.Element()
                .addClass('OxTitle')
                .css({
                    width: self.columnWidths[i] - 9 + 'px',
                    textAlign: column.align
                })
                .appendTo(self.$heads[i]);
            if (column.titleImage) {
                self.$titles[i].append(
                    $('<img>').attr({
                        src: Ox.UI.getImageURL('symbol' + Ox.toTitleCase(column.titleImage))
                    })
                );
            } else {
                self.$titles[i].html(column.title);
            }
            if (column.operator) {
                self.$orderButtons[i] = Ox.Button({
                        style: 'symbol',
                        title: column.operator == '+' ? 'up' : 'down',
                        type: 'image'
                    })
                    .addClass('OxOrder')
                    .css({marginTop: (column.operator == '+' ? 1 : -1) + 'px'})
                    .click(function() {
                        $(this).parent().trigger('click');
                    })
                    .appendTo(self.$heads[i]);
            }
            $resize = Ox.Element()
                .addClass('OxResize')
                .appendTo(that.$head.$content.$element);
            $('<div>').appendTo($resize);
            $('<div>').addClass('OxCenter').appendTo($resize);
            $('<div>').appendTo($resize);
            // if columns are resizable, bind click and drag events
            if (self.options.columnsResizable && column.resizable !== false) {
                $resize.addClass('OxResizable')
                    .bindEvent({
                        doubleclick: function(data) {
                            resetColumn(column.id, data);
                        },
                        dragstart: function(data) {
                            dragstartResize(column.id, data);
                        },
                        drag: function(data) {
                            dragResize(column.id, data);
                        },
                        dragend: function(data) {
                            dragendResize(column.id, data);
                        }
                    });
            }
        });
        that.$head.$content.css({
            width: (Ox.sum(self.columnWidths) + 2) + 'px'
        });
        pos = getColumnPositionById(self.options.columns[self.selectedColumn].id);
        if (pos > -1) {
            toggleSelected(self.options.columns[self.selectedColumn].id);
            self.$titles[pos].css({
                width: (self.options.columns[self.selectedColumn].width - 25) + 'px'
            });
        }
    }

    function constructItem(data) {
        var $item = $('<div>')
                .addClass('OxTarget')
                .css({
                    width: getItemWidth(true) + 'px'
                });
        self.visibleColumns.forEach(function(v, i) {
            var clickable = Ox.isBoolean(v.clickable) ? v.clickable : v.clickable(data),
                editable = Ox.isBoolean(v.editable) ? v.editable : v.editable(data),
                $cell;
            if (v.tooltip) {
                $cell = Ox.Element({
                    tooltip: function() {
                        return self.options.selected.indexOf(data[self.options.unique]) > -1
                            ? (Ox.isString(v.tooltip) ? v.tooltip : v.tooltip(data)) : '';
                    }
                });
            } else {
                // this is faster
                $cell = $('<div>');
            }
            $cell.addClass(
                    'OxCell OxColumn' + Ox.toTitleCase(v.id) +
                    (clickable ? ' OxClickable' : '') +
                    (editable ? ' OxEditable' : '')
                )
                .css({
                    width: (self.columnWidths[i] - (self.options.columnsVisible ? 9 : 8)) + 'px',
                    borderRightWidth: (self.options.columnsVisible ? 1 : 0) + 'px',
                    textAlign: v.align
                })
                // if the column id is not in data, we're constructing an empty cell
                .html(v.id in data ? formatValue(v.id, data[v.id], data) : '')
                .appendTo($item);
        });
        return $item;
    }

    function dragstartColumn(id, e) {
        self.drag = {
            columnOffsets: getColumnOffsets(),
            listOffset: that.$element.offset().left - that.$body.scrollLeft(),
            startPos: getColumnPositionById(id)
        }
        self.drag.stopPos = self.drag.startPos;
        $('.OxColumn' + Ox.toTitleCase(id)).css({opacity: 0.25});
        self.drag.startPos > 0 && self.$heads[self.drag.startPos].prev().children().eq(2).css({opacity: 0.25});
        self.$heads[self.drag.startPos].next().children().eq(0).css({opacity: 0.25});
        self.$heads[self.drag.startPos].addClass('OxDrag').css({ // fixme: why does the class not work?
            cursor: 'move'
        });
    }

    function dragColumn(id, e) {
        var listLeft = that.$element.offset().left,
            listRight = listLeft + that.$element.width(),
            pos = self.drag.stopPos;
        Ox.forEach(self.drag.columnOffsets, function(offset, i) {
            var x = self.drag.listOffset + offset + self.columnWidths[i] / 2;
            if (i < self.drag.startPos && e.clientX < x) {
                self.drag.stopPos = i;
                Ox.Break();
            } else if (i > self.drag.startPos && e.clientX > x) {
                self.drag.stopPos = i;
            }
        });
        if (self.drag.stopPos != pos) {
            moveColumn(id, self.drag.stopPos);
            self.drag.columnOffsets = getColumnOffsets();
            self.drag.startPos = self.drag.stopPos;
            ///*
            var left = self.drag.columnOffsets[self.drag.startPos],
                right = left + self.columnWidths[self.drag.startPos];
            if (left < that.$body.scrollLeft() || right > that.$element.width()) {
                that.$body.scrollLeft(
                    left < that.$body.scrollLeft() ? left : right - that.$element.width()
                );
                self.drag.listOffset = that.$element.offset().left - that.$body.scrollLeft();
            }
            //*/
        }
        if (e.clientX < listLeft + 16 || e.clientX > listRight - 16) {
            if (!self.scrollInterval) {
                self.scrollInterval = setInterval(function() {
                    that.$body.scrollLeft(
                        that.$body.scrollLeft() + (e.clientX < listLeft + 16 ? -16 : 16)
                    );
                    self.drag.listOffset = that.$element.offset().left - that.$body.scrollLeft();
                }, 100);
            }
        } else if (self.scrollInterval) {
            clearInterval(self.scrollInterval);
            self.scrollInterval = 0;
        }
    }

    function dragpauseColumn(id, e) {
    }

    function dragendColumn(id, e) {
        var column = self.visibleColumns.splice(self.drag.stopPos, 1)[0],
            width = self.columnWidths.splice(self.drag.stopPos, 1)[0];
        self.visibleColumns.splice(self.drag.stopPos, 0, column);
        self.columnWidths.splice(self.drag.stopPos, 0, width);
        that.$head.$content.empty();
        constructHead();
        $('.OxColumn' + Ox.toTitleCase(id)).css({opacity: 1});
        self.$heads[self.drag.stopPos].removeClass('OxDrag').css({
            cursor: 'pointer'
        });
        that.$body.clearCache();
        triggerColumnChangeEvent();
    }

    function dragstartResize(id, e) {
        var pos = getColumnPositionById(id);
        self.drag = {
            startWidth: self.columnWidths[pos]
        };
    }

    function dragResize(id, e) {
        var width = Ox.limit(
                self.drag.startWidth + e.clientDX,
                self.options.columnWidth[0],
                self.options.columnWidth[1]
            );
        resizeColumn(id, width);
    }

    function dragendResize(id, e) {
        var pos = getColumnPositionById(id);
        // fixme: shouldn't this be resizecolumn?
        that.triggerEvent('columnresize', {
            id: id,
            width: self.columnWidths[pos]
        });
    }

    function formatValue(key, value, data) {
        // fixme: this may be obscure...
        // since the format of a value may depend on another value,
        // we pass all data as a second parameter to the supplied format function
        var format = self.format[key], formatFunction;
        // FIXME: this keeps null from ever reaching a format function!
        if (value === null) {
            value = '';
        } else if (format) {
            if (Ox.isObject(format)) {
                value = (
                    /^color/.test(format.type.toLowerCase()) ? Ox.Theme : Ox
                )['format' + Ox.toTitleCase(format.type)].apply(
                    this, [value].concat(format.args || [])
                );
            } else {
                value = format(value, data);
            }
        } else if (Ox.isArray(value)) {
            value = value.join(', ');
        }
        return value;
    }

    function getCell(id, key) {
        var $item = getItem(id);
        key = key || ''; // fixme: what is this?
        return $($item.find('.OxCell.OxColumn' + Ox.toTitleCase(key))[0]);
    }

    function getColumnOffsets() {
        return self.visibleColumns.map(function(column, i) {
            return Ox.sum(self.visibleColumns.map(function(column_, i_) {
                return i_ < i ? self.columnWidths[i_] : 0;
            }));
        });
    }

    function getColumnIndexById(id) {
        return Ox.getIndexById(self.options.columns, id);
    }

    function getColumnPositionById(id) {
        return Ox.getIndexById(self.visibleColumns, id);
    }

    function getItem(id) {
        //Ox.Log('List', 'getItem', id)
        var $item = null;
        that.find('.OxItem').each(function() {
            var $this = $(this);
            if ($this.data('id') == id) {
                $item = $this;
                return false;
            }
        });
        return $item;
    }

    function getItemWidth(cached) {
        // fixme: this gets called for every constructItem and is slooow
        // the proper way to fix this would be to find out how and when
        // that.$element.width() might change... which would probably
        // mean binding to every SplitPanel and window resize...
        // for now, use a cached value
        if (!cached) {
            self.cachedWidth = that.$element.width();
        } else if (!self.cachedWidth || self.cachedWidthTime < +new Date() - 5000) {
            self.cachedWidth = that.$element.width();
            self.cachedWidthTime = +new Date();
        }
        return Math.max(
            Ox.sum(self.columnWidths),
            self.cachedWidth -
            (self.options.scrollbarVisible ? Ox.UI.SCROLLBAR_SIZE : 0)
        );
    }

    function moveColumn(id, pos) {
        //Ox.Log('List', 'moveColumn', id, pos)
        var startPos = getColumnPositionById(id),
            stopPos = pos,
            startSelector = '.OxColumn' + Ox.toTitleCase(id),
            stopSelector = '.OxColumn' + Ox.toTitleCase(self.visibleColumns[stopPos].id),
            insert = startPos < stopPos ? 'insertAfter' : 'insertBefore',
            $column = $('.OxHeadCell' + startSelector),
            $resize = $column.next();
        //Ox.Log('List', startSelector, insert, stopSelector)
        $column.detach()[insert](insert == 'insertAfter'
            ? $('.OxHeadCell' + stopSelector).next()
            : $('.OxHeadCell' + stopSelector));
        $resize.detach().insertAfter($column);
        that.$body.find('.OxItem').each(function() {
            var $this = $(this);
            $this.children(startSelector).detach()[insert](
                $this.children(stopSelector)
            );
        });
        var $head = self.$heads.splice(startPos, 1)[0],
            columnWidth = self.columnWidths.splice(startPos, 1)[0],
            visibleColumn = self.visibleColumns.splice(startPos, 1)[0];
        self.$heads.splice(stopPos, 0, $head);
        self.columnWidths.splice(stopPos, 0, columnWidth);
        self.visibleColumns.splice(stopPos, 0, visibleColumn);
        var pos = getColumnPositionById(self.options.columns[self.selectedColumn].id);
        if (pos > -1) {
            that.find('.OxResize .OxSelected').removeClass('OxSelected');
            pos > 0 && self.$heads[pos].prev().children().eq(2).addClass('OxSelected');
            self.$heads[pos].next().children().eq(0).addClass('OxSelected');
            if (pos == stopPos) {
                pos > 0 && self.$heads[pos].prev().children().eq(2).css({opacity: 0.25});
                self.$heads[pos].next().children().eq(0).css({opacity: 0.25});
            }
        }
    }

    function removeColumn(id) {
        //Ox.Log('List', 'removeColumn', id);
        var index = getColumnIndexById(id),
            itemWidth,
            position = getColumnPositionById(id),
            selector = '.OxColumn' + Ox.toTitleCase(id),
            $column = $('.OxHeadCell ' + selector),
            $order = $column.next(),
            $resize = $order.next();
        self.options.columns[index].visible = false;
        self.visibleColumns.splice(position, 1);
        self.columnWidths.splice(position, 1);
        that.$head.$content.empty();
        constructHead();
        itemWidth = getItemWidth();
        that.$body.find('.OxItem').each(function() {
            var $this = $(this);
            $this.children(selector).remove();
            $this.css({width: itemWidth + 'px'});
        });
        that.$body.$content.css({
            width: itemWidth + 'px'
        });
        !self.hasItemsArray && that.$body.options({
            keys: self.visibleColumns.map(function(column) {
                return column.id;
            }).concat(self.options.keys)
        });
        //that.$body.clearCache();
    }

    function resetColumn(id) {
        var width = self.defaultColumnWidths[getColumnIndexById(id)];
        resizeColumn(id, width);
        that.triggerEvent('columnresize', {
            id: id,
            width: width
        });
    }

    function resizeColumn(id, width) {
        var i = getColumnIndexById(id),
            pos = getColumnPositionById(id);
        self.options.columns[i].width = width;
        self.columnWidths[pos] = width;
        if (self.options.columnsVisible) {
            that.$head.$content.css({
                width: (Ox.sum(self.columnWidths) + 2) + 'px'
            });
            self.$heads[pos].css({
                width: width - 5 + 'px'
            });
            self.$titles[pos].css({
                width: width - 9 - (i == self.selectedColumn ? 16 : 0) + 'px'
            });
        }
        that.find('.OxCell.OxColumn' + Ox.toTitleCase(self.options.columns[i].id)).css({
            width: width - (self.options.columnsVisible ? 9 : 8) + 'px'
        });
        setWidth();
    }

    function setWidth() {
        var width = getItemWidth();
        that.$body.find('.OxItem').css({ // fixme: can we avoid this lookup?
            width: width + 'px'
        });
        that.$body.$content.css({
            width: width + 'px' // fixme: check if scrollbar visible, and listen to resize/toggle event
        });
    }

    function toggleSelected(id) {
        var pos = getColumnPositionById(id);
        if (pos > -1) {
            updateOrder(id);
            pos > 0 && self.$heads[pos].prev().children().eq(2).toggleClass('OxSelected');
            self.$heads[pos].toggleClass('OxSelected');
            self.$heads[pos].next().children().eq(0).toggleClass('OxSelected');
            self.$titles[pos].css({
                width: self.$titles[pos].width()
                    + (self.$heads[pos].hasClass('OxSelected') ? -16 : 16)
                    + 'px'
            });
        }
    }

    function triggerColumnChangeEvent() {
        that.triggerEvent('columnchange', {
            ids: self.visibleColumns.map(function(column) {
                return column.id;
            })
        });
    }

    function updateColumn() {
        var columnId = self.options.columns[self.selectedColumn].id,
            isSelected = columnId == self.options.sort[0].key;
        if (self.options.columnsVisible) {
            if (isSelected) {
                updateOrder(columnId);
            } else {
                toggleSelected(columnId);
                self.selectedColumn = getColumnIndexById(self.options.sort[0].key);
                toggleSelected(self.options.columns[self.selectedColumn].id);
            }
        }
    }

    function updateOrder(id) {
        var operator = self.options.sort[0].operator,
            pos = getColumnPositionById(id);
        if (pos > -1) {
            self.$orderButtons[pos].options({
                title: operator == '+' ? 'up' : 'down'
            }).css({
                marginTop: (operator == '+' ? 1 : -1) + 'px'
            });
        }
    }

    that.addItem = function(item) {
        /*
        self.options.items.push(item);
        that.$body.options({items: self.options.items});
        //that.$body.options({selected: [item.id]});
        */
    }

    /*@
    closePreivew <f> closePreview
    @*/
    that.closePreview = function() {
        that.$body.closePreview();
        return that;
    };

    /*@
    editCell <f> editCell
        (id, key, select) -> <u> edit cell
    @*/
    that.editCell = function(id, key, select) {
        Ox.Log('List', 'editCell', id, key)
        var $item = getItem(id),
            $cell = getCell(id, key),
            $input,
            html = $cell.html(),
            index = getColumnIndexById(key),
            column = self.options.columns[index],
            width = column.width - self.options.columnsVisible;
        $cell.empty()
            .addClass('OxEdit')
            .css({width: width + 'px'});
        $input = Ox.Input({
                autovalidate: column.input ? column.input.autovalidate : null,
                style: 'square',
                value: column.unformat ? column.unformat(html) : html,
                width: width
            })
            .on({
                mousedown: function(e) {
                    // keep mousedown from reaching list
                    e.stopPropagation();
                }
            })
            .bindEvent({
                blur: submit,
                cancel: submit,
                submit: submit
            })
            .appendTo($cell);
        // use timeout to prevent key to be inserted
        // into $input if triggered via keyboard shortcut
        setTimeout(function() {
            $input.focusInput(select);
        }, 0);
        function submit() {
            var value = $input.value();
            $input.remove();
            $cell.removeClass('OxEdit')
                .css({
                     // account for padding
                    width: (width - 8) + 'px'
                })
                .html(value);
            that.triggerEvent('submit', {
                id: id,
                key: key,
                value: value
            });
        }
    };

    /*@
    gainFocus <f> gainFocus
    @*/
    that.gainFocus = function() {
        that.$body.gainFocus();
        return that;
    };

    /*@
    hasFocus <f> hasFocus
    @*/
    that.hasFocus = function() {
        return that.$body.hasFocus();
    };

    /*@
    loseFocus <f> loseFocus
    @*/
    that.loseFocus = function() {
        that.$body.loseFocus();
        return that;
    };

    /*@
    openPreview <f> openPreview
    @*/
    that.openPreview = function() {
        that.$body.openPreview();
        return that;
    };

    /*@
    paste <f> paste
        (data) -> <o> paste data
    @*/
    that.paste = function(data) {
        that.$body.paste();
        return that;
    };

    /*@
    reloadList <f> reloadList
        (stayAtPosition) -> <o> reload list
    @*/
    that.reloadList = function(stayAtPosition) {
        that.$body.reloadList(stayAtPosition);
        return that;
    };

    /*@
    resizeColumn <f> resizeColumn
        (id, width) -> <o> resize column id to width
    @*/
    that.resizeColumn = function(id, width) {
        resizeColumn(id, width);
        return that;
    };

    /*@
    size <f> size
    @*/
    that.size = function() {
        setWidth();
        that.$body.size();
    };

    // fixme: deprecated
    that.sortList = function(key, operator) {
        Ox.Log('List', '$$$$ DEPRECATED $$$$')
        var isSelected = key == self.options.sort[0].key;
        self.options.sort = [{
            key: key,
            operator: operator,
            map: self.options.columns[self.selectedColumn].sort
        }];
        if (self.options.columnsVisible) {
            if (isSelected) {
                updateOrder(self.options.columns[self.selectedColumn].id);
            } else {
                toggleSelected(self.options.columns[self.selectedColumn].id);
                self.selectedColumn = getColumnIndexById(key);
                toggleSelected(self.options.columns[self.selectedColumn].id);
            }
        }
        // fixme: strangely, sorting the list blocks toggling the selection,
        // so we use a timeout for now
        setTimeout(function() {
            that.$body.options({sort: self.options.sort});
            /*
            that.$body.sortList(
                self.options.sort[0].key,
                self.options.sort[0].operator,
                self.options.sort[0].map
            );
            */
        }, 10);
        return that;
    };

    /*@
    value <f> value
        (id) -> get values of row id
        (id, key) -> get value of cell id, key
        (id, key, value) -> set id, key to value
    @*/
    that.value = function(id, key, value) {
        // fixme: make this accept id, {k: v, ...}
        var $cell;
        if (arguments.length == 1) {
            return that.$body.value(id);
        } else if (arguments.length == 2) {
            return that.$body.value(id, key);
        } else {
            that.$body.value(id, key, value);
            if (key == self.options.unique) {
                // unique id has changed
                self.options.selected = self.options.selected.map(function(id_) {
                    return id_ == id ? value : id_
                });
                id = value;
            }
            $cell = getCell(id, key);
            $cell && $cell.html(formatValue(key, value, that.$body.value(id)));
            if (key == self.options.sort[0].key) {
                // sort key has changed
                that.$body.sort();
            }
            return that;
        }
    };

    return that;

};
