/*
 * Copyright 2011 JBoss Inc
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.drools.guvnor.client.widgets.decoratedgrid;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;

import org.drools.guvnor.client.messages.Constants;
import org.drools.guvnor.client.resources.DecisionTableResources;
import org.drools.guvnor.client.resources.DecisionTableResources.DecisionTableStyle;
import org.drools.guvnor.client.widgets.decoratedgrid.CellValue.CellState;
import org.drools.guvnor.client.widgets.decoratedgrid.CellValue.GroupedCellValue;
import org.drools.guvnor.client.widgets.decoratedgrid.data.Coordinate;
import org.drools.guvnor.client.widgets.decoratedgrid.data.DynamicData;
import org.drools.guvnor.client.widgets.decoratedgrid.data.DynamicDataRow;
import org.drools.guvnor.client.widgets.decoratedgrid.data.GroupedDynamicDataRow;

import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Widget;

/**
 * An abstract grid of data. Implementations can choose the orientation to
 * render "rows" and "columns" (e.g. some may transpose the normal meaning to
 * provide a horizontal implementation of normally vertical tabular data)
 */
public abstract class MergableGridWidget<T> extends Widget
    implements
    ValueUpdater<Object>,
    HasSelectedCellChangeHandlers,
    HasRowGroupingChangeHandlers {

    /**
     * Container for a details of a selected cell
     */
    public static class CellSelectionDetail {

        private Coordinate c;
        private int        offsetX;
        private int        offsetY;
        private int        height;
        private int        width;

        CellSelectionDetail(Coordinate c,
                            int offsetX,
                            int offsetY,
                            int height,
                            int width) {
            this.c = c;
            this.offsetX = offsetX;
            this.offsetY = offsetY;
            this.height = height;
            this.width = width;
        }

        public int getHeight() {
            return height;
        }

        public int getOffsetX() {
            return offsetX;
        }

        public int getOffsetY() {
            return offsetY;
        }

        public int getWidth() {
            return width;
        }

        public Coordinate getCoordinate() {
            return c;
        }

    }

    // Enum to support keyboard navigation
    public enum MOVE_DIRECTION {
        LEFT, RIGHT, UP, DOWN, NONE
    }

    //GWT disable text selection in an HTMLTable. 
    //event.stopPropogation() doesn't prevent text selection
    private native static void disableTextSelectInternal(Element e,
                                                         boolean disable)/*-{
		if (disable) {
			e.ondrag = function() {
				return false;
			};
			e.onselectstart = function() {
				return false;
			};
			e.style.MozUserSelect = "none"
		} else {
			e.ondrag = null;
			e.onselectstart = null;
			e.style.MozUserSelect = "text"
		}
    }-*/;

    // Selections store the actual grid data selected (irrespective of
    // merged cells). So a merged cell spanning 2 rows is stored as 2
    // selections. Selections are ordered by row number so we can
    // iterate top to bottom.
    protected TreeSet<CellValue< ? extends Comparable< ? >>> selections                 = new TreeSet<CellValue< ? extends Comparable< ? >>>(
                                                                                                                                              new Comparator<CellValue< ? extends Comparable< ? >>>() {

                                                                                                                                                  public int compare(CellValue< ? extends Comparable< ? >> o1,
                                                                                                                                                                     CellValue< ? extends Comparable< ? >> o2) {
                                                                                                                                                      return o1.getPhysicalCoordinate().getRow()
                                                                                                                                                             - o2.getPhysicalCoordinate().getRow();
                                                                                                                                                  }

                                                                                                                                              } );

    // TABLE elements
    protected TableElement                                   table;

    protected TableSectionElement                            tbody;

    // Resources
    protected static final Constants                         messages                   = GWT.create( Constants.class );
    protected static final DecisionTableResources            resource                   = GWT.create( DecisionTableResources.class );
    protected static final DecisionTableStyle                style                      = resource.cellTableStyle();

    private static final ImageResource                       selectorGroupedCells       = resource.collapse();
    private static final ImageResource                       selectorUngroupedCells     = resource.expand();
    protected static final String                            selectorGroupedCellsHtml   = makeImageHtml( selectorGroupedCells );
    protected static final String                            selectorUngroupedCellsHtml = makeImageHtml( selectorUngroupedCells );

    private static String makeImageHtml(ImageResource image) {
        return AbstractImagePrototype.create( image ).getHTML();
    }

    // Data and columns to render
    protected List<DynamicColumn<T>> columns              = new ArrayList<DynamicColumn<T>>();
    protected DynamicData            data                 = new DynamicData();

    //Properties for multi-cell selection
    protected CellValue< ? >         rangeOriginCell;
    protected CellValue< ? >         rangeExtentCell;

    protected MOVE_DIRECTION         rangeDirection       = MOVE_DIRECTION.NONE;
    protected boolean                bDragOperationPrimed = false;

    /**
     * A grid of cells.
     */
    public MergableGridWidget() {
        style.ensureInjected();

        // Create some elements to contain the grid
        table = Document.get().createTableElement();
        tbody = Document.get().createTBodyElement();
        table.setClassName( style.cellTable() );
        table.setCellPadding( 0 );
        table.setCellSpacing( 0 );
        setElement( table );

        table.appendChild( tbody );

        // Events in which we're interested (note, if a Cell<?> appears not to
        // work I've probably forgotten some events. Might be a better way of
        // doing this, but I copied CellTable<?, ?>'s lead
        sinkEvents( Event.getTypeInt( "click" )
                    | Event.getTypeInt( "dblclick" )
                    | Event.getTypeInt( "mousedown" )
                    | Event.getTypeInt( "mouseup" )
                    | Event.getTypeInt( "mousemove" )
                    | Event.getTypeInt( "mouseout" )
                    | Event.getTypeInt( "change" )
                    | Event.getTypeInt( "keypress" )
                    | Event.getTypeInt( "keydown" ) );

        //Prevent text selection
        disableTextSelectInternal( table,
                                   true );

    }

    /**
     * Add a handler for RowGroupingChangeEvents
     */
    public HandlerRegistration addRowGroupingChangeHandler(RowGroupingChangeHandler handler) {
        return addHandler( handler,
                           RowGroupingChangeEvent.getType() );
    }

    /**
     * Add a handler for SelectedCellChangeEvents
     */
    public HandlerRegistration addSelectedCellChangeHandler(SelectedCellChangeHandler handler) {
        return addHandler( handler,
                           SelectedCellChangeEvent.getType() );
    }

    /**
     * Delete a column
     * 
     * @param column
     *            Column to delete
     * @param bRedraw
     *            Should grid be redrawn
     */
    public void deleteColumn(DynamicColumn<T> column,
                             boolean bRedraw) {

        //Find index of column
        int index = columns.indexOf( column );
        if ( index == -1 ) {
            throw new IllegalArgumentException(
                                                "Column not found in declared columns." );
        }

        //Expand any merged cells in colum
        boolean bRedrawSidebar = false;
        for ( int iRow = 0; iRow < data.size(); iRow++ ) {
            CellValue< ? > cv = data.get( iRow ).get( index );
            if ( cv.isGrouped() ) {
                removeModelGrouping( cv,
                                     false );
                bRedrawSidebar = true;
            }
        }

        // Clear any selections
        clearSelection();

        // Delete column from grid
        columns.remove( index );
        reindexColumns();

        data.deleteColumn( index );

        // Redraw
        if ( bRedraw ) {
            redraw();
            if ( bRedrawSidebar ) {
                RowGroupingChangeEvent.fire( this );
            }
        }

    }

    /**
     * Delete the given row. Partial redraw.
     * 
     * @param row
     */
    public void deleteRow(DynamicDataRow row) {

        //Find index of row
        int index = data.indexOf( row );
        if ( index == -1 ) {
            throw new IllegalArgumentException(
                                                "DynamicDataRow does not exist in table data." );
        }

        // Clear any selections
        clearSelection();

        //Delete row data
        data.deleteRow( index );

        // Partial redraw
        if ( !data.isMerged() ) {
            // Single row when not merged
            removeRowElement( index );
        } else {
            // Affected rows when merged
            removeRowElement( index );

            if ( data.size() > 0 ) {
                int minRedrawRow = findMinRedrawRow( index - 1 );
                int maxRedrawRow = findMaxRedrawRow( index - 1 ) + 1;
                if ( maxRedrawRow > data.size() - 1 ) {
                    maxRedrawRow = data.size() - 1;
                }
                redrawRows( minRedrawRow,
                                       maxRedrawRow );
            }
        }

    }

    /**
     * Return grid's columns
     * 
     * @return columns
     */
    public List<DynamicColumn<T>> getColumns() {
        return columns;
    }

    /**
     * Return grid's data. Grouping reflected in the UI will be collapsed in the
     * return value. Use of <code>getFlattenedData()</code> should be used in
     * preference if the ungrouped data is needed (e.g. when persisting the
     * model).
     * 
     * @return data
     */
    public DynamicData getData() {
        return data;
    }

    /**
     * Return an immutable list of selected cells
     * 
     * @return The selected cells
     */
    public List<CellValue< ? >> getSelectedCells() {
        return Collections.unmodifiableList( new ArrayList<CellValue< ? >>( this.selections ) );
    }

    /**
     * Insert a column before another
     * 
     * @param columnBefore
     *            The column before which the new column should be inserted
     * @param newColumn
     *            Column definition
     * @param columnData
     *            Data for column
     * @param bRedraw
     *            Should grid be redrawn
     */
    public void insertColumnBefore(DynamicColumn<T> columnBefore,
                                   DynamicColumn<T> newColumn,
                                   List<CellValue< ? extends Comparable< ? >>> columnData,
                                   boolean bRedraw) {

        if ( newColumn == null ) {
            throw new IllegalArgumentException( "newColumn cannot be null" );
        }
        if ( columnData == null ) {
            throw new IllegalArgumentException( "columnData cannot be null" );
        }
        if ( columnData.size() != data.size() ) {
            throw new IllegalArgumentException( "columnData contains a different number of rows to the grid" );
        }

        //Find index of new column
        int index = columns.size();
        if ( columnBefore != null ) {
            index = columns.indexOf( columnBefore );
            if ( index == -1 ) {
                throw new IllegalArgumentException(
                                                    "columnBefore does not exist in table data." );
            }
            index++;
        }

        // Clear any selections
        clearSelection();

        // Add column definition
        columns.add( index,
                     newColumn );
        reindexColumns();

        data.addColumn( index,
                        columnData,
                        newColumn.isVisible() );

        // Redraw
        if ( bRedraw ) {
            redrawColumns( index,
                           columns.size() - 1 );
        }

    }

    /**
     * Insert the given row before the provided index. Partial redraw.
     * 
     * @param rowBefore
     *            The row before which the new row should be inserted
     * @param rowData
     *            The row of data to insert
     */
    public DynamicDataRow insertRowBefore(DynamicDataRow rowBefore,
                                          List<CellValue< ? extends Comparable< ? >>> rowData) {

        if ( rowData == null ) {
            throw new IllegalArgumentException( "Row data cannot be null" );
        }
        if ( rowData.size() != columns.size() ) {
            throw new IllegalArgumentException( "rowData contains a different number of columns to the grid" );
        }

        //Find index of row
        int index = data.size();
        if ( rowBefore != null ) {
            index = data.indexOf( rowBefore );
            if ( index == -1 ) {
                throw new IllegalArgumentException(
                                                    "rowBefore does not exist in table data." );
            }
        }

        // Clear any selections
        clearSelection();

        // Find rows that need to be (re)drawn
        int minRedrawRow = index;
        int maxRedrawRow = index;
        if ( data.isMerged() ) {
            if ( index < data.size() ) {
                minRedrawRow = findMinRedrawRow( index );
                maxRedrawRow = findMaxRedrawRow( index ) + 1;
            } else {
                minRedrawRow = findMinRedrawRow( (index > 0 ? index - 1 : index) );
                maxRedrawRow = index;
            }
        }

        DynamicDataRow row = data.addRow( index,
                                          rowData );

        // Partial redraw
        if ( !data.isMerged() ) {
            // Only new row when not merged
            createRowElement( index,
                              row );
        } else {
            // Affected rows when merged
            createEmptyRowElement( index );
            redrawRows( minRedrawRow,
                        maxRedrawRow );
        }
        return row;

    }

    /**
     * Redraw the whole table
     */
    public abstract void redraw();

    /**
     * Redraw table column. Partial redraw
     * 
     * @param index
     *            Start column index (inclusive)
     */
    public abstract void redrawColumn(int index);

    /**
     * Redraw table columns. Partial redraw
     * 
     * @param startRedrawIndex
     *            Start column index (inclusive)
     * @param endRedrawIndex
     *            End column index (inclusive)
     */
    public abstract void redrawColumns(int startRedrawIndex,
                                       int endRedrawIndex);

    /**
     * Toggle the state of DecoratedGridWidget merging.
     * 
     * @return The state of merging after completing this call
     */
    public boolean toggleMerging() {
        if ( !data.isMerged() ) {
            clearSelection();
            data.setMerged( true );
            redraw();
        } else {
            clearSelection();
            data.setMerged( false );
            redraw();
            RowGroupingChangeEvent.fire( this );
        }
        return data.isMerged();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.gwt.cell.client.ValueUpdater#update(java.lang.Object)
     */
    public void update(Object value) {

        boolean bUngroupCells = false;
        Coordinate selection = selections.first().getCoordinate();

        //If selections span multiple cells, any of which are grouped we should ungroup them
        if ( selections.size() > 1 ) {
            for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
                if ( cell instanceof GroupedCellValue ) {
                    bUngroupCells = true;
                    break;
                }
            }
        }

        // Update underlying data (update before ungrouping as selections would need to be expanded too)
        for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
            Coordinate c = cell.getCoordinate();
            if ( !columns.get( c.getCol() ).isSystemControlled() ) {
                data.set( c,
                          value );
            }
            if ( value != null ) {
                cell.removeState( CellState.OTHERWISE );
            }
        }

        //Ungroup if applicable
        if ( bUngroupCells ) {
            for ( CellValue< ? extends Comparable< ? >> cell : selections ) {
                if ( cell instanceof GroupedCellValue ) {
                    removeModelGrouping( cell,
                                         true );
                }
            }
        }

        // Partial redraw
        int baseRowIndex = selections.first().getCoordinate().getRow();
        int minRedrawRow = findMinRedrawRow( baseRowIndex );
        int maxRedrawRow = findMaxRedrawRow( baseRowIndex );

        // When merged cells become unmerged (if their value is
        // cleared need to ensure the re-draw range is at least
        // as large as the selection range
        if ( maxRedrawRow < selections.last().getCoordinate().getRow() ) {
            maxRedrawRow = selections.last().getCoordinate().getRow();
        }
        redrawRows( minRedrawRow,
                    maxRedrawRow );

        //Re-select applicable cells, following change to merge
        startSelecting( selection );
    }

    //Apply grouping by collapsing applicable rows
    private void applyModelGrouping(CellValue< ? > startCell,
                                    boolean bRedraw) {

        data.applyModelGrouping( startCell );

        //Partial redraw
        if ( bRedraw ) {
            int startRowIndex = startCell.getCoordinate().getRow();
            GroupedDynamicDataRow groupedRow = (GroupedDynamicDataRow) data.get( startRowIndex );
            int minRedrawRow = findMinRedrawRow( startRowIndex - (startRowIndex > 0 ? 1 : 0) );
            int maxRedrawRow = findMaxRedrawRow( startRowIndex + (startRowIndex < data.size() - 1 ? 1 : 0) );
            for ( int iRow = 0; iRow < groupedRow.getChildRows().size() - 1; iRow++ ) {
                deleteRowElement( startRowIndex );
            }
            redrawRows( minRedrawRow,
                        maxRedrawRow );
            RowGroupingChangeEvent.fire( this );
        }

    }

    //Check whether two values are equal or both null
    private boolean equalOrNull(Object o1,
                                Object o2) {
        if ( o1 == null && o2 == null ) {
            return true;
        }
        if ( o1 != null && o2 == null ) {
            return false;
        }
        if ( o1 == null && o2 != null ) {
            return false;
        }
        return o1.equals( o2 );
    }

    // Given a base row find the maximum row that needs to be re-rendered based
    // upon each columns merged cells; where each merged cell passes through the
    // base row
    private int findMaxRedrawRow(int baseRowIndex) {

        if ( data.size() == 0 ) {
            return 0;
        }

        // These should never happen, but it's a safe-guard
        if ( baseRowIndex < 0 ) {
            baseRowIndex = 0;
        }
        if ( baseRowIndex > data.size() - 1 ) {
            baseRowIndex = data.size() - 1;
        }

        int maxRedrawRow = baseRowIndex;
        DynamicDataRow baseRow = data.get( baseRowIndex );
        for ( int iCol = 0; iCol < baseRow.size(); iCol++ ) {
            int iRow = baseRowIndex;
            CellValue< ? extends Comparable< ? >> cell = baseRow.get( iCol );
            while ( cell.getRowSpan() != 1
                    && iRow < data.size() - 1 ) {
                iRow++;
                DynamicDataRow row = data.get( iRow );
                cell = row.get( iCol );
            }
            maxRedrawRow = (iRow > maxRedrawRow ? iRow : maxRedrawRow);
        }
        return maxRedrawRow;
    }

    //Find the bottom coordinate of a merged cell
    private Coordinate findMergedCellExtent(Coordinate c) {
        if ( c.getRow() == data.size() - 1 ) {
            return c;
        }
        Coordinate nc = new Coordinate( c.getRow() + 1,
                                        c.getCol() );
        CellValue< ? > newCell = data.get( nc );
        while ( newCell.getRowSpan() == 0 && nc.getRow() < data.size() - 1 ) {
            nc = new Coordinate( nc.getRow() + 1,
                                     nc.getCol() );
            newCell = data.get( nc );
        }
        if ( newCell.getRowSpan() != 0 ) {
            nc = new Coordinate( nc.getRow() - 1,
                                     nc.getCol() );
        }
        return nc;
    }

    // Given a base row find the minimum row that needs to be re-rendered based
    // upon each columns merged cells; where each merged cell passes through the
    // base row
    private int findMinRedrawRow(int baseRowIndex) {

        if ( data.size() == 0 ) {
            return 0;
        }

        // These should never happen, but it's a safe-guard
        if ( baseRowIndex < 0 ) {
            baseRowIndex = 0;
        }
        if ( baseRowIndex > data.size() - 1 ) {
            baseRowIndex = data.size() - 1;
        }

        int minRedrawRow = baseRowIndex;
        DynamicDataRow baseRow = data.get( baseRowIndex );
        for ( int iCol = 0; iCol < baseRow.size(); iCol++ ) {
            int iRow = baseRowIndex;
            CellValue< ? extends Comparable< ? >> cell = baseRow.get( iCol );
            while ( cell.getRowSpan() != 1
                    && iRow > 0 ) {
                iRow--;
                DynamicDataRow row = data.get( iRow );
                cell = row.get( iCol );
            }
            minRedrawRow = (iRow < minRedrawRow ? iRow : minRedrawRow);
        }
        return minRedrawRow;
    }

    //Get the next cell when selection moves in the specified direction
    private Coordinate getNextCell(Coordinate c,
                                    MOVE_DIRECTION dir) {

        int step = 0;
        Coordinate nc = c;

        switch ( dir ) {
            case LEFT :

                // Move left
                step = c.getCol() > 0 ? 1 : 0;
                if ( step > 0 ) {
                    nc = new Coordinate( c.getRow(),
                                         c.getCol()
                                                 - step );

                    // Skip hidden columns
                    while ( nc.getCol() > 0
                            && !columns.get( nc.getCol() ).isVisible() ) {
                        nc = new Coordinate( c.getRow(),
                                             nc.getCol()
                                                     - step );
                    }

                    //Move to top of a merged cells
                    CellValue< ? > newCell = data.get( nc );
                    while ( newCell.getRowSpan() == 0 ) {
                        nc = new Coordinate( nc.getRow() - 1,
                                             nc.getCol() );
                        newCell = data.get( nc );
                    }

                }
                break;
            case RIGHT :

                // Move right
                step = c.getCol() < columns.size() - 1 ? 1 : 0;
                if ( step > 0 ) {
                    nc = new Coordinate( c.getRow(),
                                         c.getCol()
                                                 + step );

                    // Skip hidden columns
                    while ( nc.getCol() < columns.size() - 2
                            && !columns.get( nc.getCol() ).isVisible() ) {
                        nc = new Coordinate( c.getRow(),
                                             nc.getCol()
                                                     + step );
                    }

                    //Move to top of a merged cells
                    CellValue< ? > newCell = data.get( nc );
                    while ( newCell.getRowSpan() == 0 ) {
                        nc = new Coordinate( nc.getRow() - 1,
                                             nc.getCol() );
                        newCell = data.get( nc );
                    }

                }
                break;
            case UP :

                // Move up
                step = c.getRow() > 0 ? 1 : 0;
                if ( step > 0 ) {
                    nc = new Coordinate( c.getRow()
                                                 - step,
                                         c.getCol() );

                    //Move to top of a merged cells
                    CellValue< ? > newCell = data.get( nc );
                    while ( newCell.getRowSpan() == 0 ) {
                        nc = new Coordinate( nc.getRow() - step,
                                             nc.getCol() );
                        newCell = data.get( nc );
                    }

                }
                break;
            case DOWN :

                // Move down
                step = c.getRow() < data.size() - 1 ? 1 : 0;
                if ( step > 0 ) {
                    nc = new Coordinate( c.getRow()
                                                 + step,
                                         c.getCol() );

                    //Move to top of a merged cells
                    CellValue< ? > newCell = data.get( nc );
                    while ( newCell.getRowSpan() == 0 && nc.getRow() < data.size() - 1 ) {
                        nc = new Coordinate( nc.getRow() + step,
                                             nc.getCol() );
                        newCell = data.get( nc );
                    }
                    if ( newCell.getRowSpan() == 0 && nc.getRow() == data.size() - 1 ) {
                        nc = c;
                    }

                }
        }
        return nc;
    }

    // Re-index columns
    private void reindexColumns() {
        for ( int iCol = 0; iCol < columns.size(); iCol++ ) {
            DynamicColumn<T> col = columns.get( iCol );
            col.setColumnIndex( iCol );
        }
    }

    //Remove grouping by expanding applicable rows
    private void removeModelGrouping(CellValue< ? > startCell,
                                     boolean bRedraw) {

        List<DynamicDataRow> expandedRow = data.removeModelGrouping( startCell );

        //Partial redraw
        if ( bRedraw ) {
            int startRowIndex = startCell.getCoordinate().getRow();
            int minRedrawRow = findMinRedrawRow( startRowIndex - (startRowIndex > 0 ? 1 : 0) );
            int maxRedrawRow = findMaxRedrawRow( startRowIndex + (startRowIndex < data.size() - 2 ? 1 : 0) );
            for ( int iRow = 0; iRow < expandedRow.size() - 1; iRow++ ) {
                createEmptyRowElement( startRowIndex );

            }
            redrawRows( minRedrawRow,
                        maxRedrawRow );
            RowGroupingChangeEvent.fire( this );
        }

    }

    //Clear all selections
    protected void clearSelection() {
        // De-select any previously selected cells
        for ( CellValue< ? extends Comparable< ? >> cell : this.selections ) {
            cell.removeState( CellState.SELECTED );
            deselectCell( cell );
        }

        // Clear collection
        selections.clear();
        rangeDirection = MOVE_DIRECTION.NONE;
    }

    protected abstract void createEmptyRowElement(int index);

    protected abstract void createRowElement(int index,
                                             DynamicDataRow rowData);

    protected abstract void deleteRowElement(int index);

    //Check whether "Grouping" widget has been clicked
    protected boolean isGroupWidgetClicked(Event event,
                                           Element target) {
        String eventType = event.getType();
        if ( eventType.equals( "mousedown" ) ) {
            String tagName = target.getTagName();
            if ( "img".equalsIgnoreCase( tagName ) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Redraw table rows. Partial redraw
     * 
     * @param startRedrawIndex
     *            Start row index (inclusive)
     * @param endRedrawIndex
     *            End row index (inclusive)
     */
    protected abstract void redrawRows(int startRedrawIndex,
                                       int endRedrawIndex);

    protected abstract void removeRowElement(int index);

    /**
     * Remove styling indicating a selected state
     * 
     * @param cell
     */
    abstract void deselectCell(CellValue< ? extends Comparable< ? >> cell);

    /**
     * Extend selection from the first cell selected to the cell specified
     * 
     * @param end
     *            Extent of selection
     */
    void extendSelection(Coordinate end) {
        if ( rangeOriginCell == null ) {
            throw new IllegalArgumentException( "origin has not been set. Unable to extend selection" );
        }
        if ( end == null ) {
            throw new IllegalArgumentException( "end cannot be null" );
        }
        clearSelection();
        CellValue< ? > endCell = data.get( end );
        selectRange( rangeOriginCell,
                     endCell );
        if ( rangeOriginCell.getCoordinate().getRow() > endCell.getCoordinate().getRow() ) {
            rangeExtentCell = selections.first();
            rangeDirection = MOVE_DIRECTION.UP;
        } else {
            rangeExtentCell = selections.last();
            rangeDirection = MOVE_DIRECTION.DOWN;
        }
    }

    /**
     * Extend selection in the specified direction
     * 
     * @param dir
     *            Direction to extend the selection
     */
    void extendSelection(MOVE_DIRECTION dir) {
        if ( selections.size() > 0 ) {
            CellValue< ? > activeCell = (rangeExtentCell == null ? rangeOriginCell : rangeExtentCell);
            Coordinate nc = getNextCell( activeCell.getCoordinate(),
                                         dir );
            clearSelection();
            rangeDirection = dir;
            rangeExtentCell = data.get( nc );
            selectRange( rangeOriginCell,
                             rangeExtentCell );

        }
    }

    /**
     * Retrieve the extents of a cell
     * 
     * @param cv
     *            The cell for which to retrieve the extents
     * @return
     */
    CellSelectionDetail getSelectedCellExtents(CellValue< ? extends Comparable< ? >> cv) {

        if ( cv == null ) {
            throw new IllegalArgumentException( "cv cannot be null" );
        }

        // Cells in hidden columns do not have extents
        if ( !columns.get( cv.getCoordinate().getCol() )
                .isVisible() ) {
            return null;
        }

        Coordinate hc = cv.getHtmlCoordinate();
        TableRowElement tre = tbody.getRows().getItem( hc.getRow() )
                .<TableRowElement> cast();
        TableCellElement tce = tre.getCells().getItem( hc.getCol() )
                .<TableCellElement> cast();
        int offsetX = tce.getOffsetLeft();
        int offsetY = tce.getOffsetTop();
        int w = tce.getOffsetWidth();
        int h = tce.getOffsetHeight();
        CellSelectionDetail e = new CellSelectionDetail( cv.getCoordinate(),
                                                         offsetX,
                                                         offsetY,
                                                         h,
                                                         w );
        return e;
    }

    /**
     * Group a merged cell. If the cell is not merged across at least two rows
     * or the cell is not the top of the merged range no action is taken.
     * 
     * @param start
     *            Coordinate of top of merged group.
     */
    void groupCells(Coordinate start) {
        if ( start == null ) {
            throw new IllegalArgumentException( "start cannot be null" );
        }
        CellValue< ? > startCell = data.get( start );

        //Start cell needs to be top of a merged range
        if ( startCell.getRowSpan() <= 1 && !startCell.isGrouped() ) {
            return;
        }

        clearSelection();
        if ( startCell.isGrouped() ) {
            removeModelGrouping( startCell,
                                 true );
        } else {
            applyModelGrouping( startCell,
                                true );
        }

    }

    /**
     * Hide a column
     */
    abstract void hideColumn(int index);

    /**
     * Move the selected cell
     * 
     * @param dir
     *            Direction to move the selection
     */
    void moveSelection(MOVE_DIRECTION dir) {
        if ( selections.size() > 0 ) {
            CellValue< ? > activeCell = (rangeExtentCell == null ? rangeOriginCell : rangeExtentCell);
            Coordinate nc = getNextCell( activeCell.getCoordinate(),
                                         dir );
            startSelecting( nc );
            rangeDirection = dir;
        }
    }

    /**
     * Resize a column
     * 
     * @param col
     * @param width
     */
    abstract void resizeColumn(DynamicColumn< ? > col,
                                      int width);

    /**
     * Add styling to cell to indicate a selected state
     * 
     * @param cell
     */
    abstract void selectCell(CellValue< ? extends Comparable< ? >> cell);

    /**
     * Select a range of cells
     * 
     * @param startCell
     *            The first cell to select
     * @param endCell
     *            The last cell to select
     */
    void selectRange(CellValue< ? > startCell,
                             CellValue< ? > endCell) {
        int col = startCell.getCoordinate().getCol();

        //Ensure startCell precedes endCell
        if ( startCell.getCoordinate().getRow() > endCell.getCoordinate().getRow() ) {
            CellValue< ? > swap = startCell;
            startCell = endCell;
            endCell = swap;
        }

        //Ensure startCell is at the top of a merged cell
        while ( startCell.getRowSpan() == 0 ) {
            startCell = data.get( startCell.getCoordinate().getRow() - 1 ).get( col );
        }

        //Ensure endCell is at the bottom of a merged cell
        Coordinate nc = findMergedCellExtent( endCell.getCoordinate() );
        endCell = data.get( nc );

        //Select range
        for ( int iRow = startCell.getCoordinate().getRow(); iRow <= endCell.getCoordinate().getRow(); iRow++ ) {
            CellValue< ? > cell = data.get( iRow ).get( col );
            selections.add( cell );

            // Redraw selected cell
            cell.addState( CellState.SELECTED );
            selectCell( cell );
        }

        //Set extent of selected range according to the direction of selection
        switch ( rangeDirection ) {
            case DOWN :
                this.rangeExtentCell = this.selections.last();
                break;
            case UP :
                this.rangeExtentCell = this.selections.first();
                break;
        }
    }

    /**
     * Show a column
     */
    abstract void showColumn(int index);

    /**
     * Select a single cell. If the cell is merged the selection is extended to
     * include all merged cells.
     * 
     * @param start
     *            The physical coordinate of the cell
     */
    void startSelecting(Coordinate start) {
        if ( start == null ) {
            throw new IllegalArgumentException( "start cannot be null" );
        }

        //Raise event signalling change in selection 
        CellSelectionDetail ce = getSelectedCellExtents( data.get( start ) );
        SelectedCellChangeEvent.fire( this,
                                      ce );

        clearSelection();
        CellValue< ? > startCell = data.get( start );
        selectRange( startCell,
                     startCell );
        rangeOriginCell = startCell;
        rangeExtentCell = null;
    }

}
