package relational;

import java.io.Serializable;
import java.lang.reflect.*;
import java.util.*;
import Tools.Assert;

/**
 * A FromClause identifies an ordered list of tables, possibly by
 * name. Used in a select statement just as FROM is used in SQL.
 */
public class FromClause
	implements Serializable {

	TableDescriptor[] tds;
		// always in sorted order

	////////////////////////////////////////////////////////////
	// Constructors/factories
	////////////////////////////////////////////////////////////

	static Hashtable anonymousCache = new Hashtable();
	public static FromClause createAnonymous(Class c) {
		if (anonymousCache.get(c) == null) {
			anonymousCache.put(c, create(new String[1], new Class[] {c}));
		}
		return (FromClause) anonymousCache.get(c);
	}

	public static FromClause create(String name, Class table) {
		return create(new String[] { name }, new Class[] { table });
	}

	public static FromClause create(String[] names, Class[] tables) {
		if (tables.length != names.length) {
			throw new IllegalArgumentException("arguments must be same length");
		}

		FromClause fc = new FromClause();
		fc.tds = new TableDescriptor[tables.length];
		for (int i=0; i<tables.length; i++) {
			// unnamed tables are given the name of the defining Class
			String namei = names[i]!=null
							? names[i]
							: tables[i].getName();
			fc.tds[i] = new TableDescriptor(namei, tables[i]);
		}
		fc.ensureUniqueNames();
		Arrays.sort(fc.tds);

		return fc;
	}

	public static FromClause union(FromClause fca, FromClause fcb) {
		SortedSet ss = new TreeSet();
		ss.addAll(Arrays.asList(fca.tds));
		ss.addAll(Arrays.asList(fcb.tds));

		FromClause fc = new FromClause();
		fc.tds = (TableDescriptor[]) ss.toArray(new TableDescriptor[ss.size()]);

		// ensure that there is no name duplication
		fc.ensureUniqueNames();
			// if fca and fcb define the same name to mean different classes,
			// the TableDescriptors will compare unequal, so both TDs will
			// be added to fc, so the name duplication will show up here.
		
		return fc;
	}

	public static FromClause trimOne(FromClause fc) {
		// a FromClause with all but the first TableDescriptor of fc.
		// used in InternalDatabase's Painful Join.
		LinkedList l = new LinkedList(Arrays.asList(fc.tds));
		l.removeFirst();

		FromClause newfc = new FromClause();
		newfc.tds = (TableDescriptor[])
			l.toArray(new TableDescriptor[l.size()]);

		return newfc;
	}

	////////////////////////////////////////////////////////////
	// lookup tables, names by index
	////////////////////////////////////////////////////////////

	public int getNumTables() { return tds.length; }

	public String getName(int i) { return tds[i].name; }
	public String getName(Class t) { return getName(getIndex(t)); }

	public int getIndex(String name) {
		TableDescriptor td = new TableDescriptor(name, null);
		return Arrays.binarySearch(tds, td);
	}

	public Class getTable(int i) { return tds[i].table; }

	public boolean hasTable(Class table) {
		return (getIndex(table)>=0);
	}

	public int getIndex(Class table) {
		for (int i=0; i<tds.length; i++) {
			if (tds[i].table.equals(table)) {
				return i;
			}
		}
		return -1;
	}

	public Relational getTableFromRow(Row source, String name) {
		return getTableFromRow(source, getIndex(name));
	}

	public Relational getTableFromRow(Row source, int tableIndex) {
		if (source instanceof Relational) {
			Assert.assert(tds.length==1 && tds[0].table.isInstance(source));
			return (Relational) source;
		} else {
			return source.getTable(this, tableIndex);
		}
	}

	public boolean subsetOf(FromClause superfc) {
		// All column names appear in superfc, and map to the same table.
		Set superSet = new HashSet(Arrays.asList(superfc.tds));
		Set subSet = new HashSet(Arrays.asList(tds));
		return superSet.containsAll(superSet);
	}

	public boolean equals(Object o) {
		if (!(o instanceof FromClause)) {
			return false;
		}
		Set themSet = new HashSet(Arrays.asList(((FromClause)o).tds));
		Set meSet = new HashSet(Arrays.asList(tds));
		return meSet.equals(themSet);
	}

	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append("fromClause[");
		for (int i=0; i<tds.length; i++) {
			sb.append(tds[i].name);
			if (i<tds.length-1) {
				sb.append(", ");
			}
		}
		sb.append("]");
		return sb.toString();
	}

	////////////////////////////////////////////////////////////
	// ColumnSpec conversions
	////////////////////////////////////////////////////////////

	protected ColumnSpec cachedColumnSpec;

	public ColumnSpec getNaturalColumnSpec() {
		// returns a ColumnSpec that contains every field (including
		// primary keys) represented by the tables in this columnspec.
		if (cachedColumnSpec == null) {
			Vector vi = new Vector();
			Vector vf = new Vector();
			for (int ti = 0; ti<tds.length; ti++) {
				// insert column for primary key for this table (class)
				vi.add(new Integer(ti));
				vf.add(FieldDescriptor.get(tds[ti].table));

				// insert a column for each field of this table (class)
				Field[] fl = tds[ti].table.getFields();
				for (int fi = 0; fi<fl.length; fi++) {
					if ((fl[fi].getModifiers() & Modifier.STATIC)!=0) {
						continue;
					}
					vi.add(new Integer(ti));
					vf.add(FieldDescriptor.get(fl[fi], tds[ti].table));
				}
			}

			// no automatic way to convert array types, apparently
			int indices[] = new int[vi.size()];
			FieldDescriptor fds[] = new FieldDescriptor[vf.size()];
			Enumeration ei = vi.elements();
			Enumeration ef = vf.elements();
			int i=0;
			while (ei.hasMoreElements()) {
				Integer ii = (Integer) ei.nextElement();
				FieldDescriptor fd = (FieldDescriptor) ef.nextElement();
				indices[i] = ii.intValue();
				fds[i] = fd;
				++i;
			}
				
			cachedColumnSpec = ColumnSpec.create(this, indices, fds);
		}
		return cachedColumnSpec;
	}

	////////////////////////////////////////////////////////////
	// protected worker stuff
	////////////////////////////////////////////////////////////

	protected void ensureUniqueNames() {
		HashSet usedNames = new HashSet();
			// TODO: a hashset is probably overkill
		for (int i=0; i<tds.length; i++) {
			Assert.assert(!usedNames.contains(tds[i].name),
				"duplicate name in table definition");
			usedNames.add(tds[i].name);
		}
	}
}
