import java.sql.ResultSet;

public class Locks {
    public static final String tName = "Locks"; // Name of the this table

    // Establish abbreviations for calling methods in other modules:
    private static Lab3Props per = new Lab3Props();
      // All general utilities for dealing with persistent data
    private static Lab3Props out = per;
      // For output to terminal or browser or other logging/display device

    /* Hard-coded for the Locks table with standard field specs.
       Create the table if it doesn't already exist. */
    public static int mayMakeTable() {
	String fspec = "LKey varchar(10), PRIMARY KEY(LKey), " +
	    "UserName varchar(20)";
	int nrows = per.mayMakeTable(tName, fspec);
	if (nrows>0) {
	    out.printbr("Table " + tName + " exists and has " + nrows +
			" already.");
	    return nrows;
	}
	if (nrows<0) {
	    out.printbr("Bug: Table wasn't created");
	}
	out.printbr("Table " + tName + " exists, but it's empty.");
	return per.tableNrows(tName);
    }

    /* Hard-coded for the Locks table with standard field specs.
       For debugging, show entire table on terminal. */
    public static void showAllRecords() {
	String sql = "SELECT * from " + tName;
	ResultSet rs = per.doQuery(sql);
	while (per.next(rs)) { //Move to first/next row of SQL-query result
	    String sKey = per.getString(rs, "LKey");
	    String sName = per.getString(rs, "UserName");
	    out.printbr(sKey + " // " + sName);
	}
    }

    /* Hard-coded for the Locks table with standard field specs.
       Also for setting up debugging rigs to adapt to initial conditions
        without bombing out, probably not useful in regular code except
	from inside tryLock or unLock after the UPDATE fails, to diagnose
	whether it failed because it was already done or it failed
	because something interferred with it.
       We can count locks with either exact combination of lockname and
        username (pass strings as lockname & username), or all locks by
	lockname ignoring username (pass NULL pointer as username),
	or even audit one particular user by counting all locks by
	that person (pass NULL pointer as lockname but string as username). */
    public static int countLocks(String lockname, String username) {
	return countLocks(lockname, username, false); }
    public static int countLocks(String lockname, String username,
				     boolean trace) {
	String sql = "SELECT count(*) FROM " + tName +
	    (((lockname==null) && (username==null)) ? "" : " WHERE ") +
	    ((lockname==null) ? "" : "LKey='" + lockname + "'") +
	    (((lockname==null) || (username==null)) ? "" : " AND ") +
	    ((username==null) ? "" : "UserName='" + username + "'");
	if (trace) out.printbr("* countLocks: sql = " + sql);
	//boolean abort=true; if (abort) return -9; // Debug trick
	ResultSet rs = per.doQuery(sql);
	int n = per.rsGetSingleInt(rs);
	if (n<0) throw new java.util.MissingResourceException
	    ("*** " + sql + " returned negative number. Cloudscape bug? ",
	     "","");
	return n;
    }

    /* Hard-coded for the Locks table with standard field specs.
       When attempt to set or clear a lock fails, call this to learn
        whether the lock was already as desired (indicating possible
	confusion in logic of program), or lock is still in the wrong
	state (still set after attempt to clear is a serious problem,
	whereas not set when attempted to set simply means somebody
	else currently has the lock set and we need to wait longer,
	assuming username of that lock is different from our own).
       See discussion of countLocks for variety of null/String parameters,
        but here the lockname must be a String, not null, because it
	makes no sense to ask if a lock is set without saying which lock,
	but it still makes sense to ask whether *I* have the lock, and also
	to ask whether *anyone* has this lock.
       Return true if the lock (or any matching lock, when either
        parameter is wildcard null) is set, false if not (any such) set. */
    public static boolean isLockSet(String lockname, String username) {
	return isLockSet(lockname, username, false); }
    public static boolean isLockSet(String lockname, String username,
				    boolean trace) {
	if (lockname==null) throw new java.util.MissingResourceException
	    ("isLockSet called with wildcard lockname, nope, call countLocks "
	     + "directly if you really want that kind of info", "","");
	int n = countLocks(lockname, username, trace);
	if (n<0) throw new java.util.MissingResourceException
	    ("isLockSet: Bug, countLocks returned negative number.", "","");
	if (n>1)
	    throw new java.util.MissingResourceException
		("isLockSet detected mis-configuration of database: More than "
		 + "one record with same " +
		 ((username==null) ? ("LKey='" + lockname + "'")
		  : ("exact LKey&UserName combo: '" + lockname + "' '" +
		     username + "'")), "","");
	return (n>0); // Only 0 and 1 values can reach here, so false=0 true=1
    }

    /* Hard-coded for the Locks table with standard field specs.
       Try to establish lock by adding row to table,
       return true if successful. */
    public static boolean tryLock(String lockname, String username) {
	return tryLock(lockname, username, false); }
    public static boolean tryLock(String lockname, String username,
				  boolean trace) {
	String sql =  "INSERT INTO " + tName + " VALUES ('" + lockname +
	    "', '" + username + "')";
	if (trace) out.printbr("sql = " + sql);
	int nup = per.doUpdate(sql, trace);
	// Pass flag along in case we need to see the actual SQLException
	if (nup<0) {
	    if (trace) out.printbr("Failed to establish lock ('" + lockname +
				   "' '" + username + "')");
	    if (isLockSet(lockname, username))
		throw new java.util.MissingResourceException
		    ("tryLock('" + lockname + "' '" + username + "') failed " +
		     "because that user already had the lock set, s/w bug.",
		     "","");
	    if (trace && isLockSet(lockname, null))
		out.printbr("Somebody else has that lock, and still has " +
			    "the lock a moment later. Maybe Goldlilocks " +
			    "fell asleep?");
	    return false;
	}
	// Get here if we successful established the lock
	if (nup!=1) {
	    out.printbr("*** tryLock: Number of records changed is " + nup +
			" instead of 1.");
	}
	return true;
    }

