/**
 * Created by rburson on 3/18/16.
 */

/**
 *  Warning!  This component required 'wrapping' a non-React style, traditional javascript grid component (ag-grid)
 *  The wrapped component is very stateful and requires interaction and control with a very 'imperative' javascript api,
 *  thus this component is not a good example the stateless, functional components that React normally promotes.
 *  There has been much work done here to 'work around' the issues created by the grid component and it's requirements.
 *  i.e. the grid component does not gracefully survive React component updates.
 *  
 */
import * as React from 'react'
import {AgGridReact} from 'ag-grid-react'
import Clipboard = require('clipboard')
import {
    CvAction,
    CvState,
    CvProps,
    CvBaseMixin,
    CvListPane,
    CvRecordList,
    CvNavigationResult,
    CvEvent,
    CvRecord,
    CvContext,
    CvQueryPaneCallback,
    CvValueListener,
    CvValueAdapter,
    CvStateChangeResult,
    CvActionFiredResult,
    CvActionCallback,
    CvValueProvider,
    CvActionHandlerParams,
    CvNavigation,
    CvForm,
    CvSearchPane,
    CvSearchPaneCallback,
    CvSortDirection, CvResultCallback, CvMessage, CvEventType, CvMessageType, CvInitMode, CvEventRegistry,
    CvStateChangeType,
    UIUtil
} from 'catreact'
import {
    CvDataAnno,
    CvDataAnnoStyle,
    CvHtmlProp
} from './catreact-html'
import {
    FormContext, ListContext, Prop, ColumnDef, EntityRec, ArrayUtil, ObjUtil, QueryContext, Try,
    Log, EntityRecDef, PropDef, DialogException
} from 'catavolt-sdk'

const CV_GRID_PAGE_SIZE:number = 200;
const CV_GRID_ROW_OVERFLOW_SIZE = 1;
const CV_GRID_ROW_HEIGHT = 25;
const CV_GRID_ROW_HEIGHT_IMAGES = 50;

export enum CvGridEventType {
   SELECTION_EVENT, TOTAL_ROWS_LOADED_EVENT, FIRST_VISIBLE_ROW_CHANGE_EVENT
}

export interface CvGridEvent<T> {
    type:CvGridEventType;
    eventObj:T;
}

export interface CvGridPanelState extends CvState {
    searchNavEvent:CvEvent<CvNavigationResult>;
    quickFilterNavResult:CvNavigationResult;
    complexSearchNavResult:CvNavigationResult;
    numInitialRows:number;
    initialFirstVisibleRow:number;
}

export interface CvGridPanelProps extends CvProps {
    paneRef?:number;
    formContext?:FormContext;
    listContext?:ListContext;
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>;
    actionListeners?:Array<(event:CvEvent<CvActionFiredResult>)=>void>
    gridListener?:CvValueListener<CvGridEvent<any>>;
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    actionProvider?:CvValueProvider<CvActionHandlerParams>;
    searchable?:boolean;
    initialSelectedItems?:Array<string>;
    numInitialRows?:number;
    initialFirstVisibleRow?:number;
    gridHeight?:string;
}

//the Grid control doesn't allow periods in column key names, so we switch back and forth
const CvGridPanel_DOT_REPL = '_d_o_t_';

/*
 ***************************************************
 * Render a ListContext
 ***************************************************
 */
