/*
 * TableSupport.java    1.0 97/08/28
 *
 * Copyright (c) 1997 Netscape Communications Corporation
 *
 * Netscape grants you a non-exclusive, royalty free, license to use,
 * modify and redistribute this software in source and binary code form,
 * provided that i) this copyright notice and license appear on all copies of
 * the software; and ii) Licensee does not utilize the software in a manner
 * which is disparaging to Netscape.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * See the CDK License Agreement for additional terms and conditions.
 *
 * created: 2/97 cls
 */

package netscape.peas;



import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;


/**
 * Helper class for which provides simple implemenation of the Table interface.
 *
 */
public class TableSupport extends RowSupport implements Table {


    //-----------------------------------------------------------------------------------------------------------
    // constructor


    public TableSupport( boolean bOwnsDataContext ) {

        super( bOwnsDataContext );
        moTableChangeSupport = new TableChangeSupport( this );
    }

    public TableSupport( Table oDelegator, boolean bOwnsDataContext ) {

        super( oDelegator, bOwnsDataContext );
        moTableChangeSupport = new TableChangeSupport( oDelegator );
    }


    //-----------------------------------------------------------------------------------------------------------
    // GetInterface interface

    /**
     * Return an object which implements this specified (fully qualified) interface name.
     * If this interface is not implemented, return null.
     */
    public Object getInterface( String sInterfaceName ) {
        if ( "netscape.peas.Table".equals( sInterfaceName ) ) {
            return getTableDelegator();
        } else if ( "netscape.peas.Row".equals( sInterfaceName ) ) {
            return getRowDelegator();
        } else {
            return null;
        }
    } // getInterface


    //-----------------------------------------------------------------------------------------------------------
    // TableInfo interface

    /**
     * Getter method for this cursor's "query" property.
     */
    public String getQuery() {
        return null;
    }

    /**
     * Setter method for this cursor's "query" property.
     */
	public void setQuery( String sNewQuery ) {
    }

    /**
     * Setter method for this cursor's "bufferSize" property.
     * this specifies the number of rows fetched when the query
     * is executed, and on successive fetchMoreRows() calls.
     *
     * A buffer size of 0 means that all rows are fetched before
     * control is returned from executeQuery.
     */
	public int getFetchBufferSize() {
	    return miFetchBufferSize;
    }

    /**
     * Getter method for this cursor's "bufferSize" property.
     * this specifies the number of rows fetched when the query
     * is executed, and on successive fetchMoreRows() calls.
     */
	public void setFetchBufferSize( int iNewBufferSize ) {
	    miFetchBufferSize = iNewBufferSize;
    }

	/**
	 * Perform "generic" executeQuery functionality.
	 * This entails initializing the table to be empty.
	 *
	 * Subclasses or Delegators need to do their executeQuery stuff
	 * before invoking this code.
	 *
	 */
	public void executeQuery() {

        //System.out.println( "TS.executeQuery + " );
        mbRowsPending = true;
        miNumRowsFetched = 0;

        // notify TableListeners that we emptied the table.  more notificateions are initiated by fetchMoreRows
        fireTableChange( TableChangeEvent.TABLE_EMPTY, 0, 0, null, getTableDelegator(), null );

        fetchMoreRows();

        miCurrentRowNumber = -1;
        if ( getNumRowsFetched() > 0 ) {
            System.out.println( "TS.executeQuery 3 " );
            getTableDelegator().doSetCurrentRowNumber( 0, true, true, null ); // notifies listeners
            System.out.println( "TS.executeQuery 4 " );
            
        }
        System.out.println( "TS.executeQuery - " );

    } // executeQuery


    /**
     * Delete the specified row from the table.
     * Notify listeners
     *
     * client of table support should perform delete on database, and then delegate
     * to this implementation only if delete was successful.
     *
     */
	public void deleteRow( int iRowNumber, boolean bNotify ) {

	    int iOldRowNumber = getCurrentRowNumber();
        if ( isValidRowNumber( iRowNumber ) ) {
            int iLastRow = getNumRowsFetched() - 1;
            for ( int i = iRowNumber; i < iLastRow; i++ ) {
                maRows[i] = maRows[i+1];
            }
            maRows[iLastRow] = null;

            if ( bNotify ) {
                fireTableChange( TableChangeEvent.ROW_DELETED, iRowNumber, 0, null, getTableDelegator(), null );
            }
            miNumRowsFetched--;

            if ( 0 == miNumRowsFetched ) {
                // no rows...
                getTableDelegator().doSetCurrentRowNumber( -1, bNotify, true, null );
            } else if ( iOldRowNumber > iRowNumber ) {
                // something before us was deleted (quietly set row number to one less)
                miCurrentRowNumber--;
            } else if ( iOldRowNumber == iRowNumber ) {
                // we were deleted
                if ( iOldRowNumber == miNumRowsFetched ) {
                    // we're off end -- set row number to -1 (null row)
                    getTableDelegator().doSetCurrentRowNumber( iOldRowNumber - 1, bNotify, true, null );
                } else {
                    // (noisly set row number to same value, different row)
                    getTableDelegator().doSetCurrentRowNumber( iOldRowNumber, bNotify, true, null );
                }
            }
        }

    } // deleteRow


