
WBLOutlineAutoresizingStyle = {
    NoColumnAutoresizing: 0,                          // Disable column autoresizing
    UniformColumnAutoresizingStyle: 1,                // Autoresize all columns by distributing space equally, simultaneously.
    SequentialColumnAutoresizingStyle: 2,             // Autoresize each column sequentially, from the last auto-resizable column to the first; proceed to the next when the current has reached its minimum or maximum size.
    ReverseSequentialColumnAutoresizingStyle: 3,      // Autoresize each column sequentially, from the first auto-resizable column to the last; proceed to the next when the current has reached its minimum or maximum size.
    LastColumnOnlyAutoresizingStyle: 4,               // Autoresize only the last column.
    FirstColumnOnlyAutoresizingStyle: 5               // Autoresize only the first column.
};

WBLOutlineController = new Class({

    Extends: WBLScrollComponentController,

    Implements: [Options],

    options: {

        adjustColumnWidths: false,
        columnAutoresizingStyle:WBLOutlineAutoresizingStyle.NoColumnAutoresizing,
        complexColumnWidthComputation: false, // If set to true, takes all cell content into account
        clientSideCollapsingAndExpanding: false,
        allowsMultipleSelection: true,

        // These methods are generated by WBLAjaxActionFunction
        selectionChangedAction: null,
        toggleIsExpandedAction: null,
        collapseSelectionAction: null,
        expandSelectionAction: null,
        doubleClickAction: null,
        rowIndexBasedScrolling: false,
        isBeeingExported: false
    },

    debugName: function(){
        return 'Outline';
    },

    initialize: function(outlineElement, options){

        //        console.log(PWObjectId(this)+' Initialize MEWOutlineController');
        // Create a keyboard. It is not active by default and will be activated after the user has clicked inside the outline view.
        this.createKeyboard();
        this.classOfSelectedRows = 'selected';
        this.classOfCollapsedRows ='isCollapsed';

        this.outlineElement = outlineElement;
        this.parent(outlineElement, options)

//        console.log('this.options.isBeeingExported: '+this.options.isBeeingExported);
    },

    createMeasurementCanvas: function(){
        if(!this.measurementCanvas){
            this.measurementCanvas = new Element('canvas', {
                    styles: {
                        width:'0px',
                        height:'0px',
                        visibility: 'hidden',
                        position: 'absolute',
                    }
            });
        }
    },

    setupAfterContentDocumentIsReady: function(){

        this.parent();

        this.initializeInvariants();

        // The element used to measure widths can be inside an iFrame.
        // Because iFrames are loaded asynchronosly, we have to wait until it is loaded.
        this.createMeasurementCanvas();

        if(this.options.adjustColumnWidths)
            this.adjustColumnWidths();

        // Text in elements with a data-all-formatted-values attribute is made invisible if the cell contains an image and its size is below 50px.
        this.updateCellsWithLongText();

        // Detect the scroll element and add an observer to update autoformatted cells on scroll.
        this.updateScrollElementObserver();

        this.outlineElement.scrollViewController.updateLayout();

        this.flatRows = this.rows();

        if(this.updatesAutoformattedCells)
            this.updateAutoformattedCellsCache();

        this.makeTableColumnsResizable();
        this.makeTableRowsSelectable();

        if(this.options.clientSideCollapsingAndExpanding){
            this.prepareRowsForClientSideCollapsingAndExpanding(this.flatRows);
            this.updateRowsVisibility(this.flatRows);
        }

        this.prepareDisclosureButtons();

        // Detect clicks inside the outline view and set the first responder
        this.updateClickObserver();

    },

    updatesAutoformattedCells: true, // TODO: During debugging set to false

    autoformattedCellsFromRows: function(rows){

    },

    updateClickObserver: function(){
        var clickElement = this.iFrame ? this.iFrame.contentDocument.defaultView : this.scrollElement();
        var me = this;
        clickElement.addEvent('click', function(event){
              me.clickedInsideOutline(event);
           });
    },

    clickedInsideOutline: function(event){
         //console.log('clicked inside outline'); // TODO: Set first responder
    },

    dispose: function() {

        this.parent();

        //console.log('dispose MEWOutlineController');
        this.iFrame = this.outlineElement.getElement('iframe');
        if(this.iFrame){
            if(this.iFrame.contentDocument)
                this.iFrame.contentDocument.defaultView.removeEvents('scroll');
        }else
            this.scrollElement().removeEvents('scroll');

        this.disposeKeyboard();
        if(this.options.selectionChangedAction){
            if(window.MooDropMenu)
                window.parent.eventBus().removeObserver(MooDropMenu.WillShowMenuNotification, this.sendSelectionToServerIfNeeded, this);
            this.disposeSelectionToServerTimer();
        }
    },

    // Creates this.iFrame, this.scrollElement and this.table. Takes into account iFrames.
    initializeInvariants: function(){
        this.iFrame = this.outlineElement.getElement('iframe');
        if(this.iFrame){
            var iFrameBody = this.iFrame.contentDocument.body;
            this.table = iFrameBody.getElement('table');
            this.tableWindow = this.iFrame.contentWindow;
            this.scrollEventSource = this.iFrame.contentDocument.defaultView;

            // At this point we need to make sure we have a valid layout. So we need to remove the nolayout optimization
            iFrameBody.getElements('.nolayout').each(function(element){
                element.removeClass('nolayout');
            });
        }else{
            var tables = this.outlineElement.getElements('table');
            this.table = tables[1]; // 1 because of the header which is also a table
            // For debugging purposes only we fallback to the first table if no other is found
            if(!this.table)
                this.table = tables[0];

            this.tableWindow = window;
            this.scrollEventSource = this.scrollElement();
        }
        this.headerTable = this.outlineElement.getElement('.header');
        this.columns = this.table.getElements('col');
    },

    windowKeyboard: function(){
        if (!window.keyboard) {
            window.keyboard = new Keyboard();
            window.keyboard.activate();
        }
        return window.keyboard;
    },

    createKeyboard: function(){
        var keyboard = new Keyboard();
        this.windowKeyboard().manage(keyboard);
        var me = this;
        keyboard.addEvents({
            'up':function(e){me.upKeyPressed(e);},
            'shift+up':function(e){me.upKeyPressed(e);},
            'down':function(e){me.downKeyPressed(e);},
            'shift+down':function(e){me.downKeyPressed(e);},
            'left':function(e){me.leftKeyPressed(e);},
            'right':function(e){me.rightKeyPressed(e);},
            'enter':function(e){me.enterKeyPressed(e);},
            'enter+meta':function(e){me.enterKeyPressed(e);},
            'a+meta':function(e){me.selectAllRowsIfPossible(e);},
        });
        this.keyboard = keyboard;
    },

    disposeKeyboard: function(){
        var keyboard = this.keyboard;
        keyboard.relinquish();
        keyboard.deactivate();
        window.keyboard.drop(keyboard);
    },

    selectAllRowsIfPossible: function(event){
        if(this.options.allowsMultipleSelection){
            this.changedRows = new Array();
            var anchorIndex = null;
            this.rows().each(function(row){
                if(anchorIndex == null)
                    anchorIndex = row.index;
                this.addSelectionStyleToRow(row);
            }, this);
            this.setAnchorIndex(anchorIndex);
            this.updateSelectionBorders();
            this.selectionStyleDidChangeInRowsWithIndex(this.changedRows);
        }
        event.stop();
    },

    upKeyPressed: function(event){
        this.selectPreviousRow(event);
    },

    downKeyPressed: function(event){
        this.selectNextRow(event);
    },

    leftKeyPressed: function(event){
        this.collapseSelectedGroups();
    },

    rightKeyPressed: function(event){
        this.expandSelectedGroups();
    },

    enterKeyPressed: function(event){
    },

    collapseSelectedGroups: function(){
        if(this.options.clientSideCollapsingAndExpanding)
            this.collapseRowsOnClientSideOnly(this.collapsibleSelectedRows());
        if(this.options.collapseSelectionAction){
            this.sendSelectionToServerIfNeeded();
            var rowIndices = this.collapsibleSelectedRows().map(function(row){
                return row.index;
            });
            if(rowIndices.length)
                this.options.collapseSelectionAction({'rowIndices': JSON.encode(rowIndices)});
        }
    },

    expandSelectedGroups: function(){
        if(this.options.clientSideCollapsingAndExpanding)
            this.expandRowsOnClientSideOnly(this.expandableSelectedRows());
        else if(this.options.expandSelectionAction){
            this.sendSelectionToServerIfNeeded();
            var rowIndices = this.expandableSelectedRows().map(function(row){
                return row.index;
            });
            if(rowIndices.length)
                this.options.expandSelectionAction({'rowIndices': JSON.encode(rowIndices)});
        }
    },

    collapsibleSelectedRows: function() {
        var cssClass = this.classOfCollapsedRows;
        return this.selectedRows.filter(function(row){
            return row.hasClass('isCollapsible') && !row.hasClass(cssClass);
        });
    },

    expandableSelectedRows: function() {
        var cssClass = this.classOfCollapsedRows;
        return this.selectedRows.filter(function(row){
            return row.hasClass('isCollapsible') && row.hasClass(cssClass);
        });
    },

    prepareRowsForClientSideCollapsingAndExpanding: function(rows){

        var stack = [];
        var lastLevel = 0;
        var lastRow = null;

        rows.each(function(row){
            var level = row.getProperty('data-level').toInt();
            var index = row.getProperty('data-idx');
            row.level = level;

            row.isVisible = true;

            if(level > lastLevel)
                stack.push(lastRow);
            else if(level < lastLevel) {
                var diff = lastLevel-level;
                while(diff > 0){
                    stack.pop();
                    diff--;
                }
            }
            if(level > 0){
                var parent = stack.getLast();
                if(!parent.subrows)
                    parent.subrows = [];
                row.parentRow = parent;
                parent.subrows.push(row);
            }
            lastRow = row;
            lastLevel = level;
            lastIndex = index;
        });
    },

    flatRowsStartingWithRow: function(parentRow){
        var flatRows = [];
        this.addFlatRowsToArray(parentRow, flatRows);
        return flatRows;
    },

    addFlatRowsToArray: function(parentRow, array){
        array.push(parentRow);
        if(parentRow.subrows && parentRow.subrows.length > 0) {
            parentRow.subrows.each(function(subrow){
                this.addFlatRowsToArray(subrow, array);
            }, this);
        }
    },

    updateRowsVisibility: function(rows){

        var isAnyParentRowCollapsed = function(row){
            var iParent = row;
            do{
                iParent = iParent.parentRow;
                if(iParent && iParent.hasClass('isCollapsed'))
                    return true;
            }while(iParent);
            return false;
        }

        var classOfSelectedRows = this.classOfSelectedRows;
        var deselectedRows = new Array();
        var lastVisibleRow = null;
        var tableBody = this.table.getElement('tbody');
        rows.each(function(row){
            var parentElement = row.parentElement;
            var isInDOM = !!parentElement;
            var shouldBeVisible = !isAnyParentRowCollapsed(row);
            if(isInDOM && !shouldBeVisible){
                parentElement.removeChild(row);
                row.isVisible = false;
                if(row.hasClass(classOfSelectedRows))
                    deselectedRows.push(row);
            }else if(!isInDOM && shouldBeVisible){
                if(lastVisibleRow)
                    row.inject(lastVisibleRow, 'after');
                else
                    tableBody.appendChild(row);
                row.isVisible = true;
            }
            if(row.isVisible)
                lastVisibleRow = row;
        }, this);

        // Remove invisible rows from the selection
        if(this.selectedRows && deselectedRows.length > 0){
            this.removeRowsFromSelection(deselectedRows);
        }
        this.computeAnchorIndexFromSelection();
        this.updateSelectionBorders();
    },

    removeRowsFromSelection: function(rows){
        rows.each(function(row){
            var index = row.index;
            this.removeSelectionStyleFromRow(row);
            this.removeSelectionTopBorderFromRows([row]);
            this.removeSelectionBottomBorderFromRows([row]);
        }, this);
    },

    makeTableColumnsResizable: function(){
        var handles = this.headerTable.getElements('.handle');
        var headerTDs = this.headerTable.getElements('td');
        var contentColumns = this.table.getElements('col');
        var dragShield = new PWDragShield('col-resize');
        var me = this;
        this.headerTable.getElements('col').each(function(col, index){
            var contentCol = contentColumns[index];
            var resizing   = col.getAttribute('data-column-resizing');
            if(resizing.toInt() & 2) { // 2 == WBLOutlineColumnUserResizingMask
                var headerTD = headerTDs[index];
                var minWidth = headerTD.getStyle('min-width');
                var maxWidth = headerTD.getStyle('max-width');
                minWidth = minWidth ? minWidth.toInt() : 30;
                maxWidth = maxWidth ? maxWidth.toInt() : 10000;
                col.makeResizable({handle:handles[index],
                                    limit:{x:[minWidth, maxWidth]},
                                    onBeforeStart: function(){
                                        dragShield.show();
                                    },
                                    onDrag: function(el){
                                        me.onColumnResize(el, col, contentCol, index);
                                    },
                                    onStart: function(el){
                                        me.onColumnResizeStart(el, col, contentCol, index);
                                    },
                                    onCancel: function(){
                                        dragShield.hide();
                                    },
                                    onComplete: function(el){
                                        me.onColumnResizeComplete(el, col, contentCol, index);
                                        dragShield.hide();
                                    }});
            } else
                handles[index].setStyle('cursor','default');
        }, this);
    },

    onColumnResizeStart: function(el, headerColumn, contentColumn, index){
        this.registerUserInteraction();
    },

    onColumnResize: function(el, headerColumn, contentColumn, index){
        this.registerUserInteraction();
    },

    onColumnResizeComplete: function(el, headerColumn, contentColumn, index){
        contentColumn.setStyle('width', el.getStyle('width'));
        this.refreshAutoformattedCellsInColumn(contentColumn);
        this.updateCellsWithLongText();
    },

    // Text in elements with a data-all-formatted-values attribute is made invisible if the cell contains an image and its size is below 50px.
    updateCellsWithLongText: function(){
        var spans = this.table.getElements('[data-hide-long-text]');
        spans.each(function(span){
            var parent = span.getParent();
            var cell = parent.getParent();
            var img = cell.getElement('img');
            if(img){
                var origDisplayStyle = span.origDisplayStyle;
                if(!origDisplayStyle){
                    origDisplayStyle = span.getStyle('display');
                    span.origDisplayStyle = origDisplayStyle;
                }
                var col = this.columnForCell(cell);
                var width = this.availableTextWidthInCell(cell, parent);
                var isToSmall = width < 55;
                span.setStyle('display', isToSmall ? 'none' : origDisplayStyle);
                //parent.setStyle('text-align', isToSmall ? 'center' : null);
                img.setStyle('position', isToSmall ? 'relative' : null);
                img.setStyle('left', isToSmall ? '50%' : null);
                img.setStyle('transform', isToSmall ? 'translateX(-50%)' : null);
                img.setStyle('-webkit-transform', isToSmall ? 'translateX(-50%)' : null);
            }
        }, this);
    },

    columnForCell: function(cell){
        return this.columns[cell.getProperty('data-idx').toInt()];
    },

    // Detect the scroll element and update autoformatted cells on scroll.
    updateScrollElementObserver: function() {
        var me = this;
        this.iFrame = this.outlineElement.getElement('iframe');
        if(this.iFrame)
            this.iFrame.contentDocument.defaultView.addEvent('scroll', function(){
                me.registerUserInteraction();
//                 me.replaceContentInVisibleCellsIfNeeded(true);
            });
        else
            this.scrollElement().addEvent('scroll', function(){
                me.registerUserInteraction();
//                 me.replaceContentInVisibleCellsIfNeeded(true);
            });
    },

    // updates this.rowsWithAutoformattedCells
    updateAutoformattedCellsCache: function() {

        // Cells needing an update are added to this queue. The actual update is postponed until the user does no longer scroll.
        this.autoformattedCellsNeedingUpdates = new PWQueue();

           var spans = this.table.getElements('[data-all-formatted-values]');
        var cells = [];
        this.rowsWithAutoformattedCells = spans.map(function(span){

            // Cache the autoformatted cells on the row
            var row = span.getParent('tr');
            var cell = span.getParent('td');
//            if(!row.autoformatted)
//                row.autoformatted = new Array();
//            row.autoformatted.push(cell);
            cells.push(cell);
            return row;
        }).unique();

        this.replaceContentInCellsIfNeeded(cells);
    },

    getAutoformattedCellsInColumn: function(column){
        var index = column.getProperty('data-idx').toInt();
        tds = [];
        this.flatRows.each(function(row){
            var cells = row.getElements('td:nth-child('+(index+1)+')');
            cells.each(function(cell){
                tds.push(cell);
            });
        });
        return tds.filter(function(td){
            return td.getElement('[data-all-formatted-values]');
        });
    },

    // Returns the autoformatted cells in the given column
    invalidateAutoformattedCellsInColumn: function(column){
        var cells = this.getAutoformattedCellsInColumn(column);
        if(cells){
            cells.each(function(cell){
                cell.hasValidContent = false;
            });
        }
        return cells;
    },

    refreshAutoformattedCellsInColumn: function(column){
        this.clearCellWidthByIndex();
        var cells = this.invalidateAutoformattedCellsInColumn(column);
        this.replaceContentInCellsIfNeeded(cells);
    },

    replaceContentInCellsIfNeeded: function(cellElements, delay)
    {
        var update = false;
        cellElements.each(function(cellElement){
            if(!cellElement.hasValidContent){
                    cellElement.hasValidContent = true;
                    this.autoformattedCellsNeedingUpdates.enqueue(cellElement);
                    update = true;
            }
        }, this);
        if(update)
            this.updateAutoformattedCellsUpdateTimer();
    },

    updateAutoformattedCellsUpdateTimer: function(){

        clearInterval(this.autoformattedCellsUpdateTimer);

        var me = this;
        var update = function(){

            // Do nothing if the user is interacting with the app
            if(!!me.lastUserInteraction && (new Date().getTime() - me.lastUserInteraction) < 1000)
                return;

            // Update cells in batches
            var count = Math.min(me.autoformattedCellsNeedingUpdates.getLength(), 100);
            var cells = [];
            while(count>0){
                cells.push(me.autoformattedCellsNeedingUpdates.dequeue());
                count--;
            }
            me.updateContentInCells(cells);

            if(me.autoformattedCellsNeedingUpdates.isEmpty()){
            //    console.log('clear');
                clearInterval(me.autoformattedCellsUpdateTimer);
            }
        };

        if(!this.autoformattedCellsNeedingUpdates.isEmpty())
            this.autoformattedCellsUpdateTimer = update.periodical(0);
    },

    registerUserInteraction: function(){
        this.lastUserInteraction = new Date().getTime();
    },

    // Debugging only
    logRowOfCell: function(cellElement){
        var row = cellElement.getParent('tr').getElement('td > div > span').innerHTML;
        console.log('cell is visible ('+row.trim()+')');
    },

    clearCellWidthByIndex: function(){
        if(this.cellWidthByIndex)
            this.cellWidthByIndex.empty();
    },

    // cellElement must be a td element. element is the element we get the width from.
    // This method gets the width from a cache (this.cellWidthByIndex).
    // If a width is not found it is measured and put into the cache.
    availableTextWidthInCell: function(cellElement, element){
        var idx = cellElement.getProperty('data-idx');
        var index = idx.toInt();
        var widths = this.cellWidthByIndex;
        if(!widths){
            widths = new Array();
            this.cellWidthByIndex = widths;
        }
        var cellWidth = widths[index];
        if(!cellWidth){
            cellWidth = element.offsetWidth;
            widths[index] = cellWidth;
//            console.log('index='+index+' width='+cellWidth);
        }
        return cellWidth;
    },

    updateContentInCells: function(cells){
        var valueContainers = [];
        var texts = [];

        cells.each(function(cell){
            var valueContainer = cell.getElement('[data-all-formatted-values]');
            var allValues      = eval(valueContainer.getProperty('data-all-formatted-values'));
            var parent         = valueContainer.getParent();
            var parentWidth    = this.availableTextWidthInCell(cell, parent);
            var styles         = window.getComputedStyle(valueContainer);
            var text           = this.bestFittingTextWithStyleInWidth(allValues, styles, parentWidth);
            valueContainers.push(valueContainer);
            texts.push(text);
        }, this);

        valueContainers.each(function(valueContainer, i){
            valueContainer.innerHTML = texts[i];
        }, this);
    },

    // Returns the text which fits best
    bestFittingTextWithStyleInWidth: function(textArray, styles, width) {
        var ctx  = this.measurementCanvas.getContext("2d");
        ctx.font = this.canvasFontFromCSSStyles(styles);
        var padding = this.horizontalPaddingFromCSSStyles(styles);
        var text = '';
        var count = textArray.length;
        for(var i=0; i<count; i++){
            text = textArray[i];
            if(ctx.measureText(text).width+padding < width)
                 break;
        }
        return text;
   },

   canvasFontFromCSSStyles: function(styles){
        var font = '';
        var fontStyle = styles['font-style'];
        var fontWeight = styles['font-weight'];
        var fontSize = styles['font-size'];
        var fontFamily = styles['font-family'];
        if(fontStyle)
            font += fontStyle+' ';
        if(fontWeight)
            font += fontWeight+' ';
        if(fontSize)
            font += fontSize+' ';
        if(fontFamily)
            font += fontFamily;
        return font;
   },

   horizontalPaddingFromCSSStyles: function(styles){
        var padding = 0.0;
        var paddingLeft = styles['padding-left'];
        var paddingRight = styles['padding-right'];
        if(paddingLeft)
            padding += paddingLeft.toFloat();
        if(paddingRight)
            padding += paddingRight.toFloat();
        return padding;
   },

    // For all columns without a width, the typical value string is used to determine a width.
    // The computed width is then applied to the header col and outline table col elements.
   adjustColumnWidths: function(){
        var autoresizingStyle = this.options.columnAutoresizingStyle;
        var allHeaderCols = this.outlineElement.getElements('.header col');
        allHeaderCols.each(function(headerCol, index){
            if(!headerCol.style['width'].toInt()){

                var identifier   = headerCol.getProperty('data-identifier');
                var outlineCol   = this.table.getElement('col[data-identifier='+identifier+']')
                var outlineCells = this.table.getElements('tr td:nth-child('+(index+1)+')');
                var headerCell   = this.outlineElement.getElement('tr:first-child td:nth-child('+(index+1)+')');
                var width = this.proposedColumnWidth(outlineCol, outlineCells, headerCell, identifier);

                var widthInPixel = ''+width+'px';
                headerCol.setStyle('width', widthInPixel);
                outlineCol.setStyle('width', widthInPixel);
            }
        }, this);

        // TODO: support more autoresizingStyles
        if(autoresizingStyle == WBLOutlineAutoresizingStyle.FirstColumnOnlyAutoresizingStyle)
        {
            allHeaderCols[0].setStyle('width', '100%');
            this.table.getElements('col')[0].setStyle('width', '100%');
        }
    },


    // The width is computed by taking into account the header text and the typical value text.
    proposedColumnWidth: function(outlineColumn, outlineCells, headerCell, identifier){

        // Compute min and max width:
        var headerStyles = window.getComputedStyle(headerCell);
        var minWidth = headerStyles['min-width'].toInt();
        var maxWidth = headerStyles['max-width'].toInt();
        var nonTextWidthString = outlineColumn.getProperty('data-non-text-width');
        var nonTextWidth = nonTextWidthString ? parseFloat(nonTextWidthString) : 0;

        // Note: the max width provided on by element is only used in drag operations (headerStyles['max-width'].toInt())
        var typicalValue = outlineColumn.getProperty('data-typical-value');

        // Get the proposed header content width:
        var proposedHeaderWidth = 2 + this.getMaxWidthFromElements(headerCell.getChildren(), headerStyles, null, identifier);

        // Get the proposed cell content width:
        var proposedCellWidth = 0;

        if(outlineCells && outlineCells.length > 0){
            var cells = this.options.complexColumnWidthComputation ? outlineCells : [outlineCells[0]];
            cells.each(function(cell){
                var outlineCellStyles = window.getComputedStyle(cell);
                var cellWidth = nonTextWidth + this.getMaxWidthFromElements(cell.getChildren(), outlineCellStyles, typicalValue, identifier);
                if(cellWidth > proposedCellWidth)
                    proposedCellWidth = cellWidth;
            }, this);
        }

        var width = Math.max(proposedHeaderWidth, proposedCellWidth);

        // Limit the width to min and max values
        if(width < minWidth)
            width = minWidth;
        else if(maxWidth != 0 && width > maxWidth)
            width = maxWidth;

        return width;
    },


    // Returns the maximum width of the given elements. Text is treated as single line text.
    getMaxWidthFromElements: function(elements, styles, replacementText, identifier){

        var ctx  = this.measurementCanvas.getContext("2d");
        ctx.font = this.canvasFontFromCSSStyles(styles);
        var padding = this.horizontalPaddingFromCSSStyles(styles);

        // Gather all spans:
        var spans = [];
        elements.each(function(element){
            if(element.get('tag') == 'span')
                spans.push(element);
            element.getElements('span').each(function(span){
                spans.push(span);
            });
        }, this);

        // Get the max width of the widest element:
        var width = 0;
        var lastIndex = spans.length-1;

        var text;
        for(var i=0; i<=lastIndex; i++){
            span = spans[i];

            // If long text should be hidden there is no need to measure it,
            // because those cells contain an image too.
            if(span.getProperty('data-hide-long-text'))
                return 0.0; // Text can be invisble so return 0.

            text = span.get('text');

            // Replace the text inside the last span if needed:
            if(replacementText && lastIndex == i)
                text = replacementText;

            var spanWidth = ctx.measureText(text).width+padding;
            if( spanWidth > width)
                width = spanWidth;
        }

        return width+padding;
    },

    rows: function(){
        return this.table ? this.table.getElements('.row') : [];
    },

    rowAtIndex: function(index){
        if(this.table) {
            var rows = this.table.getElements('.row[data-idx="'+index+'"]');
            return rows.length > 0 ? rows[0] : null;
        }
        return null;
    },

    clearRowSelection: function(){
        var selectedRows = Array.clone(this.selectedRows);
        selectedRows.each(function(row){
            this.removeSelectionStyleFromRow(row);
        }, this);
    },

    computeAnchorIndexFromSelection: function(){
       this.setAnchorIndex(null);
       this.rows().each(function(row){
           if(row.hasClass(this.classOfSelectedRows)){
               this.setAnchorIndex(row.index);
               return;
           }
       }, this);
    },

    // The setter makes sure the index is in a valid range
    setAnchorIndex: function(index){
        var visibleRows = this.rows();
        var firstRowIndex = 0;
        var lastRowIndex  = 0;
        if(visibleRows.length > 0){
            firstRowIndex = visibleRows[0].index;
            lastRowIndex = visibleRows.getLast().index;
        }

        if(index < firstRowIndex)
            index = firstRowIndex;
        else
        {
            if(index > lastRowIndex)
                index = lastRowIndex;
        }
        this.anchorIndex = index;
    },

    selectSingleRow: function(row){
        this.clearRowSelection();
        this.addSelectionStyleToRow(row);
        this.setAnchorIndex(row.index);
    },

    selectUpToRow: function(row){
        var index = row.index;
        if(this.anchorIndex == null){
            this.addSelectionStyleToRow(row);
            this.setAnchorIndex(index);
        }else {
            this.clearRowSelection();
            var minIndex = Math.min(this.anchorIndex, index);
            var maxIndex = Math.max(this.anchorIndex, index);
            var rows = this.flatRows;
            for(var i=minIndex; i<=maxIndex; i++)
            {
                var row = rows[i];
                if(row.isVisible)
                    this.addSelectionStyleToRow(row);
            }
        }
    },

    toggleSelectionOfRow: function(row){
        var selClass = this.classOfSelectedRows;
        if(row.hasClass(selClass)){
            this.removeSelectionStyleFromRow(row);
            this.computeAnchorIndexFromSelection();
        }else{
            this.addSelectionStyleToRow(row);
            this.setAnchorIndex(row.index);
        }
    },

    selectedClassProperty: 'data-selected-class',
    notSelectedClassProperty: 'data-not-selected-class',

    addSelectionStyleToRow: function(row){
        if(!this.selectedRows.contains(row))
            this.selectedRows.push(row);

        // Replace the content of the 'class' property  by the content of the 'data-selected-class' property if available.
        // (on the row and any element below with this property).
        var rowSelectedClass = row.getProperty(this.selectedClassProperty);
        var changed = false;
        if(rowSelectedClass){
            if(this.applySelectionStyleToElement(row))
                changed = true;
        }
        if(changed)
            this.selectionStyleDidChangeInRow(row);
        row.addClass(this.classOfSelectedRows);
    },

    removeSelectionStyleFromRow: function(row){
        this.selectedRows.erase(row);

        var rowSelectedClass = row.getProperty(this.selectedClassProperty);

        if(rowSelectedClass && this.applyNormalStyleToElement(row))
            this.selectionStyleDidChangeInRow(row);

        row.removeClass(this.classOfSelectedRows);
    },

    selectionStyleDidChangeInRow: function(r){
        this.changedRows.push({row:r, index:r.index});
    },

    applySelectionStyleToElement: function(element){
        var origClass = element.getProperty('class');
        var selClass = element.getProperty(this.selectedClassProperty);
        if(!element.isNotSelectedClass)
            this.cacheNotSelectedClassInElement(element, origClass);

        if(origClass != selClass){
            element.setProperty('class', selClass);
            return true;
        }
        return false;
    },

    applyNormalStyleToElement: function(element){
        var newClass = element.isNotSelectedClass;
        if(newClass){
            var classOfCollapsedRows = this.classOfCollapsedRows;
            if(element.getProperty('class') != newClass){
                // adjust the collapsed state
                if(element.hasClass(classOfCollapsedRows)){
                    if(!newClass.contains(classOfCollapsedRows))
                        newClass += " "+classOfCollapsedRows;
                }
                element.setProperty('class', newClass);
                return true;
            }
        }
        return false;
    },


    selectionEventInRow: function(row, event){
        this.changedRows = new Array();

        // Shift the keyboard focus to this outline view:
        var withKeyboard = event.type == 'keydown';
        var selectedRows = this.selectedRows;
        if(!withKeyboard) // Mouse events
            this.keyboard.activate();
        if(event.meta){
            if(selectedRows.length > 0 && !this.options.allowsMultipleSelection && !selectedRows.contains(row))
                return;
            this.toggleSelectionOfRow(row);
        }else if(event.shift && this.options.allowsMultipleSelection)
            this.selectUpToRow(row);
        else
            this.selectSingleRow(row);
        this.updateSelectionBorders();
        this.selectionStyleDidChangeInRowsWithIndex(this.changedRows);
        event.preventDefault();
    },

    selectedRowIndices: function(){
        var indices = [];
        var count = this.selectedRows.length;
        for(var i=0; i<count; i++){
            var row = this.selectedRows[i];
            if(row.index !== undefined) // Only outline rows have an index attached (in contrast to gantt rows)
                indices.push(row.index);
        }
        return indices;
    },

    selectedRowIndicesAsJSONString: function(){
        var result = '';
        var indices = this.selectedRowIndices();
        var count = indices.length;
        for(var i=0; i<count; i++){
            result += indices[i] + ',';
        }
        if(result.length)
            result = result.substring(0, result.length-1);
        return result;
    },

    // Can be overwritten in subclasses
    selectionStyleDidChangeInRowsWithIndex: function(rowsWithIndex){
        if(this.options.selectionChangedAction){
            this.disposeSelectionToServerTimer();

            // We wait one second from the last selection change before we send the selection state to the server:
            this.selectionToServerTimer = this.sendSelectionToServer.delay(1000, this);
        }
    },

    disposeSelectionToServerTimer: function(){
        clearTimeout(this.selectionToServerTimer);
        this.selectionToServerTimer = undefined;
    },

    sendSelectionToServer: function(){
        if(this.options.selectionChangedAction){
            this.disposeSelectionToServerTimer();
            this.options.selectionChangedAction({'indices':this.selectedRowIndicesAsJSONString()});
        }
    },

    sendSelectionToServerIfNeeded: function(){
        if(this.options.selectionChangedAction && this.selectionToServerTimer)
            this.sendSelectionToServer();
    },

    addSelectionTopBorderToRows: function(rows){
        Array.each(rows, function(row){
            row.addClass('selectionTop');
        });
    },

    addSelectionBottomBorderToRows: function(rows){
        Array.each(rows, function(row){
            row.addClass('selectionBottom');
        });
    },

    removeSelectionTopBorderFromRows: function(rows){
        Array.each(rows, function(row){
            row.removeClass('selectionTop');

        });
    },

    removeSelectionBottomBorderFromRows: function(rows){
        Array.each(rows, function(row){
            row.removeClass('selectionBottom');
        });
    },

    // Returns an array with rows sorted by index.
    sortedRows: function(rows){
        rows = Array.clone(rows);
        rows.sort(function(rowA, rowB){
            var aIndex = rowA.index;
            var bIndex = rowB.index;
            if (aIndex < bIndex)
              return -1;
            if (aIndex > bIndex)
              return 1;
            return 0;
        });
        return rows;
    },

    updateSelectionBorders: function(){
        var selectedOutlineRows = this.sortedRows(this.selectedRows);
        var visibleOutlineRows = this.rows();

        var previousRowsWithSelection = [];
        visibleOutlineRows.each(function(row){
            if(row.hasClass('selectionTop') || row.hasClass('selectionBottom'))
                previousRowsWithSelection.push(row);
        });

        if(selectedOutlineRows.length > 0){

            var count = visibleOutlineRows.length;

            var findVisibleIndexForRow = function(row, startIndex){
                var rowIndex = row.index;
                for(var i=startIndex; i<count; i++){
                    var iRow = visibleOutlineRows[i];
                    if(rowIndex == iRow.index)
                        return i;
                }
                console.log('Error: selected row is not visible')
            }

            var firstIndex = findVisibleIndexForRow(selectedOutlineRows[0], 0);
            var lastIndex  = findVisibleIndexForRow(selectedOutlineRows.getLast(), firstIndex);

            var selClass = 'selected';
            var previousRowIsSelected = false;
            var previousRow = null;

            count = lastIndex+1-firstIndex;
            for(var i=firstIndex; i<=lastIndex; i++)
            {
                var row = visibleOutlineRows[i];
                previousRowsWithSelection.erase(row);
                var rows = [row];

                var isSelected = row.hasClass(selClass);
                if(!isSelected){
                    this.removeSelectionTopBorderFromRows(rows);
                    this.removeSelectionBottomBorderFromRows(rows);
                }
                if(!previousRowIsSelected && isSelected)
                    this.addSelectionTopBorderToRows(rows);
                else if(!isSelected && previousRowIsSelected)
                    this.addSelectionBottomBorderToRows([previousRow]);
                else if(isSelected && previousRowIsSelected) {
                    this.removeSelectionBottomBorderFromRows([previousRow]);
                    this.removeSelectionTopBorderFromRows(rows);
                }

                if(isSelected && i == lastIndex){
                    this.addSelectionBottomBorderToRows(rows);
                }

                previousRowIsSelected = isSelected;
                previousRow = row;
            }
        }

        // Remove selection from previously selected rows
        previousRowsWithSelection.each(function(row){
            row.removeClass('selectionTop');
            row.removeClass('selectionBottom');
        });
    },

    clickCount: 0,

    singleClick: function(row, event){
//        console.log('singleClick');
    },

    doubleClick: function(row, event){
        if(this.options.doubleClickAction){
            this.sendSelectionToServer(); // Amke sure the selection is immediately pushed to the server.
            this.options.doubleClickAction({'rowIndex':row.index});
        }
    },

    makeTableRowsSelectable: function(){

        this.selectedRows = new Array();
        var selClass = this.classOfSelectedRows;
        this.prepareRowsWithServerSideSelection();

        var me = this;
        this.flatRows.each(function(row){
            row.index = row.getProperty('data-idx').toInt(); // Used in computeAnchorIndexFromSelection()
            row.addEvent('mousedown', function(event){
                me.lastRowIndexSelectedWithShift = undefined;
                me.selectionEventInRow(row, event);
                me.registerUserInteraction();
                event.stop();
            });
            row.addEvent('click', function(event) {
                me.clickCount++;
                if (me.clickCount === 1) {
                    me.singleClickTimer = setTimeout(function() {
                        me.clickCount = 0;
                        me.singleClick(row, event);
                    }, 400);
                } else if (me.clickCount === 2) {
                    clearTimeout(me.singleClickTimer);
                    me.clickCount = 0;
                    me.doubleClick(row, event);
                }
                me.registerUserInteraction();
                event.stop();
            }, false);

        });
        this.updateSelectionBorders();
        this.computeAnchorIndexFromSelection();

        if(this.options.selectionChangedAction){
            // Get notified before an action menu is opened, so we can push the selection to the server if needed.
            // This is needed, because in order to validate actions the server needs a valid selection.
            if(window.MooDropMenu)
                window.parent.eventBus().addObserver(MooDropMenu.WillShowMenuNotification, this.sendSelectionToServerIfNeeded, this);
        }
    },

    prepareDisclosureButtons: function(){
        // Add event handlers to disclosure buttons.
        this.flatRows.each(function(row){
            var button = row.getElement('.disclosure');
            if(button){
                var me = this;
                // Note: button.toggleExpansionState can be called from unit tests as well
                button.toggleExpansionState = function(event){
                    me.toggleRowExpansionState(row);
                    me.registerUserInteraction();
                    if(event)
                        event.stop();
                };
                button.addEvent('mousedown', function(event){
                    button.toggleExpansionState(event);
                });
            }
        }, this);
    },

    // Used in unit tests:
    toggleRowExpansionStateForRowAtIndex: function(index){
        this.toggleRowExpansionState(this.rowAtIndex(index));
    },

    toggleRowExpansionState: function(row){
        if(this.options.clientSideCollapsingAndExpanding)
            this.toggleRowExpansionStateOnClientSideOnly(row);
        else if(this.options.toggleIsExpandedAction) {
            if(this.options.selectionChangedAction)
                this.sendSelectionToServerIfNeeded();
            this.options.toggleIsExpandedAction({'rowIndex':row.index});
        }
    },

    toggleRowExpansionStateOnClientSideOnly: function(row){
        var cssClass = this.classOfCollapsedRows;
        if(!row.hasClass(cssClass))
            this.addCollapsedStatusToRow(row);
        else
            this.removeCollapsedStatusFromRow(row);

        this.updateRowsVisibility(this.flatRowsStartingWithRow(row));
    },

    isRowCollapsed: function(row){
        return row.hasClass(this.classOfCollapsedRows);
    },

    expandRowsOnClientSideOnly: function(rows){
        if(rows.length > 0){
            var affectedRows = [];
            rows.each(function(row){
                this.removeCollapsedStatusFromRow(row);
                affectedRows.append(this.flatRowsStartingWithRow(row));
            }, this);
            this.updateRowsVisibility(affectedRows);
        }
    },

    collapseRowsOnClientSideOnly: function(rows){
        if(rows.length > 0){
            var affectedRows = [];
            rows.each(function(row){
                this.addCollapsedStatusToRow(row);
                affectedRows.append(this.flatRowsStartingWithRow(row));
            }, this);
            this.updateRowsVisibility(affectedRows);
        }
    },

    addCollapsedStatusToRow: function(row){
        var classOfCollapsedRows = this.classOfCollapsedRows;

        // Add the class to the "class" property
        row.addClass(classOfCollapsedRows);

        // Add the class to the selected and not selected properties as well
        var selectedProperty = this.selectedClassProperty;
        var notSelectedProperty = this.notSelectedClassProperty;
        var selectedClass = row.getProperty(selectedProperty);
        var notSelectedClass = row.getProperty(notSelectedProperty);

        if(selectedClass && !selectedClass.contains(classOfCollapsedRows)){
            selectedClass += " "+classOfCollapsedRows;
            row.setProperty(selectedProperty, selectedClass);
            row.selectedClass = selectedClass;
        }
        if(notSelectedClass && !notSelectedClass.contains(classOfCollapsedRows)){
            notSelectedClass += " "+classOfCollapsedRows;
            row.setProperty(notSelectedProperty, notSelectedClass);
            row.notSelectedClass = notSelectedClass;
        }
    },

    removeCollapsedStatusFromRow: function(row){
        var classOfCollapsedRows = this.classOfCollapsedRows;

        // Remove the class from the "class" property
        row.removeClass(classOfCollapsedRows);

        var selectedProperty = this.selectedClassProperty;
        var notSelectedProperty = this.notSelectedClassProperty;
        var selectedClass = row.getProperty(selectedProperty);
        var notSelectedClass = row.getProperty(notSelectedProperty);
        if(selectedClass){
            selectedClass = selectedClass.replace(/isCollapsed/g, '');
            row.setProperty(selectedProperty, selectedClass);
            row.selectedClass = selectedClass;
        }
        if(notSelectedClass){
            notSelectedClass = notSelectedClass.replace(/isCollapsed/g, '');
            row.setProperty(notSelectedProperty, notSelectedClass);
            row.notSelectedClass = notSelectedClass;
        }
    },


    // Fills the normal css classes cache of elements with a server side selection.
    prepareRowsWithServerSideSelection: function(){
        var selectedClassProperty = this.selectedClassProperty;
        var notSelectedClassProperty = this.notSelectedClassProperty;
        this.table.getElements('['+notSelectedClassProperty+']').each(function(element){
            this.cacheNotSelectedClassInElement(element, element.getProperty(notSelectedClassProperty));
               element.setProperty(selectedClassProperty, element.getProperty('class'));
        }, this);

        // Initially fill the selected rows array:
        this.selectedRows = this.table.getElements('.row.selected');
    },

    cacheNotSelectedClassInElement: function(element, notSelectedClass){
        notSelectedClass = notSelectedClass.replace(/isCollapsed/g, '');
        element.isNotSelectedClass = notSelectedClass;
    },

    selectPreviousRow: function(event){
        var rows = this.flatRows;
        var count = rows.length;
        if(!event.shift){
            if(this.lastRowIndexSelectedWithShift  != undefined)
                this.setAnchorIndex(this.lastRowIndexSelectedWithShift);
            this.lastRowIndexSelectedWithShift = undefined;
        }
        var anchorIndex = this.anchorIndex;
        if(anchorIndex == undefined)
            anchorIndex = count;
        var startIndex = this.lastRowIndexSelectedWithShift != undefined ? this.lastRowIndexSelectedWithShift : anchorIndex;
        var index = this.getPreviousSelectableRowIndex(startIndex);
        var row = rows[index];
        this.selectionEventInRow(row, event);
        if(event.shift)
            this.lastRowIndexSelectedWithShift = row.index;
        this.scrollRowIntoVisibleArea(row);
    },

    getPreviousSelectableRowIndex: function(startIndex){
        var flatRows = this.flatRows;
        var currentRow = this.flatRows[startIndex];
        for(var i=startIndex-1; i >= 0; i--){
            if(flatRows[i].isVisible)
                return i;
        }
        return startIndex;
    },

    selectNextRow:function(event){
        var rows = this.flatRows;
        var count = rows.length;
        if(!event.shift){
            if(this.lastRowIndexSelectedWithShift != undefined)
                this.setAnchorIndex(this.lastRowIndexSelectedWithShift);
            this.lastRowIndexSelectedWithShift = undefined;
        }
        var anchorIndex = this.anchorIndex;
        if(anchorIndex == undefined)
            anchorIndex = -1;

        var startIndex = this.lastRowIndexSelectedWithShift != undefined ? this.lastRowIndexSelectedWithShift : anchorIndex;
        var index = this.getNextSelectableRowIndex(startIndex);
        var row = rows[index];
        this.selectionEventInRow(row, event);
        if(event.shift)
            this.lastRowIndexSelectedWithShift = index;
        this.scrollRowIntoVisibleArea(row);
    },

    getNextSelectableRowIndex: function(startIndex){
        var flatRows = this.flatRows;
        var count = flatRows.length;
        var currentRow = this.flatRows[startIndex];
        for(var i=startIndex+1; i < count; i++){
            if(flatRows[i].isVisible)
                return i;
        }
        return startIndex;
    },

    //ak
    scrollRowIntoVisibleArea: function(row){

        var me = this;
        var scroll = function(){

//            var watch = new PWStopWatch(true);
            var scrollElement = me.outlineElement.scrollViewController.scrollElement();

            var oTop    = scrollElement.scrollTop; // This is expensive in FF with 1000 rows -> 45ms
            var oBottom = oTop + scrollElement.getSize().y;
            var rTop    = row.offsetTop;
            var rBottom = rTop + row.offsetHeight;

            if(    (rTop < oTop || rBottom > oBottom)
               && !(rTop < oTop && rBottom > oBottom)) {
                if(rTop < oTop)
                    scrollElement.pwScrollToTop(rTop);
                else if (rBottom > oBottom)
                    scrollElement.pwScrollToTop(rBottom-(oBottom-oTop));
            }

//            watch.logDuration('scrollRowIntoVisibleArea');
        };

        scroll.delay(100);
//        scroll();
    }

 });