export var CvGridPanel = React.createClass<CvGridPanelProps, CvGridPanelState>({

    mixins: [CvBaseMixin],

    componentWillMount: function() {
        //set up unmanaged, init values
        const numInitialRows = this.props.numInitialRows ? this.props.numInitialRows : CV_GRID_PAGE_SIZE;
        this.setState({numInitialRows: numInitialRows});
    },

    componentDidMount: function () {
        /* Allow for copying cell content */
        new Clipboard('.cv-clipboard-target', {
            text: function (trigger) {
                return trigger.innerText;
            }
        });
        /*
            register to receive events from the 'popup search' editor model
            the list pane will refresh itself when the search is submitted but we need
            to reset the data source before that happens
        */
        (this.eventRegistry() as CvEventRegistry)
            .subscribe<CvStateChangeResult>(this._dataChangeListener, CvEventType.STATE_CHANGE);
    },

    componentWillUnmount: function() {
        (this.eventRegistry() as CvEventRegistry).unsubscribe(this._dataChangeListener);
    },

    componentWillReceiveProps(props, state) {
        //Log.debug('GridPanel will receive props:');
        //Log.debug(props);
    },

    componentWillUpdate(props, state) {
        //Log.debug('GridPanel will update: ');
        //Log.debug(props);
    },

    getDefaultProps: function () {
        return {
            paneRef: null,
            formContext: null,
            listContext: null,
            navigationListeners: [],
            actionListeners: [],
            gridListener: null,
            stateChangeListeners: [],
            actionProvider: null,
            searchable: false,
            initialSelectedItems: null,
            numInitialRows: CV_GRID_PAGE_SIZE,
            initialFirstVisibleRow: null,
            gridHeight: '100%'
        }
    },

    getInitialState: function () {
        return {searchNavEvent: null,
            quickFilterNavResult: null,
            complexSearchNavResult: null,
            numInitialRows: null,
            initialFirstVisibleRow: null
        }
    },


    render: function () {

        /* We can't do ES7 style rest destructuring in Typescript yet, so we do this... */
        const listPaneProps = {
            paneRef: this.props.paneRef,
            formContext: this.props.formContext,
            queryContext: this.props.listContext,
            stateChangeListeners: this.props.stateChangeListeners,
            actionListeners: this.props.actionListeners,
            actionProvider: this.props.actionProvider
        }
        return (
            <CvListPane {...listPaneProps} recordPageSize={this.state.numInitialRows} queryRenderer={(cvContext:CvContext, callback:CvQueryPaneCallback)=>{
                const listContext:ListContext = cvContext.scopeCtx.scopeObj;
                return(
                    <div className="cv-component-container cv-grid-container">
                        {(()=>{
                         return this.props.searchable ?
                            <CvAction actionId={'#search'} fireOnLoad={(success, error)=>{}} paneContext={listContext}
                                navigationListeners={[(event:CvEvent<CvNavigationResult>)=>{
                                    //hold on to the nav event so we can pass it along later to the complex search popup
                                    let newState = {complexSearchNavResult: event.eventObj, searchNavEvent: event};
                                    if(this._unifiedSearchSupport()) {
                                        newState["quickFilterNavResult"] = event.eventObj;
                                    }
                                    this.setState(newState);
                                }]}/> : null;
                        })()}
                        {(()=>{
                            //older server versions need to use the deprecated 'searchQuery' action for keyword search
                             return this.props.searchable && !this._unifiedSearchSupport() ?
                                <CvAction actionId={'searchQuery'} fireOnLoad={(success, error)=>{}} paneContext={listContext}
                                    navigationListeners={[(event:CvEvent<CvNavigationResult>)=>{
                                        this.setState({quickFilterNavResult: event.eventObj});
                                    }]}/> : null;
                        })()}
                        <div className="cv-component-toolbar">
                            <CvQuickFilter filterValueListener={(filterValue)=>{if(this.table){this.table.quickFilter(filterValue)} }}
                             ref={(me)=>this.quickFilter=me}/>
                            {/*<button className="btn btn-default" onClick={()=>{if(this.table){this.table.autoFit();}}} title="Fit To Table">
                                <span className="glyphicon glyphicon-resize-full" aria-hidden="true"/>
                            </button>
                            <button className="btn btn-default" onClick={()=>{if(this.table){this.table.autoSize();}}} title="Autosize Columns">
                                <span className="glyphicon glyphicon-text-width" aria-hidden="true"/>
                            </button>*/}
                            <button className="btn btn-default" onClick={callback.refresh} title="Refresh All">
                                <span className="glyphicon glyphicon-refresh" aria-hidden="true"/>
                            </button>
                            {(()=>{
                                /*
                                 The 'search' button fires a typical navigation which opens a search pane, which shows in a modal popup
                                 We're passing along the previously obtained 'search navigation' result
                                */
                                return this.props.searchable && this.state.searchNavEvent ?
                                    <CvNavigation navigationResult={this.state.searchNavEvent.eventObj}>
                                        <CvForm formContext={this.state.searchNavEvent.eventObj.navRequest}>
                                            <CvSearchPane paneRef={0} initMode={CvInitMode.MANUAL}
                                                  detailsRenderer={(cvContext:CvContext, entityRec:EntityRec, searchCallback:CvSearchPaneCallback)=>{
                                                      return <button className="btn btn-default"
                                                                     onClick={() => {
                                                                         searchCallback.reopenSearch((success, error)=> {
                                                                             if (error) {
                                                                                 this.props.eventRegistry.publishError('complex search reopen failed with '
                                                                                     + ObjUtil.formatRecAttr(error));
                                                                             } else {
                                                                                 if (this.quickFilter.isSet()) this.quickFilter.resetText();
                                                                                     this.props.navigationListeners.forEach(
                                                                                         listener => listener(this.state.searchNavEvent)
                                                                                     );
                                                                             }
                                                                         });
                                                                     }} title="Search">
                                                          <span className="glyphicon glyphicon-search"
                                                                aria-hidden="true"/>
                                                      </button>
                                                  }}/>
                                        </CvForm>
                                        </CvNavigation>
                                    : null;
                            })()}
                        </div>
                            <div className="cv-component-body">
                                {(()=>{
                                    /* make sure that the search actions have been opened */
                                    return this._areSearchHandlesReady() ?
                                        <CvNavigation navigationResult={this.state.quickFilterNavResult}>
                                            <CvForm formContext={this.state.quickFilterNavResult.navRequest}>
                                                <CvSearchPane paneRef={0} initMode={CvInitMode.MANUAL} detailsRenderer={(cvContext:CvContext,
                                                    entityRec:EntityRec, quickFilterCallback:CvSearchPaneCallback)=>
                                                        <CvGridTable listContext={listContext}
                                                                     lastRefreshTime={listContext.lastRefreshTime}
                                                                     initialSelectedItems={this.props.initialSelectedItems}
                                                                     numInitialRows={this.props.numInitialRows}
                                                                     gridHeight={this.props.gridHeight}
                                                                     initialFirstVisibleRow={this.props.initialFirstVisibleRow}
                                                                     queryPaneCallback={callback}
                                                                     searchNavResult={this.state.searchNavEvent.eventObj}
                                                                     quickFilterCallback={quickFilterCallback}
                                                                     eventRegistry={this.eventRegistry()}
                                                                     navigationListeners={this.props.navigationListeners}
                                                                     actionListeners={this.props.actionListeners}
                                                                     gridListener={this.props.gridListener}
                                                                     stateChangeListeners={this.props.stateChangeListeners}
                                                                     ref={table=>this.table = table}/>
                                                }/>
                                            </CvForm>
                                    </CvNavigation> : null;
                                })()}
                            </div>
                        </div>);
            }}/>
        );
    },

    shouldComponentUpdate(nextProps, nextState) {
        /*
         NOTE:
         initialSelectedItems, numInitialRows, initialFirstVisibleRow are all unmanaged properties, meaning
         that when they change, they are not synchronized with component state (we ignore them)
         Here they are simply changing URL params to be retained only for navigation or a refresh
         */
        /*
            This is an 'exclusive' change list - updates happen by default unless included here
            Props changes that should not fire updates, should be added here
         */

        //Don't update for selection changes as this is NOT a managed param - the Grid control handles selections internally
        //determine if this was a selection change
        //if so avoid a component update
        const initialSelectedItems = this.props.initialSelectedItems;
        const nextSelectedItems = nextProps.initialSelectedItems;

        //do we already have selected items
        //if so have we simply added or removed a selection
        if(initialSelectedItems != null) {
            if (nextSelectedItems && (initialSelectedItems.length != nextSelectedItems.length)) {
                return false;
            }
        } else {
            //we don't have a selection but now, but having an incoming selection
            if(nextSelectedItems != null) {
                return false;
            }
        }
        //Don't update for changes in original num rows - also handled internally by Grid control
        //this is NOT a managed parameter
        if(nextProps.numInitialRows != this.props.numInitialRows) {
            return false;
        }
        //Don't update for changes in initialFirstVisibleRow
        //this is NOT a managed parameter
        if(nextProps.initialFirstVisibleRow != this.props.initialFirstVisibleRow) {
            return false;
        }
        //otherwise we'll continue to update the grid...
        return true;

    },

    _dataChangeListener: function (dataChangeResult:CvEvent<CvStateChangeResult>) {

        const eventSource = dataChangeResult.eventObj.source;
        const eventType = dataChangeResult.eventObj.type;
        const complexSearchNavResult:CvNavigationResult = this.state.complexSearchNavResult;
        //listen for the modal search submission
        if (eventType === CvStateChangeType.DATA_CHANGE) {
            if(eventSource && complexSearchNavResult && complexSearchNavResult.navRequest) {
                if (eventSource.parentContext == complexSearchNavResult.navRequest) {
                    this.table.resetDataSource();
                }
            }
        }
    },

    _areSearchHandlesReady: function() {
       return this.state.quickFilterNavResult && this.state.searchNavEvent;
    },

    _unifiedSearchSupport: function() {
        return this.catavolt().isFeatureSetAvailable("Unified_Search");
    }

});