    /**
     * Delete the current row, and notify listeners.
     */
	public void deleteRow() {
	    int iRowNumber = getCurrentRowNumber();
	    deleteRow( iRowNumber, true );
    }

    /**
     * add an empty row
     */
	public void addRow() {
	    int iNumColumns = getNumColumns();

	    Object[] aValues = new Object[iNumColumns];
	    for ( int iColumn = 0; iColumn < iNumColumns; iColumn++ ) {
	        aValues[iColumn] = new String( "" );
        }
        addRowValues( aValues, true );
    }


    /**
     * Getter method for "NumRowsFetched" property.  This
     * is the total number of rows satisfying the query if
     * and only if the "rowsPending" property is true.
     */
	public int getNumRowsFetched() {
	    return miNumRowsFetched;
	}


    /**
     * Getter for the booelan "rowsPending" property.
     */
	public boolean getRowsPending() {
	    return mbRowsPending;
	}

    /**
     * No default behavior yet, must be handled by Subclass/Delegator.
     */
    public void fetchMoreRows() {
        // must be implemented by derived class
    }

    /**
     * getCurrentRowNumber allows clients of this Table to get the
     * value of its its "currentRowNumber" property.  It ought to be
     * exposed in the beaninfo as the "read" method for this property.
     */
	public int getCurrentRowNumber() {
	    return miCurrentRowNumber;
	}


    /**
     * setCurrentRowNumber allows clients of this Table adjust
     * its "currentRowNumber" property.  It ought to be exposed in
     * the beaninfo as the "write" method for this property.
     */
	public boolean doSetCurrentRowNumber( int iNewRowNumber, boolean bNotify, boolean bUpdateView, NotifyList oNotifyList ) {
	    if ( isValidRowNumber( iNewRowNumber ) ) {

	        if ( iNewRowNumber != miCurrentRowNumber ) {
	            synchronized ( this ) {
    	            miCurrentRowNumber = iNewRowNumber;

                    // give our curent row values to RowSupport
                    // we don't clone here so it's playing with live data
                    // also note, RowSupport fires PropertyChangeEvent s for us...
                    setColumnValues( (Object[])maRows[miCurrentRowNumber], bNotify );
               }
            	if ( bNotify ) {
            	    fireTableChange( TableChangeEvent.CURRENT_ROW_NUMBER_CHANGED, iNewRowNumber, 0, null, getTableDelegator(), oNotifyList );
            	}
        	}
        	return true;
        }
        return false;
	}


    public boolean setCurrentRowNumber( int iNewRowNumber ) {
        return getTableDelegator().doSetCurrentRowNumber( iNewRowNumber, true, true, null );
    }


    /**
     * Change the position of this cursor to the
     * next row.  Return false if there is none.
     */
	public boolean nextRow() {
	    if ( isValidRowNumber( miCurrentRowNumber + 1 ) ) {
            return setCurrentRowNumber( miCurrentRowNumber + 1 );
        } else {
            return false;
        }
    }


    /**
     * Change the position of this cursor to the
     * next row.  Return false if there is none.
     */
	public boolean previousRow() {
	    if ( isValidRowNumber( miCurrentRowNumber - 1 ) ) {
            return setCurrentRowNumber( miCurrentRowNumber-1 );
        } else {
            return false;
        }
    }


    /**
     * Helper to perform actual database update.  This, or updateCurrentRow can be overridden
     * to actually sen data to the database.
     */
    public void doUpdateRow( Object[] aColumnNames, Object[] aNewValues, Object[] aOldValues ) {
        // client of table support should subclass this to send values to database
    }

    /**
     * Return true if current row has been changed
     */
    public boolean isCurrentRowChanged() {
        return isRowChanged();
    }


    /**
     * Update (to underlying database) this table's current row
     */
    public void updateCurrentRow() {
        if ( isCurrentRowChanged() ) {
            getTableDelegator().doUpdateRow( getColumnNames(), getColumnValues(), getOriginalColumnValues() );
            resetOriginalValues();
        }

    }


