package relational.email;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.tree.*;
import javax.swing.event.*;
import java.lang.reflect.*;
import java.util.Vector;
import java.rmi.RemoteException;
import java.util.*;
import relational.*;

/**
 * The model part of an MVC pattern for specifying an email filter
 * query. The output of the query is what appears in the index of the
 * email GUI.
 */
public class FilterModel
	extends AbstractTableModel
	implements ChangeListener {

	Database db, lastDb;
	Vector messageVector;
	Vector columns;
	Where whereClause;
	CellCache cc;

	public FilterModel() {
		columns = new Vector();
		columns.add("From");
		columns.add("Subject");
		columns.add("Date");
		columns.add("Length");

		messageVector = new Vector();
			// so that updates on this object before it's loaded
			// work okay.

		cc = new CellCache(30);
			// TODO: change cellCache size whenever
			// window grows
	}

	protected void loadData() {
		messageVector = new Vector();	// clear it in case load fails
		// clear the cellCache
		cc.flush();
		if (db!=null && whereClause!=null) {
			// hackish approach for now, until we have a join
			// and unique mechanism on the server.

			String sortHeader = "Subject";
			FromClause hufc = FromClause.create("Header", Header.class);
			ColumnSpec hucs = ColumnSpec.create(hufc,
				new String[] { "Header" },
				new FieldDescriptor[] { Header.f_msg }
				);
			Select selHeaderUndefined = new Select(hucs, hufc,
				new WhereEquals(Header.f_name, sortHeader));
				// this Select statement describes the messages that
				// have 'sortHeader' defined.

			// get all the matching messages
			System.err.println("loading new data "+whereClause);
			// Select sel = new Select(Message.class, whereClause);
			FromClause fc = FromClause.create(
				new String[] { "Message", "Header" },
				new Class[] {Message.class, Header.class});
			ColumnSpec cs = FromClause.createAnonymous(Message.class)	
				.getNaturalColumnSpec();
			// TODO: As of this writing, having a smaller ColumnSpec won't
			// improve performance, since server still sends the entire
			// row over. But eventually it will.
			// To sort by a header value, we need to join the message to
			// that header, so we can filter on the msg key (using the
			// whereClause) and then sort on the subject value.

			Header dummyHeader = new Header(db);
			dummyHeader.name = sortHeader;
			dummyHeader.value = "";	// sort where an empty header would sort
			dummyHeader.synthetic = true;
			dummyHeader.msg_fk = new Double(0.0);
				// matches no Message, is Serializable

			Select sel = new Select(cs, fc,
				new WhereOr(
					new WhereAnd(
						whereClause,
						new WhereAnd(
							new WhereEquals(Header.f_name, sortHeader),
							new WhereJoin(Message.f_primaryKey, Header.f_msg)
						)
					),
					new WhereAnd(
						new WhereAnd(
							whereClause,
							new WhereNot(
								new WhereIn(Message.f_primaryKey,
									selHeaderUndefined)
							)
						),
						new WhereConstant("Header", dummyHeader)
					)
				)
			);
			sel.setOrderBy(new OrderByOne(Header.f_value,
				new SubjectComparator()));

			ResultSet rs = sel.evaluate(db);
			messageVector = rs.getVector();
			System.err.println("loaded new data; num = "+messageVector.size());

			fireTableStructureChanged();
		}
	}

	public Database getDatabase() { return db; }

	public void setDatabase(Database db) {
		this.db = db;
		loadData();
		// TODO: database has changed -- rerun query (go get query
		// from TreeModel again?)
	}

	public int getRowCount() {
		return messageVector.size();
	}

	public int getColumnCount() {
		return columns.size();
	}

	public String getColumnName(int modelIndex) {
		if (columns==null) {
			return "*";
		}
		String columnName = (String) columns.elementAt(modelIndex);
		return columnName;
	}

	class CellCache {
		int cacheSize;
		int depth;
			// how many rows to refill with a single select()
			// window size + depth should be < cacheSize, or else we'll
			// blow out the cache and it'll be useless. But if cacheSize
			// is too big, of course, we'll cache too much locally, wasting
			// memory.
		int rowMap[];
			// rowMap[row%cacheSize]==row => hm is valid for that row
		String values[][];
			// values[row%cacheSize][col] = value for column[col]
		HashMap columnToIndex;
		Where columnWC = null;

		CellCache(int cacheSize) {
			this.cacheSize = cacheSize;

			// prebuild the column part of the where queries, since it
			// doesn't vary as we look up different rows.
			columnToIndex = new HashMap();
				// a way to get from returned data back to which column the
				// data belongs in
			for (int i=0; i<columns.size(); i++) {
				Where w = new WhereEquals(Header.f_name, columns.elementAt(i));
				columnWC = (columnWC==null)
					? w
					: new WhereOr(columnWC, w);
				columnToIndex.put(columns.elementAt(i), new Integer(i));
			}
			depth = 4;

			rowMap = new int[cacheSize];
			values = new String[cacheSize][];
			for (int ri=0; ri<values.length; ri++) {
				values[ri] = new String[columns.size()];
			}

			flush();
		}

		void flush() {
			Arrays.fill(rowMap, -1);
		}

		void fillCache(int needRow) {
			// look up depth rows in the database.
			int row = needRow - (needRow % depth);
				// lookup blocks are aligned on 
			// look up rows (row) through (row+depth-1) in the database,
			// overwriting some of the cache
			int endRow = Math.min(row+depth-1, messageVector.size()-1);
			System.out.println("Fetching table rows "
				+row+" to "+endRow);

			// build row part of query
			HashMap msgToCacheRow = new HashMap();
			Where rowWC = null;
			for (int ri=row; ri<=endRow; ri++) {
				Row mro = (Row) messageVector.elementAt(ri);
				Integer mpk = (Integer) Message.f_primaryKey.get(mro);
				msgToCacheRow.put(mpk, new Integer(ri%cacheSize));
				Where w = new WhereEquals(Header.f_msg, mpk);
				rowWC = (rowWC==null)
					? w
					: new WhereOr(rowWC, w);
			}

			Select sel = new Select(Header.class,
				new WhereAnd(rowWC, columnWC));
			ResultSet rs = sel.evaluate(db);

			// mark all rows valid, since all were queried over
			// empty all column data, so missing data doesn't inherit
			// stale values.
			for (int ri=row; ri<=endRow; ri++) {
				rowMap[ri%cacheSize] = ri;
				Arrays.fill(values[ri%cacheSize], "");
			}

			Iterator iter = rs.iterator();
			while (iter.hasNext()) {
				Relational rval = (Relational) iter.next();
				Integer cRow = (Integer)
					msgToCacheRow.get(Header.f_msg.get(rval));
				String name = (String) Header.f_name.get(rval);
				String value = (String) Header.f_value.get(rval);
				Integer colInt = (Integer) columnToIndex.get(name);
				Tools.Assert.assert(colInt!=null, "got column I didn't expect");
				values[cRow.intValue()][colInt.intValue()] = value;
			}
		}

		String lookup(int row, int col) {
			if (rowMap[row%cacheSize] != row) {
				fillCache(row);
			}

			// now pull requested field out of cache
			return values[row%cacheSize][col];
		}
	}

	public Object getValueAt(int row, int col) {
		try {
			return cc.lookup(row, col);
		} catch (Throwable ex) {
			ex.printStackTrace();
			// don't want to upset the paint process.
			return ex.toString();
		}
	}

	public Message getMessageAt(int row) {
		if (row<messageVector.size()) {
			Row ro = (Row) messageVector.elementAt(row);
			return (Message) ro.getTable(Message.class);
		} else {
			return null;
		}
	}

	public void stateChanged(ChangeEvent e) {
		WherePanel source = (WherePanel) e.getSource();
		whereClause = source.getQuery();
		loadData();
	}

/*
	// support for TreeModelListener interface
	public void treeNodesChanged(TreeModelEvent e) {
		// we're going to get these too often, because refresh() in the
		// tree model gets carried away sending these around.
		System.err.println("treeNodesChanged...");
		Object o = e.getPath()[0];
		if (o instanceof Where) {
			whereClause = (Where) o;
			System.err.println("treeNodesChanged; new is "+whereClause);
			loadData();
		} else {
			System.err.println("Oh dear -- what the heck is that object?");
		}
	}
	public void treeNodesInserted(TreeModelEvent e) {}
	public void treeNodesRemoved(TreeModelEvent e) {}
	public void treeStructureChanged(TreeModelEvent e) {
		// handle it the tame way
		treeNodesChanged(e);
	}
*/
}