export var CvQuickFilter = React.createClass<{filterValueListener:CvValueListener<string>}, {searchText:string, isSet:boolean}>({

    getDefaultProps: function () {
        return {filterValueListener: null}
    },

    getInitialState: function () {
        return {searchText: "", isSet:false}
    },
    
    isSet: function() {
        return this.state.searchText;   
    },

    render: function () {
        return <span className="form-inline cv-quick-filter">
                <input placeholder="Quick Filter..." className="form-control" type="text" value={this.state.searchText}
                       onChange={this._onFilterTextChange} onKeyUp={this._filterKeyUp}/>
                <button className="btn btn-default" onClick={this.resetText} title="Reset Quick Filter">
                    <span className="glyphicon glyphicon-remove-sign" aria-hidden="true"/>
                </button>
        </span>
    },

    resetText() {
        this.setState({searchText: ""});
        const listener:CvValueListener<string> = this.props.filterValueListener;
        if (listener) {
            listener("");
        }
    },

    _filterKeyUp(e:any) {
        if (e.keyCode === 13) {
            const listener:CvValueListener<string> = this.props.filterValueListener;
            if (listener) {
                listener(this.state.searchText);
            }
        }
    },

    _onFilterTextChange(e:any) {
        const value = e.target.value ? e.target.value : "";
        this.setState({searchText: value});
    }

});