    // end of TableInfo interface
    //-----------------------------------------------------------------------------------------------------------



    //-----------------------------------------------------------------------------------------------------------
    // TableProvider interface

    public Table getTable() {
        return getTableDelegator();
    }

    public RowProvider getCurrentRowValues() {
        return getTableDelegator(); // ?? or its delegator??
    }

    public void addTableChangeListener( TableChangeListener oListener ) {
        try {
            moTableChangeSupport.addTableChangeListener( oListener );
        } catch ( Exception ex ) {
            System.out.println( "Caught exception in TableSupport.addTableChangeListener" );
        }

   }

    public void removeTableChangeListener( TableChangeListener oListener ) {
        moTableChangeSupport.removeTableChangeListener( oListener );
    }

    public void fireTableChange( int iChangeType, int iRowAffected, int iColumnAffected, Object oNewValue, TableProvider oTableProvider, NotifyList oNotify ) {
        moTableChangeSupport.fireTableChange( iChangeType, iRowAffected, iColumnAffected, oNewValue, oTableProvider, oNotify );
    }

    /**
     * Return the values of this row
     */
    public Object[] getNonCurrentRowValues( int iRowNumber ) {
        Object[] aValues = null;
        if ( isValidRowNumber( iRowNumber ) ) {
            try {
                Object[] aRow = (Object[])maRows[iRowNumber];
                aValues = (Object[])aRow.clone();
            } catch ( Exception ex ) {
                System.out.println( "Caught exception in getRowValues: " + ex );
            }
        }
        return aValues;
    }


    // end of TableProvider interface
    //-----------------------------------------------------------------------------------------------------------



    //-----------------------------------------------------------------------------------------------------------
    // TableReceiver interface

    // notification that our client is trying to change our data
	public void tableChange( TableChangeEvent oEvent ) {

        switch ( oEvent.getChangeType() ) {

            case TableChangeEvent.VALUE_CHANGED: {

                TableProvider oTableProvider = oEvent.getTableProvider();
                int iRow = oEvent.getRowAffected();
                int iColumn = oEvent.getColumnAffected();
                Object oNewValue = oEvent.getNewValue();
                getTableDelegator().setTableValue( iRow, iColumn, oNewValue, oEvent.getNotifyList() );
                return;
            }

            case TableChangeEvent.COLUMN_INFO_CHANGED: {
                TableProvider oTableProvider = (TableProvider)oEvent.getTableProvider();
                getTableDelegator().initializeColumnInfo( oTableProvider );
                return;
            }

            case TableChangeEvent.ROW_INSERTED: {
                TableProvider oTableProvider = oEvent.getTableProvider();
                Object[] aValues = oTableProvider.getNonCurrentRowValues( oEvent.getRowAffected() );

                // false flag disables firing TABLE CHANGE event
                addRowValues( aValues, false );
                moTableChangeSupport.propegateTableChange( oEvent );
                return;
            }

            case TableChangeEvent.ROWS_INSERTED: {
                TableProvider oTableProvider = oEvent.getTableProvider();
                for ( int iRow = oEvent.getRowAffected(); iRow < oTableProvider.getNumRowsFetched(); iRow++ ) {
                    Object[] aValues = oTableProvider.getNonCurrentRowValues( iRow );

                    // false flag disables firing TABLE CHANGE event
                    addRowValues( aValues, false );
                }
                moTableChangeSupport.propegateTableChange( oEvent );
                return;
            }


            case TableChangeEvent.ROW_DELETED: {
                // call delete row in "quiet" mode... we propegate the event ourselves
                getTableDelegator().deleteRow( oEvent.getRowAffected(), false );
                moTableChangeSupport.propegateTableChange( oEvent );
                return;
            }

            case TableChangeEvent.TABLE_EMPTY: {
                synchronized (this) {
                    miNumRowsFetched = 0;
                }
                moTableChangeSupport.propegateTableChange( oEvent );
                return;
            }

            // Table object within same data context share row number
            case TableChangeEvent.CURRENT_ROW_NUMBER_CHANGED: {
                getTableDelegator().doSetCurrentRowNumber( oEvent.getRowAffected(), true, true, oEvent.getNotifyList() );
                return;
            }

            case TableChangeEvent.ALL_CHANGED:
            case TableChangeEvent.NUM_FETCHED:
            case TableChangeEvent.CURRENT_ROW_CHANGED:// not used
            case TableChangeEvent.NON_CURRENT_ROW_CHANGED:
            default: {
                moTableChangeSupport.propegateTableChange( oEvent );
                return;
            }
        }

	} // tableChange