    /* Hard-coded for the Locks table with standard field specs.    
       Try to establish lock, if fails then keep trying until ntries exhausted.
       Ntries must be greater than zero, or behaviour is undefined.
       Ntries should be greater than one, or you should just call tryLock,
        but with ntries=1 the behaviour is nevertheless well-defined.
       Return -1 if it never succeeded, else positive number of tries needed.
    */
    public static int tryLockRepeatedly(String lockname, String username,
					    int ntries) {
	return tryLockRepeatedly(lockname, username, ntries, false); }
    public static int tryLockRepeatedly(String lockname, String username,
					    int ntries, boolean trace) {
	if (trace) out.print("tryLockRepeatedly('"+ lockname +"' '"+
			     username +"'):");
	for (int used=1; used<=ntries; ++used) {
	    if (used>1) {
		if (trace) out.print("(s..");
		try {
		    Thread.sleep(3000); //Milliseconds
		} catch (InterruptedException ex) {
		    out.printbr("*** Sleep was interrupted: " + ex);
		}
		if (trace) out.print(".w)");
	    }
	    if (trace) out.print(""+used); //Need explicit string here
	    boolean suc = tryLock(lockname, username, false);
	    /* Do *not* pass along trace flag here, it messes things up!!
	       To debug, call tryLock(lockname, username, true);
	        explicitly, bypassing this code until problem is diagnosed. */
	    if (suc) {
		if (trace) out.printbr("!");
		return used;
	    }
	    if (trace) out.print("x");
	}
	if (trace) out.printbr("=GiveUp.");
	return -1;
    }

    /* Hard-coded for the Locks table with standard field specs.    
       Remove a lock previously set. As protection against a system bug
        whereby somebody sets a lock and somebody removes the same lock,
	the username is *required* to remove a lock. Presumably the
	software can accurately keep track of which user is on each thread.
       Return true if lock-removal was successful. Failure leaves an
        indeterminate state where no such lock existed or it existed but
	couldn't be removed.
       Update: We now diagnose those two cases, one of which is a bug
        in the DBMS, other is a bug in our own software, throw unchecked
	exception in both cases, so we never return false value currently. */
    public static boolean unLock(String lockname, String username) {
	return unLock(lockname, username, false); }
    public static boolean unLock(String lockname, String username,
				 boolean trace) {
	String sql = "DELETE FROM " + tName + " WHERE LKey='" + lockname +
	    "' AND UserName='" + username + "'";
	if (trace) out.printbr("sql = " + sql);
	int nup = per.doUpdate(sql, trace);
	// Pass along trace flag, because we often need the info to debug here.
	if (nup<0) {
	    out.printbr("*** unLock (" + sql + "): Entire UPDATE bombed.");
	    throw new java.util.MissingResourceException("Abort", "","");
	}
	if (nup==0) {
	    out.printbr("unLock (" + lockname + " " + username + ") failed " +
			"(UPDATE succeeded, but with 0 records changed)");
	    if (isLockSet(lockname, username))
		throw new java.util.MissingResourceException
		    ("DBMS broken, we have lock we can't get rid of: " +
		     "lockname='" + lockname + "' username='" + username +
		     "'", "","");
	    else
		throw new java.util.MissingResourceException
		    ("Attempt to remove a lock we didn't have in the first " +
		     "place, i.e. s/w bug: lockname='" + lockname +
		     "' username='" + username + "'", "","");
	    // Unreachable code at the moment: return false;
	}
	if (nup!=1)
	    out.printbr("*** unLock: Number of records changed is " + nup +
			" instead of 1, bug here or database config bad.");
	return true;
    }

    public static void main(String args[]) {
	// This is just a bunch of unit-testing, nothing to really run.
	out.printbr("** Starting test rig for Locks...");
	out.printbr("Stand by while we get data to show all records...");
	showAllRecords();

	boolean b;
	b = tryLock("Duo", "Betty", true);
	out.printbr((b ? "A" : "Una") + "ble to establish Duo/Betty.");
	b = unLock("Uno", "Veronica", true);
	out.printbr("unLock Uno/Veronica = " + b);
	b = unLock("Nil", "Veronica", true);
	out.printbr("unLock Nil/Veronica = " + b);

	///
	boolean exitnow=true; if (exitnow) return;

	int nt =  tryLockRepeatedly("Uno", "Trixie", 10, true);
	out.printbr("nt = " + nt);
	showAllRecords();
	showAllRecords();

	b = isLockSet("Uno", null, true);
	out.printbr("Uno/null = " + b);
	b = isLockSet("Una", null, true);
	out.printbr("Una/null = " + b);
	b = isLockSet("Uno", "Betty", true);
	out.printbr("Uno/Betty = " + b);
	b = isLockSet("Uno", "Veronica", true);
	out.printbr("Uno/Veronica = " + b);

	int n000 = countLocks(null, "Peter", true);
	out.printbr("n000 = " + n000);
	int n0 = countLocks(null, null, true);
	out.printbr("n0 = " + n0);
	int n04 = countLocks(null, "Betty", true);
	out.printbr("n04 = " + n04);
	int n02 = countLocks(null, "Veronica", true);
	out.printbr("n02 = " + n02);
	int n1x = countLocks("Uno", "Betty", true);
	out.printbr("n1x = " + n1x);

	showAllRecords();
	///
	out.printbr("** End of test.");
    }

    /*
    */
}