export interface CvGridTableState extends CvState {
    numInitialRows:number;
    initialFirstVisibleRow:number;
    initialSelectedItems:Array<string>;
    forceAutoSize:boolean;
}

export interface CvGridTableProps extends CvProps {
    listContext:ListContext;
    lastRefreshTime:Date;
    initialSelectedItems?:Array<string>;
    numInitialRows?:number;
    gridHeight?:string;
    initialFirstVisibleRow?:number;
    queryPaneCallback:CvQueryPaneCallback;
    searchNavResult:CvNavigationResult;
    quickFilterCallback:CvSearchPaneCallback;
    eventRegistry:CvEventRegistry;
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>;
    actionListeners?:Array<(event:CvEvent<CvActionFiredResult>)=>void>
    gridListener?:CvValueListener<CvGridEvent<any>>;
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
}

export var CvGridTable = React.createClass<CvGridTableProps,CvGridTableState>({

    autoSize: function () {
        const columns = this.columnApi.getAllColumns().filter(col=>col.visible).map(col=>col.colDef.field);
        this.columnApi.autoSizeColumns(columns);
    },

    autoFit: function () {
        this.gridApi.sizeColumnsToFit();
    },

    componentWillMount: function() {
        //set up unmanaged, init values
        const numInitialRows = this.props.numInitialRows ? this.props.numInitialRows : CV_GRID_PAGE_SIZE;
        this.setState({numInitialRows: numInitialRows});
        const initialFirstVisibleRow = this.props.initialFirstVisibleRow ? this.props.initialFirstVisibleRow : 0;
        this.setState({initialFirstVisibleRow: initialFirstVisibleRow});
        this.setState({initialSelectedItems:this.props.initialSelectedItems});
    },

    componentDidMount() {
    },

    componentWillReceiveProps(props, state) {
        //Log.debug('GridTable will receive props:');
        //Log.debug(props);
    },

    componentWillUpdate(props, state) {
        //Log.debug('GridTable will update:');
        //Log.debug(props);
    },

    componentDidUpdate(prevProps, prevState) {
        //Log.debug('GridTable component did update');
            //make sure there's not another update in progress, as this breaks the gridApi call here
            if(this.gridApi && !this.props.queryPaneCallback.isInProgress()){
                //Log.debug('calling refreshInfintePageCache');
                this.gridApi.refreshInfinitePageCache();
            }
    },

    getDefaultProps: function () {
        return {
            listContext: null,
            lastRefreshTime: null,
            initialSelectedItems: null,
            numInitialRows: CV_GRID_PAGE_SIZE,
            gridHeight: '100%',
            initialFirstVisibleRow: null,
            queryPaneCallback: null,
            searchNavResult: null,
            quickFilterCallback: null,
            eventRegistry: null,
            navigationListeners: [],
            actionListeners: [],
            gridListener: null,
            stateChangeListeners: []
        }
    },

    getInitialState: function () {
        return {numInitialRows: null, initialFirstVisibleRow: null, initialSelectedItems:null, forceAutoSize:false}
    },

    quickFilter(filterText:string) {

        //submit the quick search
        const quickFilterCallback:CvSearchPaneCallback = this.props.quickFilterCallback;
        const queryPaneCallback:CvQueryPaneCallback = this.props.queryPaneCallback;
        if (quickFilterCallback) {
            quickFilterCallback.reopenSearch((success, error)=> {
                if (error) {
                    this.props.eventRegistry.publishError('quickFilter reopen failed with ' + ObjUtil.formatRecAttr(error));
                } else {
                    quickFilterCallback.clearSearchValues();
                    quickFilterCallback.setPropValue('keyword', filterText);
                    //this is key - ag-grid must rebuild the grid instance
                    this.resetDataSource();
                    //this.setState({forceAutoSize:true});
                    quickFilterCallback.submitSearch((success, error)=> {
                        if (error) {
                            queryPaneCallback.refresh();
                            this.props.eventRegistry.publishError('quickFilter error ' + ObjUtil.formatRecAttr(error));
                        }
                    });
                }
            });
        }
    },

    render: function () {

        const {listContext, queryPaneCallback, searchNavResult, navigationListeners, actionListeners, stateChangeListeners, eventRegistry} = this.props;

        const entityRecs:Array<EntityRec> = ArrayUtil.copy<EntityRec>(listContext.scroller.buffer);
        const hasBinaries = this._anyBinaries(listContext, entityRecs);
        const rowHeight = hasBinaries ? CV_GRID_ROW_HEIGHT_IMAGES : CV_GRID_ROW_HEIGHT;
        return (
            <CvNavigation navigationResult={searchNavResult}>
                <CvForm formContext={searchNavResult.navRequest}>
                    <CvSearchPane paneRef={0} initMode={CvInitMode.MANUAL} detailsRenderer={(cvContext:CvContext,
                    entityRec:EntityRec, searchCallback:CvSearchPaneCallback)=> {
                        const columnDefs = listContext.listDef.activeColumnDefs.map((columnDef:ColumnDef)=> {
                            //@TODO - fully integrate custom filtering
                            //filter: this._getFilterType(columnDef.propertyDef),
                            //filterParams: {apply:true},

                            //the grid doesn't allow '.' in key names
                            return {
                                headerName: columnDef.heading,
                                field: columnDef.name.replace('.', CvGridPanel_DOT_REPL),
                                width: this._getColumnWidth(columnDef),
                                cellRendererFramework: CvGridTableCell
                            };
                        });
                        columnDefs[0].pinned = 'left';
                        columnDefs[0].cellRendererFramework = CvGridTableCellColumn0;

                        if(!this.dataSource) {
                           this.dataSource = new CvGridDataSource(-1, listContext, searchCallback, queryPaneCallback,
                                   navigationListeners, actionListeners, stateChangeListeners, eventRegistry);
                        } else {
                            this.dataSource.update(listContext, searchCallback, queryPaneCallback,
                                navigationListeners, actionListeners, stateChangeListeners, eventRegistry);
                        }

                        return (
                        <div style={{height: this.props.gridHeight}} className="ag-fresh">
                            <AgGridReact
                                onGridReady={this._onGridReady}
                                showToolPanel={false}
                                onRowSelected={this._onRowSelected}
                                onRowDoubleClicked={this._onRowDoubleClicked}
                                onBodyScroll={UIUtil.debounce(this._onBodyScroll.bind(this, rowHeight), 200)}
                                getRowStyle={this._getRowStyle}
                                getRowNodeId={this._getRowNodeId}
                                rowHeight={rowHeight}
                                columnDefs={columnDefs}
                                enableServerSideSorting={true}
                                enableServerSideFilter={false}
                                suppressRowClickSelection="false"
                                rowSelection="multiple"
                                rowDeselection={true}
                                enableColResize={true}
                                enableSorting={false}
                                enableFilter={false}
                                groupHeaders={false}
                                suppressCellSelection={true}
                                debug={false}
                                rowModelType='infinite'
                                infiniteInitialRowCount={this.state.numInitialRows}
                                paginationPageSize={this.state.numInitialRows}
                                infiniteBlockSize={this.state.numInitialRows}
                                maxConcurrentDatasourceRequests={1}
                                sortingOrder={['asc','desc']}
                                datasource={this.dataSource}
                                paginationOverflowSize={CV_GRID_ROW_OVERFLOW_SIZE}
                            />
                        </div>
                        );}
                    }/>
                </CvForm>
            </CvNavigation>
        );
    },

    resetDataSource: function() {
        this.dataSource = null;
    },

    shouldComponentUpdate(nextProps, nextState) {
        /*
         NOTE:
         initialSelectedItems, numInitialRows, initialFirstVisibleRow are all unmanaged properties, meaning
         that when they change, they are not synchronized with component state (we ignore them)
         They are simply changing URL params to be retained only for navigation or a refresh
         */

        //don't update if we've change the forceAutoSize value
        if(this.state.forceAutoSize != this.state.forceAutoSize) {
            return false;
        }

        //update only if we have new list data
        if(this.props.queryPaneCallback && !this.props.queryPaneCallback.isInProgress()) {
            if (nextProps.lastRefreshTime) {
                if (this.props.lastRefreshTime) {
                    if (nextProps.lastRefreshTime.getTime() > this.props.lastRefreshTime.getTime()) {
                        return true;
                    }
                } else {
                    return true;
                }
            }
        }

        return false;
    },

    _anyBinaries: function (listContext:ListContext, entityRecs:Array<EntityRec>):boolean {
        return listContext.entityRecDef.propertyDefs.some(propDef=>propDef.isBinaryType) ||
            entityRecs.some(entityRec=>!!entityRec.imageName || entityRec.props.some(prop=>!!prop.imageName));
    },

    _onBodyScroll: function(rowHeight, event) {

        /*
            1) Report change in 'first visible row' (unmanaged parameter)
         */
        //seems to be the only option for approximating the first visible row.  really need a better solution
        const vpr = this.gridApi.getVerticalPixelRange();
        const rowNum = Math.round(vpr.bottom / rowHeight) - 1;
        if(this.props.gridListener) this.props.gridListener({type: CvGridEventType.FIRST_VISIBLE_ROW_CHANGE_EVENT, eventObj:rowNum});
    },


    _onGridReady: function (params:any) {
        this.gridApi = params.api;
        this.columnApi = params.columnApi;
        //relying on this event to tell us when more pages have been loaded and add to the scroll buffer
        this.gridApi.addEventListener('paginationChanged', this._onPaginationChanged)
        //relying on this event firing when brand new data is inserted
        this.gridApi.addEventListener('columnEverythingChanged', this._onColumnEverythingChanged)
        //relying on this event firing after rows are loaded and inserted (this seems to be the only indication)
        this.gridApi.addEventListener('scrollVisibilityChanged', this._onRowsInitialized);
        //uncomment this to log all the ag-grid events to the console for debugging
        //this.gridApi.addGlobalListener((event, p1, p2, p3)=>{ const e = event; Log.debug(e); });
    },

    _onPaginationChanged: function(event) {
        /*
         *  1) Report change in total rows loaded (unmanaged param)
         */
        const model = this.gridApi.getModel();
        if(model) {
            const rowsLoaded = this.props.listContext.scroller.buffer.length;
            //don't update if rowsLoaded hasn't actually changed (i.e. on the first load)
            if(this.state.numInitialRows != rowsLoaded) {
                if(this.props.gridListener && (rowsLoaded > CV_GRID_PAGE_SIZE)) {
                    this.props.gridListener({type: CvGridEventType.TOTAL_ROWS_LOADED_EVENT, eventObj: rowsLoaded});
                }
            }
        }
    },

    _onColumnEverythingChanged: function(event) {
        //if(this.state.forceAutoSize) {
        //The grid doesn't report an event when all rows are rendered, so we're
        //left with the horrible option of trying to time the autoSize.  somebody help.
        setTimeout(this.autoSize, 500);
        //this.setState({forceAutoSize:false});
        //}
    },

    _onRowDoubleClicked: function (event) {
        event.node.data.rowCallback.fireAction();
    },

    _onRowSelected: function (event) {
        /*
            1) Report change in selections (unmanaged parameter)
         */
        //get the oids of the selected rows and notify the gridListeners fo the selection
        if (event.node.data && event.node.data.entityRec) {
            const selectedItems = this.gridApi.getSelectedRows().map(data => data.entityRec.objectId);
            if (this.props.gridListener) this.props.gridListener({type: CvGridEventType.SELECTION_EVENT, eventObj:selectedItems});
        }
    },

    _onRowsInitialized: function(event) {
        //set the first visible row if specified
        if(this.state.initialFirstVisibleRow) {
            this.gridApi.ensureIndexVisible(this.state.initialFirstVisibleRow);
        }
        //select initial rows based on the supplied selectedItems oids
        const selectedItems = this.state.initialSelectedItems;
        if(selectedItems && selectedItems.length > 0){
            selectedItems.forEach(selectedItem=>{
                this.gridApi.forEachNode(node=>{
                    if (node.data && node.data.entityRec &&
                        (node.data.entityRec.objectId === selectedItem)) {
                        node.setSelected(true);
                    }
                });
            });
        }
        //autosize the columns
        this.autoSize();
        //remove this listener as we only want this to fire on the first load
        this.gridApi.removeEventListener('scrollVisibilityChanged', this._onRowsInitialized);
    },

    _getFilterType: function (propDef:PropDef) {
        if (propDef.isDateType || propDef.isDateTimeType) {
            return 'date';
        } else if (propDef.isNumericType) {
            return 'number'
        } else {
            return 'text';
        }
    },

    _getRowStyle: function (params) {
        if (params.data && params.data.entityRec) {
            const styleInfo:CvDataAnnoStyle = (CvDataAnno as any).generateStyleInfo(params.data.entityRec);
            return styleInfo.cssStyle;
        } else {
            return null;
        }
    },

    _getRowNodeId: function (data) {
        return data.entityRec.objectId;
    },

    _getColumnWidth: function (columnDef:ColumnDef) {
        return columnDef.propertyDef.presLength < 200 || columnDef.propertyDef.presLength > 2000 ? 250 : columnDef.propertyDef.presLength;
    },

});