    /**
     * Method to set this table's value at row# iRow and column# iColumn.
     */
    public void setTableValue( int iRow, int iColumn, Object oNewValue, NotifyList oNotifyList ) {
        if ( (!isValidRowNumber(iRow)) || (!isValidColumnNumber(iColumn)) ) {
            return;
        }
        if ( iRow == getCurrentRowNumber() ) {
            // set value and fire RowChange and PropertyChange events (and we hook into this to fire TableChange event)
            setValueByNumber( iColumn, oNewValue, oNotifyList );
        } else {
            synchronized ( this ) {
                ((Object[])maRows[iRow])[iColumn] = oNewValue;
            }
            fireTableChange( TableChangeEvent.VALUE_CHANGED, iRow, iColumn, oNewValue, getTableDelegator(), oNotifyList );
        }

    } /// setTableValue




    // end of TableReceiver interface
    //-----------------------------------------------------------------------------------------------------------


    /**
     * This method sets this table's row's column info.  It gets called in the following cases:
     *
     * A RowChangeEvent.COLUMN_INFO_CHANGED causes our superclass (RowSupport) to call this method to set column info
     *
     * A TableChangeEvent.COLUMN_INFO_CHANGED causes us to call this method.
     *
     * In both cases we first delegate to ROW_SUPPORT, which performs the actual change, and
     * also notifies its RowChangeListener s, and then notify our TableChangeListener s
     */
    public void setColumnNames( String[] aColumnNames, int[] aColumnWidths, NotifyList oNotify ) {
        super.setColumnNames( aColumnNames, aColumnWidths, oNotify );
        fireTableChange( TableChangeEvent.COLUMN_INFO_CHANGED, 0, 0, null, getTableDelegator(), oNotify );
    }


    protected void setRowsPending( boolean bNewValue ) {
        synchronized ( this ) {
            mbRowsPending = bNewValue;
        }
    }

    /**
     * Add rows to this table, and notify listeners.
     * Clients wishing to implement insert ought to
     * do insertAtEnd, then call this method to add values to this table
     */
    protected void addRowValues( Object[] aValues, boolean bNotifyListeners ) {

        if ( aValues.length == getNumColumns() ) {

            int iNewRowNumber = miNumRowsFetched;

            synchronized ( this ) {
                try {
                    maRows[iNewRowNumber] = aValues.clone();
                } catch ( Exception ex ) {
                    System.out.println( "Caught exception in addRowValues: " + ex );
                }
                miNumRowsFetched++;
            }

            if ( bNotifyListeners ) {
                fireTableChange( TableChangeEvent.ROW_INSERTED, iNewRowNumber, 0, null, getTableDelegator(), null );
            }
        }
    }



    /**
     * Override the RowSupport method so that we can notify TableChangeListeners using a TableChangeEvent.
     * This gets called in various cases:
     *
     * our "RowListener" interface gets notified of a rowChangeEvent.ROW_VALUE_CHANGED.
     *
     * our "TableListenter" interface gets called on a TableChangeEvent.TABLE_VALUE_CHANGED
     *
     * our Delegator's view gets changed by the user
     *
     * our Delegator's model gets chagned by the user
     *
     *
     * We split this into two cases: current row and non-current row
     *
     */
    public void setValueByNumber( int iColumnNumber, Object oNewValue, NotifyList oNotifyList ) {
        super.setValueByNumber( iColumnNumber, oNewValue, oNotifyList );
        fireTableChange( TableChangeEvent.VALUE_CHANGED, getCurrentRowNumber(), iColumnNumber, oNewValue, getTableDelegator(), oNotifyList );
    }




    //-------------------------------------------------------------------------------------
    // helpers


    private Table getTableDelegator() {
        return (Table)getRowDelegator();
    }



    public boolean isValidRowNumber( int iRowNumber ) {
        return ( 0 <= iRowNumber && iRowNumber < getNumRowsFetched() );
    }



    //-----------------------------------------------------------------------------------------------------------
    // member variables


    // instance variables

    //-----table data support stuff --------------
    private int miNumRowsFetched = 0; // actuall num rows fetched
    private int miCurrentRowNumber = 0;

    private int miTotalBufferSize = 1000; // not intended for large scale use
    private Object maRows[] = new Object[miTotalBufferSize];
    private int miFetchBufferSize = 0;
    private boolean mbRowsPending;
    //----------------------------------------

    private TableChangeSupport moTableChangeSupport = null;

} // TableSupport
