package ModSQL;
import java.sql.*;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;

/* $Id: IndexTable_Sort.java,v 1.11 2004/01/04 02:23:08 cvs Exp $
 *
 * Copyright (c) 2004 Chris Studholme <chris.studholme@utoronto.ca>
 *
 * May be copied or modified under the terms of the GNU General Public
 * License.  See COPYING for more information.
 */


/**
 * <p>Implementation of DatabaseTable for tables that are indexed by
 * the ModSQL driver.  
 *
 * <p>This index can be accessed just as the original table is accessed.
 * Access to the column that is indexed will be fast while access to other
 * columns will be slower as data must be loaded from the original table.
 *
 * @author chris.studholme@utoronto.ca
 */
class IndexTable_Sort extends IndexTable {

  protected final static int TYPE = 4;
  protected final static int VERSION = 0;
  protected final static int BUFFERSIZE = 65536;

  /* Index is access as follows:
   *   - first time through, next() only and sorted array is created
   *   - next time through, findNext() is possible
   */

  protected FileInputStream file;
  protected BufferedInputStream buffer;
  protected ObjectInputStream istream;

  protected DatabaseTable table;
  protected int columnindex;
  protected int columntype;

  protected IndexTableEntry[] entries;
  protected int entries_to_read;

  protected boolean before_first;
  protected boolean after_last;

  protected int current_entry;  // index into entries
  protected int current_index;  // within rowset
  protected long current_count; // remaining rows


  /**
   * Constructor.  Open existing index.
   *
   * @param table open table to index
   * @param filename path to index file
   * @throws DatabaseException if there is a problem with the index
   */
  public IndexTable_Sort(DatabaseTable table, String filename) 
    throws DatabaseException {

    this.table=table;
		
    try {
      // read index file header
      file = new FileInputStream(filename);
      buffer = new BufferedInputStream(file,BUFFERSIZE);
      istream = new ObjectInputStream(buffer);
	    
      if (istream.readInt()!=TYPE)
	throw new DatabaseException("index data is wrong type");
      if (istream.readInt()!=VERSION)
	throw new DatabaseException("index data is wrong version");
      if (istream.readLong()!=table.getTableSignature())
	throw new DatabaseException("index signature does not match");
      columnindex=istream.readInt();
      columntype=istream.readInt();
      entries_to_read=istream.readInt();
    }
    catch (Exception e) {
      if (e instanceof DatabaseException)
	throw (DatabaseException)e;
      if (e instanceof FileNotFoundException)
	throw new DatabaseException("index file not found");
      throw new DatabaseException("unknown error loading index",e);
    }

    entries = new IndexTableEntry[entries_to_read];
    current_entry = -1;
    beforeFirst();
  }
    
  /**
   * Close index.
   */
  protected void finalize() {
    close();
  }
    

  //======================================================================
  // Methods for manipulating table
  //======================================================================

  public final void close() {
    try {
      istream.close();
      buffer.close();
      file.close();
    }
    catch (IOException e) {
      // oh well... what can we do?
    }
    istream=null;
    buffer=null;
    file=null;
  }


  //======================================================================
  // Methods for accessing table metadata
  //======================================================================

  public long getRowCount() {
    return -1;
  }

  public long getDistinctCount() {
    return entries.length;
  }

  public long getNullCount() {
    return -1;
  }

  //======================================================================
  // Methods for changing current row
  //======================================================================

  public boolean next() throws DatabaseException {
    if (after_last) return false;

    if (!before_first) {
      if (--current_count>0) {
	if (!table.next())
	  throw new DatabaseException("index out-of-sync with table");
	return true;
      }
      current_count=0;
      if (++current_index<entries[current_entry].rows.rowid.length)
	return true;
    }

    before_first=false;

    if (istream!=null) {
      // still reading from file
      if (entries_to_read<=0) {
	// done reading from file
	close();
	after_last=true;
	return false;
      }
      // read next index entry
      try {
	entries[++current_entry] = (IndexTableEntry)istream.readObject();
	current_index = 0;
	current_count = 0;
	--entries_to_read;
	return true;
      }
      catch (Exception e) {
	throw new DatabaseException("error reading index",e);
      }
    }

    if (++current_entry<entries.length) {
      current_index = 0;
      current_count = 0;
      return true;
    }
    
    // done table
    after_last=true;
    return false;
  }

