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

/* $Id: IndexTable_Hash.java,v 1.13 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 using a hash table.  
 *
 * <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_Hash extends IndexTable {

  public static final int TYPE = 3;
  public static final int VERSION = 0;
  public static final int BUFFERSIZE = 65536;

  /* Index is access as follows:
   *   - first time through, next() only and hash map 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 HashMap map;
  protected int rows_to_read;

  protected boolean before_first;
  protected boolean after_last;

  protected Iterator current_iterator;
  protected Object current_data;
  protected IndexTableRowset current_rowset=null;
  protected int current_index;  // within rowset
  protected long current_count;  // remaining rows
  
  /**
   * Constructor.  Open existing hash table.
   *
   * @param table open table to index
   * @param filename path to index file
   * @throws DatabaseException if there is a problem with the index
   */
  public IndexTable_Hash(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();
      rows_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);
    }

    map = new HashMap(rows_to_read);
    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 map.size()+rows_to_read;
  }

  public long getNullCount() {
    return -1;
  }

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

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

    if (current_rowset!=null) {
      // still in same rowset
      if (--current_count>0) {
	if (!table.next())
	  throw new DatabaseException("index out-of-sync with table");
	return true;
      }
      current_count=0;
      if (++current_index<current_rowset.rowid.length)
	return true;
    }

    if (istream!=null) {
      // still reading from file
      before_first=false;
      if (rows_to_read<=0) {
	// done reading index
	close();
	after_last=true;
	return false;
      }
      // read next index entry
      try {
	IndexTableEntry e = (IndexTableEntry)istream.readObject();
	map.put(e.data,e.rows);
	current_data = e.data;
	current_rowset = e.rows;
	current_index = 0;
	current_count = 0;
	--rows_to_read;
	return true;
      }
      catch (Exception e) {
	throw new DatabaseException("error reading index",e);
      }
    }

    if (before_first) {
      // create iterator
      before_first=false;
      current_iterator = map.entrySet().iterator();
    }

    if (current_iterator!=null) {
      if (!current_iterator.hasNext()) {
	// done table
	after_last=true;
	return false;
      }
      Map.Entry e = (Map.Entry)current_iterator.next();
      current_data = e.getKey();
      current_rowset = (IndexTableRowset)e.getValue();
      current_index = 0;
      current_count = 0;
      return true;
    }

    else {
      // findNext() started the scan
      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 (after_last) return false;
    if (!before_first || 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 (DriverConfig.debugLevel>0)
      System.err.println("ModSQL.IndexTable: hash map lookup '"+data+"'");

    // hash table lookup
    before_first=false;
    current_rowset = (IndexTableRowset)map.get(data);
    if (current_rowset==null) {
      // data not found
      after_last=true;
      return false;
    }
    current_data = data;
    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 (map.size()>0 && rows_to_read>0) {
      // read remaining data
      try {
	do {
	  IndexTableEntry e = (IndexTableEntry)istream.readObject();
	  map.put(e.data,e.rows);
	} while (--rows_to_read>0);
      }
      catch (Exception e) {
	throw new DatabaseException("error reading index",e);
      }
      close();
    }
    current_iterator=null;
    current_rowset=null;
    before_first=true;
    after_last=false;
    table.beforeFirst();
  }

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


  //======================================================================
  // Methods for accessing column data
  //======================================================================

  public Object getObject(int column) throws DatabaseException {
    if (column==columnindex)
      return current_data;

    // seek correct row in underlying table (if necessary)
    if (current_count<=0) {
      Object rowid = current_rowset.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 = current_rowset.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_Hash: 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 DatabaseException("failed to read table",e);
    }

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

    HashMap map = new HashMap(rows.size());

    for (Iterator i=rows.iterator(); i.hasNext(); ) {
      IndexTableEntry entry = (IndexTableEntry)i.next();
      IndexTableRowset rowset = (IndexTableRowset)map.get(entry.data);
      if (rowset==null) 
	rowset = entry.rows;
      else
	rowset.mergeWith(entry.rows);
      map.put(entry.data,rowset);
    }

    // write index
    if (DriverConfig.debugLevel>0)
      System.err.print(map.size()+" distinct, saving... ");

    /* the following should be saved:
     *   - type of index
     *   - index version number
     *   - table signature
     *   - column index
     *   - sql type for column
     *   - number of rows
     *   - the data (IndexTableEntry)
     */

    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(map.size());

      Set set = map.entrySet();
      for (Iterator i=set.iterator(); i.hasNext(); ) {
	Map.Entry e = (Map.Entry)i.next();
	Object data = e.getKey();
	IndexTableRowset rowset	= (IndexTableRowset)e.getValue();
	rowset.finish();
	out.writeObject(new IndexTableEntry(data,rowset));
      }
      
      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 DatabaseException("index creation failed",e);
    }
  }
  
      
};