class CvGridDataSource {

    private inProgress:boolean = false;

    constructor(public rowCount:number,
                private listContext:ListContext,
                private searchCallback:CvSearchPaneCallback,
                private queryPaneCallback:CvQueryPaneCallback,
                private navigationListeners:Array<(event:CvEvent<CvNavigationResult>)=>void> = [],
                private actionListeners:Array<(event:CvEvent<CvActionFiredResult>)=>void> = [],
                private stateChangeListeners:Array<(event:CvEvent<CvStateChangeResult>)=>void> = [],
                private eventRegistry:CvEventRegistry) {

        this.init();
    }

    public update(listContext:ListContext,
                  searchCallback:CvSearchPaneCallback,
                  queryPaneCallback:CvQueryPaneCallback,
                  navigationListeners:Array<(event:CvEvent<CvNavigationResult>)=>void> = [],
                  actionListeners:Array<(event:CvEvent<CvActionFiredResult>)=>void> = [],
                  stateChangeListeners:Array<(event:CvEvent<CvStateChangeResult>)=>void> = [],
                  eventRegistry:CvEventRegistry) {

        this.listContext = listContext;
        this.searchCallback = searchCallback;
        this.queryPaneCallback = queryPaneCallback;
        this.navigationListeners = navigationListeners;
        this.actionListeners = actionListeners;
        this.stateChangeListeners = stateChangeListeners;
        this.eventRegistry = eventRegistry;

        this.init();

    }