  public boolean findNext(Object data) throws DatabaseException {
    return findNext(columnindex,data);
  }

  /**
   * Old version of findNext() above that requires a column index.  Do not
   * use this method directly.
   *
   * @throws DatabaseException if there is a problem with the index
   */  
  public boolean findNext(int column, Object data) 
    throws DatabaseException {
    if (istream!=null || column!=columnindex) {
      // we have to do it the slow way
      while (next()) {
	Object o = getObject(column);
	if (data==null) {
	  if (o==null)
	    return true;
	}
	else if (data.equals(o))
	  return true;
      } 
      return false;
    }

    if (!before_first && 
	((data==null && entries[current_entry].data==null) ||
	 (data!=null && data.equals(entries[current_entry].data)))) {
      // current data is still valid
      if (--current_count>0) {
	if (!table.next())
	  throw new DatabaseException("index out-of-sync with table");
	return true;
      }
      current_count=0;
      if (++current_index<entries[current_entry].rows.rowid.length)
	return true;
      // data is unique so no need to search further
      after_last=true;
      return false;
    }

    if (DriverConfig.debugLevel>0)
      System.err.println("ModSQL.IndexTable: binary search '"+data+"'");

    // binary search
    before_first=false;
    IndexTableEntry data_wrap = new IndexTableEntry(data);
    int entry = Arrays.binarySearch(entries,data_wrap);
    if (entry<=current_entry) {
      // data not found
      after_last=true;
      return false;
    }
    current_entry = entry;
    current_index = 0;
    current_count = 0;
    return true;
  }

  public void deleteRow() throws DatabaseException {
    throw new DatabaseException("IndexTable does not support deleteRow()");
  }

  public boolean isBeforeFirst() {
    return before_first;
  }

  public boolean isAfterLast() {
    return after_last;
  }

  public void beforeFirst() throws DatabaseException {
    if (current_entry>=0 && entries_to_read>0) {
      // read remaining data
      try {
	do {
	  entries[++current_entry] = (IndexTableEntry)istream.readObject();
	} while (--entries_to_read>0);
      }
      catch (Exception e) {
	throw new DatabaseException("error reading index",e);
      }
      close();
    }
    before_first=true;
    after_last=false;
    current_entry=-1;
    table.beforeFirst();
  }

  public void afterLast() throws DatabaseException {
    if (istream!=null) {
      // read remaining data
      try {
	while (entries_to_read>0) {
	  entries[++current_entry] = (IndexTableEntry)istream.readObject();
	  --entries_to_read;
	}
      }
      catch (Exception e) {
	throw new DatabaseException("error reading index",e);
      }
      close();
    }
    before_first=false;
    after_last=true;
    current_entry=entries.length;
    table.afterLast();
  }
  
  public Object getRowId() throws DatabaseException {
    return entries[current_entry].rows.rowid[current_index];
  }


  //======================================================================
  // Methods for accessing column data
  //======================================================================
  
  public Object getObject(int column) throws DatabaseException {
    if (column==columnindex) 
      return entries[current_entry].data;

    // seek correct row in underlying table (if necessary)
    if (current_count<=0) {
      Object rowid = entries[current_entry].rows.rowid[current_index];
      if (DriverConfig.debugLevel>1)
	System.err.println("ModSQL.IndexTable: seeking rowid "+rowid);
      if (!table.absolute(rowid))
	throw new DatabaseException("failed to locate row in table");
      current_count = entries[current_entry].rows.count[current_index];
    }

    // go to underlying table for column value
    return table.getObject(column);
  }

  public void updateObject(int column, Object x)
    throws DatabaseException {
    throw new DatabaseException("IndexTable does not support updateObject()");
  }
  public void commitUpdates() throws DatabaseException {
    throw new DatabaseException("IndexTable does not support updateObject()");
  }


  //======================================================================
  // Methods to create index
  //======================================================================

  /**
   * Create new index.
   *
   * @param tablemanager active table manager
   * @param tablename name of table to index
   * @param columnname name of column to index
   * @throws SQLException if there is a problem creating the index
   */
  static void createIndex(DatabaseManager tablemanager,
			  String tablename, String columnname) 
    throws SQLException {
    DatabaseTable table = tablemanager.openTable(tablename);
    if (table==null)
      throw new SQLException("cannot find table '"+tablename+"'");
    int columnindex = table.findColumn(columnname);
    if (columnindex<0)
      throw new SQLException("cannot find column '"+columnname+"'");
    createIndex(table,columnindex);
  }

