
//--------------------------------------------------------------------
// Class ClientCacheMgr
//
// Purpose  : manage the client's caches
//--------------------------------------------------------------------

import java.io.*;

public class ClientCacheMgr
{
	private Cache[] theCache;
	private int theCurrentCache;
	private int theNumOfCache;
	private int theCacheSize;


//--------------------------------------------------------------------
// Contructor for ClientCacheMgr
//
// Input	: numOfCache, cacheSize (in Bytes)
// Output   : None
// Purpose  : initialize the cache manager parameters and create
//            each cache entity
//--------------------------------------------------------------------

	ClientCacheMgr(int numOfCache, int cacheSize)
	{
		this.theNumOfCache = numOfCache;
		this.theCacheSize = cacheSize;
		theCurrentCache = 0;
		theCache = new Cache[numOfCache];
		for (int i = 0; i < numOfCache ; ++i) 
			theCache[i] = new Cache(cacheSize);
	}


//--------------------------------------------------------------------
// Synchronized Method readFile
//
// Input	: fileName, lastModified,
//            DataInputStream input, PrintStream output
//            (input and output interface with server)
// Output   : dataPackage (from either cache or server)
// Purpose  : get the file requested by taking from cache if exist.
//            Otherwise request from server.
//--------------------------------------------------------------------

	public synchronized String readFile (int MFTIndex, long lastModified,
										int fileSize, DataInputStream input, 
										PrintStream output)
				throws IOException
	{
		String dataPackage = new String();

		int blockIndex = 0;
		int id = 0, total = 0, tranf = 0;

		if ((id = findCache(MFTIndex)) != -1)
		{
			// the data in cache is no longer valid
			if (theCache[id].theLastModified != lastModified)
			{
				setAvailable(MFTIndex);
			}
		}

		while (true)
		{
			// look in cache
			if ( (id = findCache(MFTIndex, blockIndex)) != -1)
			{
				System.out.println("found in cache " + id); 
				String stringBuffer = new String(theCache[id].theCacheData,
												 0, theCache[id].theDataSize);
				dataPackage += stringBuffer;
				theCache[id].theRefBit = 1;
				++blockIndex;
				total += theCache[id].theDataSize;
				if (theCache[id].endOfFile) break;
				continue;
			}

			if (fileSize == 0)
			{
				id = findVictimFor (MFTIndex);
				theCache[id] = new Cache (theCacheSize);
				theCache[id].theMFTIndex = MFTIndex;
				theCache[id].theLastModified = lastModified;
				theCache[id].theFileSize = fileSize;
				theCache[id].theRefBit = 1;
				break;
			}

			// not in cache request from server
			output.println(Integer.toString(blockIndex));
			String stringBuffer = new String();

			int tmplen, len = 0;
			byte[] bArray = new byte[theCacheSize];
			while ( (tmplen = input.read(bArray)) > 0)
			{
				total += tmplen;
				if (total > fileSize)
				{
					tmplen = fileSize + tmplen - total; 
					total = fileSize;
				}
				len += tmplen;
				tranf += tmplen;
				stringBuffer += new String(bArray, 0, tmplen);
				dataPackage += new String(bArray, 0, tmplen);
				if ( (total < fileSize) && 
					 (len < theCacheSize) ) continue;
					// look for victim to overwrite
				id = findVictimFor (MFTIndex);
				theCache[id] = new Cache (theCacheSize);
					// set caches
				if (len <= theCacheSize)
				{
					stringBuffer.getBytes(0, len,
					                  theCache[id].theCacheData, 0);
					theCache[id].theDataSize = len;
					len = 0;
					stringBuffer = new String();
				}
				else
				{
					stringBuffer.getBytes(0, theCacheSize,
						                  theCache[id].theCacheData, 0);
					theCache[id].theDataSize = theCacheSize;
					len -= theCacheSize;
					stringBuffer = stringBuffer.substring(theCacheSize);
				}

				theCache[id].theMFTIndex = MFTIndex;
				theCache[id].theLastModified = lastModified;
				theCache[id].theFileSize = fileSize;
				theCache[id].theRefBit = 1;
				theCache[id].theBlockIndex = blockIndex;
				++blockIndex;
			
				// end of file
				if (total >= fileSize)
				{
					if (len > 0)
					{
						id = findVictimFor (MFTIndex);
						theCache[id] = new Cache (theCacheSize);
						stringBuffer.getBytes(0, len,
							                  theCache[id].theCacheData, 0);
						theCache[id].theDataSize = len;
						theCache[id].theMFTIndex = MFTIndex;
						theCache[id].theLastModified = lastModified;
						theCache[id].theFileSize = fileSize;
						theCache[id].theRefBit = 1;
						theCache[id].theBlockIndex = blockIndex;
					}
					break;
				}
			}

			// rubbish plus <eof>
			input.readLine();
			break;
		}

		output.println("end request");
		theCache[id].endOfFile = true;
		return dataPackage.substring(0, fileSize);
	}


//--------------------------------------------------------------------
// Synchronized Method writeFile
//
// Input	: fileName, dataPackage,
//            DataInputStream input, PrintStream output
//            (input and output interface with server)
// Output   : None
// Purpose  : write the file to server and update the cache
//--------------------------------------------------------------------