    private init() {
        this.searchCallback.suppressEvents(true);
        this.queryPaneCallback.suppressEvents(true);
    }

    public getRows(params:any) {

        const {sortModel} = params;

        if (sortModel && sortModel.length > 0) {
            this._handleSort(sortModel, this.searchCallback, (success, error)=> {
                if (error) {
                    this.eventRegistry.publishError('CvGridPanel._handleSoft failed with ' + ObjUtil.formatRecAttr(error));
                } else if (success) {
                    //refresh the list
                    this.listContext.scroller.refresh().onComplete(entityRecTry=> {
                        if (entityRecTry.isFailure) {
                            this.eventRegistry.publishError('CvGridPanel::getRows queryContext.scroller.refresh failed with ' + ObjUtil.formatRecAttr(error));
                        } else {
                            this._getRows(params);
                        }
                    });
                } else {
                    this._getRows(params);
                }
            });
        } else {
            this._getRows(params);
        }

    }
    
    private _getRows(params:any) {

        const {startRow, endRow, successCallback, failCallback} = params;

        let rowData = [];
        const currentRows:Array<EntityRec> = this.listContext.scroller.buffer;
        //any rows loaded yet?
        if (currentRows.length > 0) {
            this.pageForwardToRange((success, error)=>{
                if(error) {
                   this.eventRegistry.publishError(ObjUtil.formatRecAttr(error));
                   failCallback();
                } else {
                    const currentRows:Array<EntityRec> = this.listContext.scroller.buffer;

                    let lastIndex = -1;
                    lastIndex = (currentRows.length > endRow || this.queryPaneCallback.hasMoreForward()) ? -1 :
                        (endRow > currentRows.length) ? currentRows.length : endRow;
                    rowData = this._buildRows(currentRows.slice(startRow, endRow));
                    successCallback(rowData, lastIndex);
                }
            }, this.queryPaneCallback, startRow, endRow);
        } else {
            successCallback(rowData, 0);
        }
    }