  /**
   * Create new index.
   *
   * @param tablemanager active table manager
   * @param tablename name of table to index
   * @param columnindex index of column to index
   * @throws SQLException if there is a problem creating the index
   */
  static void createIndex(DatabaseManager tablemanager,
			  String tablename, int columnindex) 
    throws SQLException {
    DatabaseTable table = tablemanager.openTable(tablename);
    if (table==null)
      throw new SQLException("cannot find table '"+tablename+"'");
    createIndex(table,columnindex);
  }

  /**
   * Create new index.
   *
   * @param table open table to index
   * @param columnindex index of column to index
   * @throws SQLException if there is a problem creating the index
   */
  static void createIndex(DatabaseTable table, int columnindex) 
    throws SQLException {
    // scan table
    if (DriverConfig.debugLevel>0)
      System.err.print("ModSQL.IndexTable_Sort: reading... ");
    
    ArrayList rows = new ArrayList();

    try {
      Object data = null;
      Object rowid = null;
      long count=0;

      while (table.next()) {
	Object newdata = table.getObject(columnindex);
	if (rowid==null) {
	  rowid = table.getRowId();
	  data = newdata;
	  count = 1;
	}
	else if ((data==null && newdata==null) || 
		 (data!=null && data.equals(newdata))) {
	  ++count;
	}
	else {
	  // append rowset to rows
	  rows.add(new IndexTableEntry(data,rowid,count));
	  // new data
	  rowid = table.getRowId();
	  data = newdata;
	  count = 1;
	}
      }
      if (rowid!=null)
	rows.add(new IndexTableEntry(data,rowid,count));
    }
    catch (Exception e) {
      throw new SQLExceptionWithCause("failed to read table",e);
    }

    // sort rows
    if (DriverConfig.debugLevel>0)
      System.err.print(rows.size()+" entries, sorting... ");

    IndexTableEntry[] rows_array = new IndexTableEntry[rows.size()];
    rows_array = (IndexTableEntry[])rows.toArray(rows_array);
    Arrays.sort(rows_array);

    // combine entries with same data
    IndexTableEntry[] rows_distinct = new IndexTableEntry[rows.size()];
    int ndistinct=0;
    if (rows_array.length>0)
      rows_distinct[ndistinct++] = rows_array[0];
    for (int i=1; i<rows_array.length; ++i) {
      if (rows_array[i].sameData(rows_distinct[ndistinct-1]))
	rows_distinct[ndistinct-1].rows.mergeWith(rows_array[i].rows);
      else
	rows_distinct[ndistinct++] = rows_array[i];
    }

    if (DriverConfig.debugLevel>0)
      System.err.print(ndistinct+" distinct, saving... ");
    
    /* the following should be saved:
     *   - index type
     *   - index version number
     *   - table signature
     *   - column index
     *   - sql type for column
     *   - number of rows
     *   - the data (rowid and row data)
     */
    try {
      String filename = 
	getIndexFilename(table.getTableName(),
			 table.getColumnName(columnindex));
      FileOutputStream file = new FileOutputStream(filename);
      BufferedOutputStream buffer = new BufferedOutputStream(file,BUFFERSIZE);
      ObjectOutputStream out = new ObjectOutputStream(buffer);

      out.writeInt(TYPE);
      out.writeInt(VERSION);
      out.writeLong(table.getTableSignature());
      out.writeInt(columnindex);
      out.writeInt(table.getColumnType(columnindex));
      out.writeInt(ndistinct);

      for (int i=0; i<ndistinct; ++i) {
	rows_distinct[i].finish();
	out.writeObject(rows_distinct[i]);
      }

      out.flush();
      buffer.flush();
      file.close();

      if (DriverConfig.debugLevel>0)
	System.err.println("done.");
    }
    catch (Exception e) {
      if (DriverConfig.debugLevel>0) {
	System.err.println("FAILED ("+e.toString()+").");
	e.printStackTrace();
      }
      throw new SQLExceptionWithCause("index creation failed",e);
    }
  }
   
};