	public synchronized void writeFile (int MFTIndex, String dataPackage,
						     DataInputStream input, PrintStream output)
				throws IOException
	{
		int blockIndex = 0;
		int fileSize = dataPackage.length();
		int nBytesToWrite = fileSize;

		// file was modified.  
		// Hence reset all data in cache concerning this file
		setAvailable(MFTIndex);

		int id = 0;

		if (fileSize == 0)
		{
			id = findVictimFor (MFTIndex);
			theCache[id] = new Cache (theCacheSize);
			theCache[id].theMFTIndex = MFTIndex;
			theCache[id].theRefBit = 1;
		}

		while(nBytesToWrite > 0)
		{
			// look for cache to put the filedata into
			id = findVictimFor(MFTIndex);
			if (nBytesToWrite > theCacheSize)
			{
				theCache[id] = new Cache (theCacheSize);
				dataPackage.getBytes(blockIndex * theCacheSize,
					theCacheSize + blockIndex * theCacheSize,
					theCache[id].theCacheData, 0);

				// send to server
				output.write(theCache[id].theCacheData, 0, theCacheSize);

				// set cache
				theCache[id].theMFTIndex = MFTIndex;
				theCache[id].theFileSize = fileSize;
				theCache[id].theRefBit = 1;
				theCache[id].theBlockIndex = blockIndex;
				theCache[id].theDataSize = theCacheSize;
				++blockIndex;
				nBytesToWrite -= theCacheSize;
			}
			else
			{
				// it is the last block of file
				theCache[id] = new Cache (theCacheSize);
				dataPackage.getBytes(blockIndex * theCacheSize,
	                 nBytesToWrite + blockIndex * theCacheSize,
					 theCache[id].theCacheData, 0);

				// send to server
				output.write(theCache[id].theCacheData, 0, nBytesToWrite);

				// set cache
				theCache[id].theMFTIndex = MFTIndex;
				theCache[id].theFileSize = fileSize;
				theCache[id].theRefBit = 1;
				theCache[id].theBlockIndex = blockIndex;
				theCache[id].theDataSize = nBytesToWrite;
				break;
			}
		}
		
		// End OF File
		theCache[id].endOfFile = true;

		// server feedback with the exact time of lastmodified file
		// client update its content
		long lastModified = Long.parseLong(input.readLine());
		setLastModified(MFTIndex, lastModified);
		return ;
	}

		
//--------------------------------------------------------------------
// Synchronized Method findVictimFor
//
// Input	: fileName
// Output   : victim cacheId  ** SECOND CHANCE (CLOCK) ALGORITHM **
// Purpose  : find victim cache to be overwritten by fileName content
//--------------------------------------------------------------------