    /*
     @TODO - fully integrate custom filtering!
     Resolve to true if the filter value has changed, otherwise false.
     private _handleFilters(filterModel:any, searchCallback:CvSearchPaneCallback,
     resultCallback:CvResultCallback<boolean>) {
     Object.keys(filterModel).forEach((key:string)=>{
     const colName = key;
     const pFilterValue = filterModel[key].filter;
     const pOperator = filterModel[key].type;
     });
     }
     */

    /*
     Resolve to true if the sort value has changed, otherwise false.
     */
    private _handleSort(sortModel:Array<any>, searchCallback:CvSearchPaneCallback,
                        resultCallback:CvResultCallback<boolean>) {

        const colName = sortModel[0].colId.replace(CvGridPanel_DOT_REPL, '.');
        const isDesc = sortModel[0] && sortModel[0].sort.indexOf('d') === 0;
        let sortDirection:CvSortDirection = null;
        if ((isDesc && !searchCallback.isDescending(colName))) {
            sortDirection = 'DSC';
        } else if (!isDesc && !searchCallback.isAscending(colName)) {
            sortDirection = 'ASC';
        }
        if (sortDirection) {
            searchCallback.reopenSearch((success, error)=> {
                if(error) {
                    resultCallback(false, error);
                } else {
                    searchCallback.clearSortValues();
                    searchCallback.setSortValue(colName, sortDirection, 0);
                    searchCallback.submitSearch((success, error)=> {
                        resultCallback(!error, error);
                    });
                }
            })
        } else {
            resultCallback(false);
        }
    }

