package simpledb.unittest;

import java.io.*;
import java.util.*;
import java.text.ParseException;
import simpledb.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class Utility {
    /**
     * @return a Type array of length len populated with Type.INT_TYPE
     */
    public static Type[] getTypes(int len) {
	Type[] types = new Type[len];
	for (int i = 0; i < len; ++i) 
	    types[i] = Type.INT_TYPE;
	return types;
    }

    /**
     * @return a TupleDesc with n fields of type Type.INT_TYPE
     */
    public static TupleDesc getTupleDesc(int n) {
	return new TupleDesc(getTypes(n));
    }
    
    /**
     * @return an IntField with value n
     */
    public static Field getField(int n) {
	return new IntField(n);
    }

    /**
     * @return a Tuple with a single IntField with value n and with
     *   RecordID(HeapPageId(1,2), 3)
     */
    public static Tuple getHeapTuple(int n) {
	Tuple tup = new Tuple(getTupleDesc(1));
	tup.setRecordID(new RecordID(new HeapPageId(1, 2), 3));
	tup.setField(0, new IntField(n));
	return tup;
    }

    /**
     * @return a Tuple with an IntField for every element of tupdata
     *   and RecordID(HeapPageId(1, 2), 3)
     */
    public static Tuple getHeapTuple(int[] tupdata) {
	Tuple tup = new Tuple(getTupleDesc(tupdata.length));
	tup.setRecordID(new RecordID(new HeapPageId(1, 2), 3));
	for (int i = 0; i < tupdata.length; ++i)
	    tup.setField(i, new IntField(tupdata[i]));
	return tup;
    }
    
    /**
     * @return a Tuple with a 'width' IntFields each with value n and
     *   with RecordID(HeapPageId(1, 2), 3)
     */
    public static Tuple getHeapTuple(int n, int width) {
	Tuple tup = new Tuple(getTupleDesc(width));
	tup.setRecordID(new RecordID(new HeapPageId(1, 2), 3));
	for (int i = 0; i < width; ++i)
	    tup.setField(i, new IntField(n));
	return tup;
    }

    /**
     * @return a Tuple with a 'width' IntFields with the value tupledata[i]
     *         in each field. 
     *         do not set it's RecordID, hence do not distinguish which 
     *         sort of file it belongs to.
     */
    public static Tuple getTuple(int[] tupledata, int width) {
	if(tupledata.length != width) {
	    System.out.println("get Hash Tuple has the wrong length~");
	    System.exit(1);
	}
	Tuple tup = new Tuple(getTupleDesc(width));
	for (int i = 0; i < width; ++i)
	    tup.setField(i, new IntField(tupledata[i]));
	return tup;
    }

    /**
     * @return a DbIterator over a list of tuples constructed over the data
     *   provided in the constructor.
     * @param width the number of fields in each tuple
     * @param tupdata an array such that the ith element the jth tuple lives
     *   in slot j * width + i
     * @require tupdata.length % width == 0
     * @throws DbException if we encounter an error creating the
     *   TupleIterator
     */
    public static TupleIterator createTupleList(int width, int[] tupdata) 
	throws DbException {
	int i = 0;
	LinkedList<Tuple> tuplist = new LinkedList<Tuple>();
	while (i < tupdata.length) {
	    Tuple tup = new Tuple(getTupleDesc(width));
	    for (int j = 0; j < width; ++j)
		tup.setField(j, getField(tupdata[i++]));
	    tuplist.add(tup);
	}

	return new TupleIterator(getTupleDesc(width), 
				 new LinkedHashSet(tuplist));
    }
    
    /**
     * @return true iff the tuples have the same number of fields and
     *   corresponding fields in the two Tuples are all equal.
     */
    public static boolean compareTuples(Tuple t1, Tuple t2) {
	if (t1.getTupleDesc().numFields() != t2.getTupleDesc().numFields())
	    return false;
	
	for (int i = 0; i < t1.getTupleDesc().numFields(); ++i) {
	    if (!(t1.getField(i).equals(t2.getField(i))))
		return false;
	}

	return true;
    }

    /**
     * Check to see if the DbIterators have the same number of tuples and
     *   each tuple pair in parallel iteration satisfies compareTuples .
     * If not, throw an assertion.
     */
    public static void compareDbIterators(DbIterator expected, 
					  DbIterator actual) 
	throws Exception {

	boolean passedExpected = false;
	try {
	    while (true) {
		passedExpected = false;
		Tuple expectedTup = expected.getNext();
		passedExpected = true;
		Tuple actualTup = actual.getNext();
		
		assertTrue(compareTuples(expectedTup, actualTup));
	    }
	} catch(NoSuchElementException e) {
	    if (!passedExpected) {
		// the exception was thrown on expected.getNext(), so we need to
		// call actual.getNext() one more time as well
		assertTrue(checkExhausted(actual));
	    }
	    // both should be done now 
	    assertTrue(checkExhausted(actual) && checkExhausted(expected));
	}
    }

    /**
     * Check to see if every tuple in expected matches <b>some</b> tuple
     *   in actual via compareTuples. Note that actual may be a superset.
     * If not, throw an assertion.
     */
    public static void matchAllTuples(DbIterator expected, 
				      DbIterator actual) 
	throws Exception {
	
	// TODO(ghuo): this n^2 set comparison is kind of dumb, but we haven't
	// implemented hashCode or equals for tuples.
	boolean matched = false;
	try {
	    while (true) {
		Tuple expectedTup = expected.getNext();
		matched = false;
		actual.rewind();
		
		try {
		    while (true) {
			Tuple next = actual.getNext();
			if (compareTuples(expectedTup, next)) {
			    matched = true;
			    break;
			}
		    }
		} catch (NoSuchElementException e) {
		    // explicitly ignore
		}

		if (!matched) {
		    System.out.println("expected match but found none:");
		    expectedTup.print();
		    throw new Exception("unmatched tuple in expected list");
		}
	    }
	} catch(NoSuchElementException e) {
	    // we've exhausted the expected tuples list. all is well.
	}
    }

    /**
     * Verifies that the DbIterator has been exhausted of all elements.
     */
    public static boolean checkExhausted(DbIterator it) 
	throws TransactionAbortedException, DbException {
	Tuple t;
	try {
	    t = it.getNext();
	} catch (NoSuchElementException e) {
	    return true;
	}
	
	System.out.println("Got unexpexted tuple:");
	t.print();
	return false;
    }
    
    /**
     * @return a byte array containing the contents of the file 'path'
     */
    public static byte[] readFileBytes(String path) throws Exception {
	File f = new File(path);
	InputStream is = new FileInputStream(f);
	byte[] buf = new byte[(int) f.length()];
	
	int offset = 0;
	int count = 0;
	while (offset < buf.length
	       && (count = is.read(buf, offset, buf.length - offset)) >= 0) {
	    offset += count;
	}
    
	// check that we grabbed the entire file
	if (offset < buf.length)
	    throw new IOException("failed to read test data");
	
	// Close the input stream and return bytes
	is.close();
	return buf;
    }
    
    /**
     * A utility method to create a new HeapFile with a single empty page,
     * assuming the path does not already exist. If the path exists, the file
     * will be overwritten. The new table will be added to the Catalog with
     * the specified number of columns as IntFields.
     */
    public static HeapFile createEmptyHeapFile(String path, int cols) 
	throws IOException {
	File f = new File(path);
	// touch the file
	FileOutputStream fos = new FileOutputStream(f);
	fos.write(new byte[0]);
	fos.close();
	
	// create the HeapFile and add it to the catalog
	HeapFile hf = new HeapFile(f);
	Database.getCatalog().addTable(hf, getTupleDesc(cols));
	HeapPageId pid = new HeapPageId(hf.id(), 0);

	HeapPage page = null;
	try {
	    page = new HeapPage(pid, HeapPage.createEmptyPageData(hf.id()));
	} catch (IOException e) {
	    // this should never happen for an empty page; bail;
	    throw new RuntimeException("failed to create empty page in HeapFile");
	}

	hf.writePage(page);
	return hf;
    }

    /**
     * Stub DbFile class for unit testing.
     */
    public static class SkeletonFile implements DbFile {
	private int tableid;

	public SkeletonFile(int tableid) {
	    this.tableid = tableid;
	}

	public Page readPage(PageId id) throws NoSuchElementException {
	    throw new RuntimeException("not implemented");
	}
	
	public int numPages() {
	    throw new RuntimeException("not implemented");
	}
	
	public void writePage(Page p) throws IOException {
	    throw new RuntimeException("not implemented");
	}
	
	public Vector<Page> addTuple(TransactionId tid, Tuple t) 
	    throws DbException, IOException, TransactionAbortedException {
	    throw new RuntimeException("not implemented");
	}
	
	public Page deleteTuple(TransactionId tid, Tuple t) 
	    throws DbException, TransactionAbortedException {
	    throw new RuntimeException("not implemented");
	}
	
	public int bytesPerPage() {
	    throw new RuntimeException("not implemented");
	}
	
	public int id() {
	    return tableid;
	}

	public DbFileIterator iterator(TransactionId tid) {
	    throw new RuntimeException("not implemented");
	}
    }
    
    /**
     * Mock SeqScan class for unit testing.
     */
    public static class MockScan implements DbIterator {
	private int cur, low, high, width;
	
	/**
	 * Creates a fake SeqScan that returns tuples sequentially with 'width'
	 * fields, each with the same value, that increases from low (inclusive)
	 * and high (exclusive) over getNext calls.
	 */
	public MockScan(int low, int high, int width) {
	    this.low = low;
	    this.high = high;
	    this.width = width;
	    this.cur = low;
	}

	public void open() {
	}
	
	public void close() {
	}

	public void rewind() {
	    cur = low;
	}

	public TupleDesc getTupleDesc() {
	    return Utility.getTupleDesc(width);
	}

	public Tuple getNext() throws NoSuchElementException {
	    if (cur >= high)
		throw new NoSuchElementException();

	    Tuple tup = new Tuple(getTupleDesc()); 
	    for (int i = 0; i < width; ++i)
		tup.setField(i, new IntField(cur));
	    cur++;
	    return tup;
	}
    }

    /**
     * Helper class that attempts to acquire a lock on a given page in a new
     * thread.
     *
     * @return a handle to the Thread that will attempt lock acquisition after it
     *   has been started
     */
    static class LockGrabber extends Thread {
	
	TransactionId tid; 
	PageId pid; 
	Permissions perm;
	boolean acquired;
	Exception error;
	Object alock;
	Object elock;

	/**
	 * @param tid the transaction on whose behalf we want to acquire the lock
	 * @param pid the page over which we want to acquire the lock 
	 * @param perm the desired lock permissions
	 */
	public LockGrabber(TransactionId tid, PageId pid, Permissions perm) {
	    this.tid = tid;
	    this.pid = pid;
	    this.perm = perm;
	    this.acquired = false;
	    this.error = null;
	    this.alock = new Object();
	    this.elock = new Object();
	}
	
	public void run() {
	    try {
		Database.getBufferPool().getPage(tid, pid, perm);
		synchronized(alock) {
		    acquired = true;
		}
	    } catch (Exception e) {
		e.printStackTrace();
		synchronized(elock) {
		    error = e;
		}
		
		try {
		    Database.getBufferPool().transactionComplete(tid, false);
		} catch (java.io.IOException e2) {
		    e2.printStackTrace();
		}
	    }
	}

	/**
	 * @return true if we successfully acquired the specified lock
	 */
	public boolean acquired() {
	    synchronized(alock) {
		return acquired;
	    }
	}

	/**
	 * @return an Exception instance if one occured during lock acquisition;
	 *   null otherwise
	 */
	public Exception getError() {
	    synchronized(elock) {
		return error;
	    }
	}
    }
    
}