	private synchronized int findVictimFor (int MFTIndex)
	{
		int victimId;

		// look for available cache first
		if ((victimId = findAvailable ()) != -1)
		{
			return victimId;
		}

		// All cache are in used, find victim by CLOCK ALGORITHM
		int noOfFileCache = 0;
		while (true)
		{
			victimId = theCurrentCache;
			
			// skip the cache that contain the same file content
			if ( (noOfFileCache < theNumOfCache) &&
					(theCache[victimId].theMFTIndex == MFTIndex) )
			{
				theCurrentCache = (theCurrentCache + 1) % theNumOfCache;
				++noOfFileCache;
				continue;
			}

			// cache that has refBit '0' become victim
			if (theCache[victimId].theRefBit <= 0)
			{
				theCurrentCache = (theCurrentCache + 1) % theNumOfCache;
				return victimId;
			}

			// cache that has refBit more than '0' is given SECOND CHANCE
			--theCache[victimId].theRefBit;

			// CLOCK Hand move next
			theCurrentCache = (theCurrentCache + 1) % theNumOfCache;
			continue;
		}
	}

	
//--------------------------------------------------------------------
// Synchronized Method findAvailable
//
// Input	: None
// Output   : id of cache unoccupied
// Purpose  : find available (unoccupied) cache 
//--------------------------------------------------------------------

	private synchronized int findAvailable ()
	{
		int j = theCurrentCache;
		for (int i = 0 ; i < theNumOfCache ; ++i)
		{
			if (theCache[j].isAvailable()) return j;
			j = (j + 1) % theNumOfCache;
		}

		return -1;
	}


//--------------------------------------------------------------------
// Method findCache (1)
//
// Input	: fileName
// Output   : id of cache with fileName identity
// Purpose  : locate the cache that contain fileName's content
//--------------------------------------------------------------------

	private int findCache(int MFTIndex)
	{
		for (int id = 0 ; id < theNumOfCache ; ++id)
		{
			if (theCache[id].theMFTIndex == MFTIndex)
				return id;
		}		
		return -1;
	}
	
//--------------------------------------------------------------------
// Method findCache (2)
//
// Input	: fileName, fromByte
// Output   : id of cache with fileName identity
// Purpose  : locate the cache that contain fileName's content at exact
//            starting byte offset
//--------------------------------------------------------------------

	private int findCache(int MFTIndex, int blockIndex)
	{
		for (int id = 0 ; id < theNumOfCache ; ++id)
		{
			if ( (theCache[id].theMFTIndex == MFTIndex)
				&& (theCache[id].theBlockIndex == blockIndex) )
				return id;
		}		
		return -1;
	}


//--------------------------------------------------------------------
// Method setAvailable
//
// Input	: fileName
// Output   : None
// Purpose  : set cache that contain fileName's content to become 
//            available cache. (fileName = "$$$" represent unoccupied 
//            cache
//--------------------------------------------------------------------

	private void setAvailable(int MFTIndex)
	{
		for (int id = 0 ; id < theNumOfCache ; ++id)
		{
			if (theCache[id].theMFTIndex == MFTIndex)
				theCache[id] = new Cache (theCacheSize);
		}
		return;
	}


//--------------------------------------------------------------------
// Method setLastModified
//
// Input	: fileName, lastModified
// Output   : None
// Purpose  : update cache that contain fileName's content with the
//            lastModified data.  Usually happen when save an editted
//            file to server and server respond with exact time the
//            file was written to disk.
//--------------------------------------------------------------------

	private void setLastModified(int MFTIndex, long lastModified)
	{
		for (int id = 0 ; id < theNumOfCache ; ++id)
		{
			if (theCache[id].theMFTIndex == MFTIndex)
				theCache[id].theLastModified = lastModified;
		}
		return;
	}


//--------------------------------------------------------------------
// Method printAllCaches, printCache
//
// Input	: None
// Output   : standard output print out all cache identity
// Purpose  : print out all the cache identity
//--------------------------------------------------------------------

	public void printAllCaches()
	{
		for ( int id = 0 ; id < theNumOfCache ; ++id)
		{
			System.out.print("cacheId = " + id + ", ");
			theCache[id].print();
		}
		return;
	}

	public void printCache(int id)
	{
		System.out.println(new String(theCache[id].theCacheData,
			    				      0, theCache[id].theDataSize));
		return;
	}

}

//--------------------------------------------------------------------
// End Class ClientCacheMgr
//--------------------------------------------------------------------