    private _buildRows(newPage:Array<EntityRec>):Array<any> {

        const entityRecs:Array<EntityRec> = ArrayUtil.copy<EntityRec>(newPage);
        return entityRecs.map((record:EntityRec)=> {
            const row = {entityRec: record};
            this.listContext.listDef.activeColumnDefs.forEach((colDef:ColumnDef)=> {
                const prop:Prop = record.propAtName(colDef.name);
                const styleInfo:CvDataAnnoStyle = (CvDataAnno as any).generateStyleInfo(prop);
                const cellStyle = 'cv-grid-cell cv-target';
                //select "this record" so that the action can find the target via the selectionProvider
                const selectionAdapter:CvValueAdapter<Array<string>> = new CvValueAdapter<Array<string>>();
                selectionAdapter.createValueListener()([record.objectId]);
                //the grid doesn't allow '.' in key names
                row[colDef.name.replace('.', CvGridPanel_DOT_REPL)] =
                    <CvAction actionId={this.listContext.listDef.defaultActionId}
                              paneContext={this.listContext}
                              navigationListeners={this.navigationListeners}
                              actionListeners={this.actionListeners}
                              stateChangeListeners={this.stateChangeListeners}
                              selectionProvider={selectionAdapter}
                              key={prop.name + '_'} renderer={(cvContext:CvContext, callback?:CvActionCallback)=>{
                                row['rowCallback'] = callback;
                                return (
                                <div className="cv-grid-cell-wrapper">
                                    <CvDataAnno dataAnnoStyle={styleInfo} paneContext={this.listContext} wrapperElem="div"
                                                wrapperElemProps={{className:cellStyle}}>
                                        <CvHtmlProp propName={prop.name}
                                                    entityRec={record}
                                                    paneContext={this.listContext}
                                                    imageClassName="cv-image-grid-item-max-size"
                                                    overrideValue={styleInfo.overrideText}
                                                    dataPropNames={['title']}
                                                    wrapperElemProps={{className: 'cv-clipboard-target'}}/>
                                    </CvDataAnno>
                                </div>
                                );}
                    }/>
            });
            return row;
        });
    }

    private pageForwardToRange(resultCallback:CvResultCallback<boolean>, queryPaneCallback:CvQueryPaneCallback,
                               startRow:number, endRow:number):void {

        let currentRows:Array<EntityRec> = this.listContext.scroller.buffer;
        if (startRow >= currentRows.length || endRow > currentRows.length) {
            if (queryPaneCallback.hasMoreForward()) {
                queryPaneCallback.pageForward((num, error) => {
                    const currentRows = this.listContext.scroller.buffer;
                    if (error) {
                        resultCallback(false, error);
                    } else if (startRow >= currentRows.length) {
                        //still haven't reached start row, try to page again
                        this.pageForwardToRange(resultCallback, queryPaneCallback, startRow, endRow);
                    } else {
                        //we've got the startRow, check the endRow
                        if(endRow > currentRows.length) {
                            //still haven't reached the endRow, try to page again
                            this.pageForwardToRange(resultCallback, queryPaneCallback, startRow, endRow);
                        } else {
                          resultCallback(true);
                        }
                    }
                });
            } else {
                if(startRow >= currentRows.length) {
                    resultCallback(false, new Error('Requested startRow is greater than total number of rows'));
                } else {
                    //no more pages, we've got the start row but no the end row
                    resultCallback(true);
                }
            }
        } else {
            //already loaded
            resultCallback(true);
        }
    }


}

export var CvGridTableCellColumn0 = React.createClass<{},{}>({

    render: function () {
        if (this.props.data) {
            return this.props.value || null;
        } else {
            return <i className="fa fa-spinner fa-pulse cv-table-spinner-size"/>;
        }
    }

});

export var CvGridTableCell = React.createClass<{},{}>({

    render: function () {
        return this.props.value || null;
    }

});

