
//--------------------------------------------------------------------
// Class ServerCacheMgr
//
// Purpose  : manage the server's caches
//--------------------------------------------------------------------

import java.io.*;
import java.util.StringTokenizer;


public class ServerCacheMgr
{
	private Cache[] theCache;
	private int theCurrentCache;
	private int theNumOfCache;
	private int theCacheSize;
	private int diskBlockSize;
	private int diskNoOfBlocks;
	private int stdMFTIndex;
	private int stdFSMIndex;
	private int stdRootIndex;

	private DiskDriver diskDriver;
	private MasterFileTable MFT;


//--------------------------------------------------------------------
// Contructor for ServerCacheMgr
//
// Input	: numOfCache, cacheSize (in Bytes)
// Output   : None
// Purpose  : initialize the cache manager parameters and create
//            each cache entity
//--------------------------------------------------------------------

	ServerCacheMgr(int numOfCache, int cacheSize, int blockSize, int noOfBlocks,
				   int stdMFTIndex, int stdFSMIndex, int stdRootIndex)
	{
		theNumOfCache = numOfCache;
		theCacheSize = cacheSize;
		diskNoOfBlocks = noOfBlocks;
		theCurrentCache = 0;

		this.stdMFTIndex = stdMFTIndex;
		this.stdFSMIndex = stdFSMIndex;
		this.stdRootIndex = stdRootIndex;

		theCache = new Cache[numOfCache];
		for (int i = 0; i < numOfCache ; ++i) 
			theCache[i] = new Cache(cacheSize);

		// mount to disk
		diskBlockSize = blockSize;
		try 
		{ 
			diskDriver = new DiskDriver (blockSize);

			byte [] bArray = new byte[blockSize];
			int len = 0;
			String MFTContent = new String();
			String LFMContent = new String();
			String RootContent = new String();

			// block no. 0 is the first block of MFT
			diskDriver.seek(stdMFTIndex * diskBlockSize);
			len = diskDriver.read(bArray);
			MFTContent += new String(bArray);

			// mounting first index process
			MFT = new MasterFileTable(MFTContent.substring
				(0, MFTContent.indexOf("\n") + 1) + "<eof>");

			// read all contents of MFT
			noOfBlocks = MFT.getFileAttribute(stdMFTIndex).getNoOfBlocks ();
			int i = 1;
			while (noOfBlocks > 1)
			{
				diskDriver.seek(MFT.getFileAttribute(stdMFTIndex).getBlockAt(i) 
					* diskBlockSize);
				len = diskDriver.read(bArray);
				MFTContent += new String(bArray);
				i++;
				noOfBlocks--;
			}

			MFT = new MasterFileTable(MFTContent);

			// read all contents of LSM
			noOfBlocks = MFT.getFileAttribute(stdFSMIndex).getNoOfBlocks ();
			i = 0;
			while (noOfBlocks > 0)
			{
				diskDriver.seek(MFT.getFileAttribute(stdFSMIndex).getBlockAt(i) 
					* diskBlockSize);
				len = diskDriver.read(bArray);
				LFMContent += new String(bArray);
				i++;
				noOfBlocks--;
			}

			// read all contents of Root
			noOfBlocks = MFT.getFileAttribute(stdRootIndex).getNoOfBlocks ();
			i = 0;
			while (noOfBlocks > 0)
			{
				diskDriver.seek(MFT.getFileAttribute(stdRootIndex).getBlockAt(i)
					* diskBlockSize);
				len = diskDriver.read(bArray);
				RootContent += new String(bArray);
				i++;
				noOfBlocks--;
			}

			// temp MFT
			MFT = new MasterFileTable(MFTContent, LFMContent);
		}
		catch (IOException e) 
		{ 
			System.out.println(e.toString()); 
		}
	}


//--------------------------------------------------------------------
// Synchronized Method readFile
//
// Input	: fileName, lastModified
// Output   : dataPackage (from either cache or server)
// Purpose  : get the file requested by taking from cache if exist.
//            Otherwise request from disk.
//--------------------------------------------------------------------

	public synchronized void readFile (int MFTIndex, int blockIndex, 
								long lastModified, int fileSize, PrintStream output)
			throws IOException
	{
		String dataPackage = new String();
		FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);

		int nBytesLastRead = 0;
		int nBytesRead = 0;
		int id = 0;
		int total = 0;

		if ((id = findCache(MFTIndex)) != -1)
		{
			// the data in cache is no longer valid
			if (theCache[id].theLastModified != lastModified)
			{
				setAvailable(MFTIndex);
			}
		}

		while (true)
		{
			// find in cache
			if ( (id = findCache(MFTIndex, blockIndex)) != -1)
			{
				System.out.println("found in cache " + id); 
				nBytesRead = theCacheSize;
				output.write(theCache[id].theCacheData, 0, nBytesRead);
				theCache[id].theRefBit = 1;
				++blockIndex;
				total += nBytesRead;
				if (theCache[id].endOfFile) break;
				continue;
			}
			else
			{
				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;
				}
				
				int len = 0;
				while (true)
				{
					// not in cache
					int blockNo = fileAttribute.getBlockAt(blockIndex);
					try { diskDriver.seek(diskBlockSize * blockNo); }
					catch (IOException e) { break; }

					// read from disk
					id = findVictimFor (MFTIndex);
					theCache[id] = new Cache (theCacheSize);
					if ((len = diskDriver.read(theCache[id].theCacheData))
						<= 0) break;
					total += len;
					output.write(theCache[id].theCacheData, 0, len);
					theCache[id].theMFTIndex = MFTIndex;
					theCache[id].theLastModified = lastModified;
					theCache[id].theFileSize = fileSize;
					theCache[id].theRefBit = 1;
					theCache[id].theBlockIndex = blockIndex;
					theCache[id].theDataSize = len;
					++blockIndex;
					if (total >= fileSize) break;
				}
			}
			break;
		}

		output.println("<eof>");

		// End OF File			
		theCache[id].endOfFile = true;
		return;

	}


//--------------------------------------------------------------------
// Synchronized Method readFile
//
// Input	: MFTIndex
// Output   : dataPackage
// Purpose  : get the file requested from disk
//--------------------------------------------------------------------

	public synchronized String readFile (int MFTIndex)
	{
		String dataPackage = new String();
		FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);

		int noOfBlocks = fileAttribute.getNoOfBlocks();
		int BlockNo;
		for (int i = 0 ; i < noOfBlocks ; ++i)
		{
			BlockNo = fileAttribute.getBlockAt(i);
			try 
			{
				diskDriver.seek(diskBlockSize * BlockNo);
				byte [] bArray = new byte [diskBlockSize];
				int len = diskDriver.read(bArray);
				dataPackage += new String(bArray, 0, len);
				fileAttribute.lastAccessed = System.currentTimeMillis();
			}
			catch (IOException e)
			{
				System.out.println(e.toString());
				break;
			}
		}

		MFT.updateEntry(fileAttribute, MFTIndex);
		return dataPackage.substring(0, fileAttribute.size);
	}


//--------------------------------------------------------------------
// Synchronized Method writeFile
//
// Input	: fileName, dataPackage,
// Output   : None
// Purpose  : write the file to disk and update the cache
//--------------------------------------------------------------------

	public synchronized long writeFile (int MFTIndex, int fileSize,
		                                   DataInputStream input)
		throws IOException, NotEnoughSpaceException

	{
		long lastModified;

		// file was modified.  
		// Hence reset all data in cache concerning this file
		setAvailable(MFTIndex);

		// file size is 0
		if (fileSize == 0) 
		{
			writeFile(MFTIndex, new String());

			FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);

			int id = findVictimFor (MFTIndex);
			theCache[id] = new Cache (theCacheSize);
			theCache[id].theMFTIndex = MFTIndex;
			lastModified = fileAttribute.lastModified;
			theCache[id].theLastModified = lastModified;
			theCache[id].endOfFile = true;
			theCache[id].theRefBit = 1;

			return lastModified;
		}


		String stringBuffer = new String();
		String dataPackage = new String();
		int tmplen, len = 0, total = 0, blockIndex = 0;
		int id = 0;

		byte[] bArray = new byte[theCacheSize];
		while ( (tmplen = input.read(bArray)) > 0)
		{
			dataPackage += new String(bArray, 0, tmplen);
			len += tmplen; 
			total += tmplen;
			stringBuffer += 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].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].theFileSize = fileSize;
					theCache[id].theRefBit = 1;
					theCache[id].theBlockIndex = blockIndex;
				}
				break;
			}
		}

		dataPackage = dataPackage.substring(0, fileSize);

		System.out.println( "total " + dataPackage.length() + " bytes transferred");

		// End Of File
		theCache[id].endOfFile = true;

		// server feedback with the exact time of lastmodified file
		// update cache identity
		writeFile (MFTIndex, dataPackage);
		FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);

		lastModified = fileAttribute.lastModified;
		setLastModified(MFTIndex, lastModified);
		return lastModified;
	}


//--------------------------------------------------------------------
// Synchronized Method writeFile
//
// Input	: MFTIndex
// Output   : dataPackage
// Purpose  : write the file to disk
//--------------------------------------------------------------------

	public synchronized void writeFile (int MFTIndex, String dataPackage)
		throws IOException, NotEnoughSpaceException
	{
		FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);

		// update fileAttribute properties
		int len = dataPackage.length();
		fileAttribute.size = len;
		fileAttribute.lastModified = System.currentTimeMillis();
		fileAttribute.lastAccessed = System.currentTimeMillis();
		MFT.updateEntry(fileAttribute, MFTIndex);

		// if it is MFT, this attribute is latest one
		if (MFTIndex == stdMFTIndex) 
			dataPackage = new String(MFT.getContent());

		byte [] bArray = new byte[diskBlockSize];

		// check if extra block needed
		float extraBlocks = 
			(float)(len - fileAttribute.getNoOfBlocks() * diskBlockSize)
			/ diskBlockSize;


		if (extraBlocks > 0)
		{
			boolean flag = ((extraBlocks - (int)extraBlocks) == 0); 
			int moreBlocks = flag ? (int)extraBlocks : (int)extraBlocks + 1;
			if ((moreBlocks >= getNoOfFreeBlocks()) &&
				(MFTIndex != stdMFTIndex))
				throw new NotEnoughSpaceException
				(moreBlocks, getNoOfFreeBlocks());

			MFT.addBlocks(MFTIndex, moreBlocks);
			MFT.reallocate(MFTIndex);
		}
		else
		{
			// check if blocks need to be removed
			float removeBlocks =	
				(float)(getNoOfBlocks(MFTIndex) * diskBlockSize - len - diskBlockSize)
				/ diskBlockSize;
			if (removeBlocks > 0)
			{
				boolean flag = ((removeBlocks - (int)removeBlocks) == 0); 
				int lessBlocks = flag ? (int)removeBlocks : (int)removeBlocks + 1;
				MFT.deleteBlocks(MFTIndex, lessBlocks);
				MFT.reallocate(MFTIndex);
			}
		}
	
		fileAttribute = MFT.getFileAttribute(MFTIndex);

		int i = 0;
		while (len > 0)
		{
			if (len > diskBlockSize)
			{
				dataPackage.getBytes(i * diskBlockSize, 
					(i + 1) * diskBlockSize, bArray, 0);
				diskDriver.seek(fileAttribute.getBlockAt(i) * diskBlockSize);
				diskDriver.write(bArray, 0, diskBlockSize);
				len -= diskBlockSize;
			}
			else
			{
				dataPackage.getBytes(i * diskBlockSize,
					i * diskBlockSize + len, bArray, 0);
				diskDriver.seek(fileAttribute.getBlockAt(i) * diskBlockSize);
				diskDriver.write(bArray, 0, len);
				len -= diskBlockSize;
				break;
			}
			bArray = new byte[diskBlockSize];
			++i;
		}

		return;
	}


//--------------------------------------------------------------------
// Synchronized Method newFile
//
// Input	: file properties
// Output   : MFTIndex corresponding to that new file
// Purpose  : allocate space for new file
//--------------------------------------------------------------------

	public synchronized int newFile (String fileName, String linkName,
		int linkIndex, String type, int noOfBlocks)
		throws IOException, NotEnoughSpaceException
	{
		if (noOfBlocks >= getNoOfFreeBlocks())
			throw new NotEnoughSpaceException
			(noOfBlocks, getNoOfFreeBlocks());

		int MFTIndex = MFT.newEntry(fileName, linkName, 
							linkIndex, type, noOfBlocks);
		updateMFT();
		return MFTIndex;
	}


//--------------------------------------------------------------------
// Synchronized Method defrag
//
// Input	: None
// Output   : None
// Purpose  : Defragmentation.
//--------------------------------------------------------------------

	public synchronized void defrag()
	{
		int count = MFT.size();
		int MFTIndex, maxLoop = 0;
		int maxFileIndex = 0, maxNoOfBlocks = 0;

		// find the biggest file in the system, skip stdMFTIndex
		for ( MFTIndex = 1 ; MFTIndex < count ; ++MFTIndex )
		{
			int noOfBlocks = getNoOfBlocks(MFTIndex);
			maxFileIndex = maxNoOfBlocks > noOfBlocks ? maxFileIndex : MFTIndex;
			maxNoOfBlocks = maxNoOfBlocks > noOfBlocks ? maxNoOfBlocks : noOfBlocks;
		}

		// remove out temporary
		String maxDataPackage = readFile(maxFileIndex);
		MFT.deleteBlocks(maxFileIndex, maxNoOfBlocks);

		// defragmentation
		while (!MFT.defragOK(diskNoOfBlocks))
		{
			if (maxLoop > 3) break;

			// compack all files to the beginning of disk
			for ( MFTIndex = 0 ; MFTIndex < count ; ++MFTIndex )
			{
				if ((!MFT.isAvailable(MFTIndex)) && 
					(MFTIndex != maxFileIndex))
				{
					try
					{
						String dataPackage = readFile(MFTIndex);
						MFT.reallocate(MFTIndex);
						writeFile(MFTIndex, dataPackage);
					}
					catch (Exception e) { }
				}
			}
			++maxLoop;
		}

		// put biggest file back
		try 
		{ 
			writeFile(maxFileIndex, maxDataPackage);
			MFT.trim();
			updateMFT();
			MFT.validate();
		}
		catch (Exception e) { }

		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	: MFTIndex, 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 setLastAccessed
//
// Input	: MFTIndex
// Output   : None
// Purpose  : time stamp for each access
//--------------------------------------------------------------------

	public void setLastAccessed(int MFTIndex)
	{
		FileAttribute fileAttribute = MFT.getFileAttribute(MFTIndex);
		fileAttribute.lastAccessed = System.currentTimeMillis();

		MFT.updateEntry(fileAttribute, MFTIndex);
		return;
	}


//--------------------------------------------------------------------
// Method format
//
// Input	: noOfBlocks required for harddisk
// Output   : none
// Purpose  : format and prepare harddisk
//--------------------------------------------------------------------

	public synchronized String format(int noOfBlocks, String volumeName, 
										PrintStream output)
			throws IOException, NotEnoughSpaceException
	{
		String feedback = new String();

		byte [] bArray = new byte[diskBlockSize];
		diskDriver.seek(0);
		for (int i = 0 ; i < noOfBlocks ; )
		{
			diskDriver.write(bArray, 0, diskBlockSize);
			feedback += Integer.toString(i) + "\t";
			i++;
			if (i%10 == 0) feedback += "\n";
		}


		// write to disk FSM and MFT
		MFT = new MasterFileTable(diskNoOfBlocks, volumeName + ":");

		// write to disk new root directory
		DirFile newDirFile = 
			new DirFile (stdMFTIndex, stdFSMIndex, stdRootIndex, volumeName + ":");
		writeFile(stdRootIndex, newDirFile.getContent());

		// write MFT and FSM to disk
		updateMFT();

		if (feedback.endsWith("\n"))
			return (feedback +
			        Integer.toString(MFT.getNoOfFreeBlocks() * diskBlockSize) +
					" bytes free.");
		else
			return (feedback + "\n" +
			        Integer.toString(MFT.getNoOfFreeBlocks() * diskBlockSize) +
					" bytes free.");
	}


//--------------------------------------------------------------------
// Method updateMFT
//
// Input	: none
// Output   : none
// Purpose  : write MasterFileTable and FreeSpaceMgr to disk
//--------------------------------------------------------------------
	
	public synchronized void updateMFT()
		throws IOException, NotEnoughSpaceException
	{
		// update FreeSpaceMgr (FSM) and MasterFileTable (MFT)
		writeFile(stdFSMIndex, MFT.getFSMContent());
		writeFile(stdMFTIndex, MFT.getContent());
		return;
	}


//--------------------------------------------------------------------
// Method getFileName
//
// Input	: MFTIndex
// Output   : fileName
// Purpose  : get fileName at MFTIndex
//--------------------------------------------------------------------

	public String getFileName(int MFTIndex)
	{
		return MFT.getFileName(MFTIndex);
	}


//--------------------------------------------------------------------
// Method getLastModified
//
// Input	: MFTIndex
// Output   : lastModified
// Purpose  : get LastModified at MFTIndex
//--------------------------------------------------------------------

	public long getLastModified(int MFTIndex)
	{
		return MFT.getLastModified(MFTIndex);
	}


//--------------------------------------------------------------------
// Method getHardLinkCount
//
// Input	: MFTIndex
// Output   : hardLinkCount
// Purpose  : get hardLinkCount at MFTIndex
//--------------------------------------------------------------------

	public int getHardLinkCount(int MFTIndex)
	{
		return MFT.getFileAttribute(MFTIndex).hardLinkCount;
	}


//--------------------------------------------------------------------
// Method setHardLinkCount
//
// Input	: MFTIndex
// Output   : hardLinkCount
// Purpose  : set hardLinkCount at MFTIndex
//--------------------------------------------------------------------

	public void setHardLinkCount(int MFTIndex, int hardLinkCount)
	{
		MFT.getFileAttribute(MFTIndex).hardLinkCount = hardLinkCount;
		return;
	}


//--------------------------------------------------------------------
// Method getSize
//
// Input	: MFTIndex
// Output   : size
// Purpose  : get fileName at MFTIndex
//--------------------------------------------------------------------

	public int getSize(int MFTIndex)
	{
		return MFT.getSize(MFTIndex);
	}


//--------------------------------------------------------------------
// Method getNoOfBlocks
//
// Input	: MFTIndex
// Output   : NoOfFreeBloocks
// Purpose  : get number of blocks
//--------------------------------------------------------------------

	public int getNoOfBlocks(int MFTIndex)
	{
		return MFT.getNoOfBlocks(MFTIndex);
	}


//--------------------------------------------------------------------
// Method getNoOfFreeBloocks
//
// Input	: none
// Output   : NoOfFreeBloocks
// Purpose  : get total number of free blocks
//--------------------------------------------------------------------

	public int getNoOfFreeBlocks()
	{
		return MFT.getNoOfFreeBlocks();
	}


//--------------------------------------------------------------------
// Method getLinkName
//
// Input	: MFTIndex
// Output   : linkName
// Purpose  : get linkName at MFTIndex
//--------------------------------------------------------------------

	public String getLinkName(int MFTIndex)
	{
		return MFT.getLinkName(MFTIndex);
	}


//--------------------------------------------------------------------
// Method getLinkIndex
//
// Input	: MFTIndex
// Output   : linkIndex
// Purpose  : get linkIndex at MFTIndex
//--------------------------------------------------------------------

	public int getLinkIndex(int MFTIndex)
	{
		return MFT.getLinkIndex(MFTIndex);
	}


//--------------------------------------------------------------------
// Method isLink
//
// Input	: MFTIndex
// Output   : flag true if it is of link
// Purpose  : check if fileAttribute is of type Link
//--------------------------------------------------------------------

	public boolean isLink(int MFTIndex)
	{
		return MFT.isLink(MFTIndex);
	}


//--------------------------------------------------------------------
// Method isDirectory
//
// Input	: MFTIndex
// Output   : flag true if it is of Directory
// Purpose  : check if fileAttribute is of type Directory
//--------------------------------------------------------------------

	public boolean isDirectory(int MFTIndex)
	{
		return MFT.isDirectory(MFTIndex);
	}


//--------------------------------------------------------------------
// Method isAvailable
//
// Input	: MFTIndex
// Output   : flag true if it is available entry
// Purpose  : check if fileAttribute is available entry
//--------------------------------------------------------------------

	public boolean isAvailable(int MFTIndex)
	{
		return MFT.isAvailable(MFTIndex);
	}


//--------------------------------------------------------------------
// Method isSystem
//
// Input	: MFTIndex
// Output   : flag true if it is of System
// Purpose  : check if fileAttribute is of type System
//--------------------------------------------------------------------

	public boolean isSystem(int MFTIndex)
	{
		return MFT.isSystem(MFTIndex);
	}


//--------------------------------------------------------------------
// Method mkdir
//
// Input	: currDir
// Output   : none
// Purpose  : fileName
//--------------------------------------------------------------------

	public synchronized int mkdir(String newDirName, DirFile currentDir)
		throws IOException, NotEnoughSpaceException
	{
		int newIndex = MFT.newEntry(newDirName, "-", 0, "d", 1);
		DirFile newDir = new DirFile (currentDir.getMFTIndexAt(0), newIndex, 
									  currentDir.getAbsolutePath(), newDirName);

		writeFile (newIndex, newDir.getContent());
		updateMFT();
		return newIndex;
	}


//--------------------------------------------------------------------
// Method rmdir
//
// Input	: opt, delDirName, relDir
// Output   : none
// Purpose  : remove directory return with feedback
//--------------------------------------------------------------------

	public synchronized DirFile rmdir(String opt, String delDirName, DirFile relDir)
		throws RequestFailException
	{
		if (delDirName.length() == 0)
		{
			if (relDir.isEmpty())
				return relDir;
			else if (opt.equals("-r"))
			{
				relDir = rmAllFilesAndDirUnder(relDir);
				try { updateMFT(); }
				catch (Exception e) { }
				return relDir;
			}
			else
				throw new RequestFailException
				(relDir.getAbsolutePath() + " is not empty.");
		}

		int delDirIndex = relDir.getValue(delDirName);

		if (!MFT.isDirectory(delDirIndex))
		{
			throw new RequestFailException
			(relDir.getAbsolutePath() + delDirName + " is not directory.");
		}

		DirFile delDir = new DirFile(readFile(delDirIndex),
									 relDir.getAbsolutePath(),
									 delDirName);

		if (delDir.isEmpty()) { }
		else if (opt.equals("-r"))
		{
			rmAllFilesAndDirUnder(delDir);
			try { updateMFT(); }
			catch (Exception e) { }
		}
		else
			throw new RequestFailException
			(relDir.getAbsolutePath() + delDirName + " is not empty.");

		if (MFT.getFileAttribute(delDirIndex).hardLinkCount > 1)
			--MFT.getFileAttribute(delDirIndex).hardLinkCount;
		else
			MFT.deleteEntry(delDirIndex);

		try { updateMFT(); }
		catch (Exception e) { }
		return relDir;
	}


//--------------------------------------------------------------------
// Method rmAllFilesAndDirUnder
//
// Input	: relDir
// Output   : empty relDir
// Purpose  : recursive rmAllFilesAndDirUnder relDir
//            when called MFT must be updated
//--------------------------------------------------------------------

	private synchronized DirFile rmAllFilesAndDirUnder(DirFile relDir)
	{
		while (relDir.getNoOfFiles() > 2)
		{
			int MFTIndex = relDir.getMFTIndexAt(2);
			String fileName = relDir.getFileName(2);

			if (MFT.getFileAttribute(MFTIndex).hardLinkCount > 1)
			{
				--MFT.getFileAttribute(MFTIndex).hardLinkCount;
				try { relDir.deleteEntry(fileName); }
				catch (Exception e) { }
				continue;
			}

			if (MFT.isDirectory(MFTIndex))
			{
				DirFile delDir = new DirFile
						(readFile(MFTIndex),
						relDir.getAbsolutePath(),
						 fileName);
				if (!delDir.isEmpty())
					rmAllFilesAndDirUnder(delDir);
			}
			try 
			{ 
				relDir = rmSingleFile("-r", fileName, relDir); 
			}
			catch (Exception e) { } 
		}
		return relDir;
	}

	
//--------------------------------------------------------------------
// Method rmAllFilesUnder
//
// Input	: relDir
// Output   : empty relDir
// Purpose  : recursive rmAllFilesUnder relDir
//            when called MFT must be updated
//--------------------------------------------------------------------

	private synchronized DirFile rmAllFilesUnder(DirFile relDir)
	{
		int i = 2;
		while ( i < relDir.getNoOfFiles())
		{
			int MFTIndex = relDir.getMFTIndexAt(i);
			String fileName = relDir.getFileName(i);
			if (!(MFT.isDirectory(MFTIndex)) &&
				!(MFT.isSystem(MFTIndex)))
			{
				if (MFT.getFileAttribute(MFTIndex).hardLinkCount > 1)
				{
					--MFT.getFileAttribute(MFTIndex).hardLinkCount;
				}
				else
				{
					MFT.deleteEntry(MFTIndex);
				}
				try { relDir.deleteEntry(fileName); }
				catch (Exception e) { }
			}
			else i++;
		}

		return relDir;
	}


//--------------------------------------------------------------------
// Method rmSingleFile
//
// Input	: opt, fileName relDir
// Output   : new relDir
// Purpose  : to rm single file from relDir
//--------------------------------------------------------------------

	public synchronized DirFile rmSingleFile(String opt, 
							String fileName, DirFile relDir)
		throws RequestFailException
	{

		if ((fileName.indexOf("/") != -1) ||
			(fileName.indexOf("*") != -1))
			throw new RequestFailException
			("rmSingleFile() take pure fileName.");

		int MFTIndex = relDir.getValue(fileName);
		if (MFT.isSystem(MFTIndex))
			throw new RequestFailException
			("System file cannot be deleted.");

		if (MFT.isDirectory(MFTIndex))
		{
			DirFile delDir = new DirFile
					(readFile(MFTIndex),
					relDir.getAbsolutePath(),
					 fileName);
			if (delDir.isEmpty()) { }
			else if (opt.equals("-r"))
				rmAllFilesAndDirUnder(delDir);
			else
				throw new RequestFailException
				("directory is not empty.");
		}

		if (MFT.getFileAttribute(MFTIndex).hardLinkCount > 1)
			--MFT.getFileAttribute(MFTIndex).hardLinkCount;
		else
			MFT.deleteEntry(MFTIndex);

		try
		{
			relDir.deleteEntry(fileName);
			updateMFT();
		}
		catch (Exception e) { }
		return relDir;
	}


//--------------------------------------------------------------------
// Method mvSingleFile
//
// Input	: srcName, srcDir -> dstName, dstDir
// Output   : new dstDir
// Purpose  : to mv single file from one location to another
//--------------------------------------------------------------------

	public synchronized void mvSingleFile(String srcName, DirFile srcDir, 
							String dstName, DirFile dstDir)
		throws  RequestFailException,
				FileNotExistException,
				NotEnoughSpaceException,
				IOException,
				FileAlreadyExistException
	{
		if ((srcName.indexOf("/") != -1) ||
			(srcName.indexOf("*") != -1) ||
			(dstName.indexOf("/") != -1) ||
			(dstName.indexOf("*") != -1))
			throw new RequestFailException
			("mvSingleFile() take pure fileName.");

		int srcMFTIndex = srcDir.getValue(srcName);
		MFT.getFileAttribute(srcMFTIndex).fileName = dstName;
		dstDir.newEntry(dstName, srcMFTIndex);
		srcDir.deleteEntry(srcName);

		writeFile(srcDir.getValue("."), srcDir.getContent());
		writeFile(dstDir.getValue("."), dstDir.getContent());
		updateMFT();

		return ;
	}


//--------------------------------------------------------------------
// Method cpAllFilesAndDirUnder
//
// Input	: srcDir -> dstDir
// Output   : new dstDir
// Purpose  : recursive cpAllFilesAndDirUnder from one to another
//--------------------------------------------------------------------
	
	private synchronized DirFile cpAllFilesAndDirUnder
		(DirFile srcDir, DirFile dstDir)
		throws NotEnoughSpaceException,
		       FileAlreadyExistException,
			   IOException
	{
		int i = 2;
		int noOfFiles = srcDir.getNoOfFiles();
		while (i < noOfFiles)
		{
			int srcMFTIndex = srcDir.getMFTIndexAt(i);
			String srcName = srcDir.getFileName(i);

			if (MFT.isDirectory(srcMFTIndex))
			{
				String dstName = srcName;
				int dstMFTIndex = newFile(dstName, MFT.getLinkName(srcMFTIndex),
					MFT.getLinkIndex(srcMFTIndex), MFT.getType(srcMFTIndex), 1);
				DirFile subSrcDir = new DirFile
					(readFile(srcMFTIndex),
					srcDir.getAbsolutePath(),
					 srcName);
				DirFile subDstDir = new DirFile 
					(dstDir.getValue("."), dstMFTIndex, 
					dstDir.getAbsolutePath(), dstName);

				subDstDir = cpAllFilesAndDirUnder(subSrcDir, subDstDir);
				writeFile(subDstDir.getValue("."), subDstDir.getContent());
				dstDir.newEntry(dstName, dstMFTIndex);
			}
			else
			{
				try
				{
					String dstName = srcName;
					if (!MFT.isSystem(srcMFTIndex))
						dstDir = cpSingleFile(srcName, srcDir,
							dstName, dstDir);
				}
				catch (Exception e) { } 
			}

			i++;
			continue;
		}
		return dstDir;
	}


//--------------------------------------------------------------------
// Method cpSingleFile
//
// Input	: srcName, srcDir -> dstName, dstDir
// Output   : new dstDir
// Purpose  : to cp single file from one location to another
//--------------------------------------------------------------------

	public synchronized DirFile cpSingleFile(String srcName, DirFile srcDir, 
							String dstName, DirFile dstDir)
		throws  RequestFailException,
				FileNotExistException,
				NotEnoughSpaceException,
				IOException,
				FileAlreadyExistException
	{
		if ((srcName.indexOf("/") != -1) ||
			(srcName.indexOf("*") != -1) ||
			(dstName.indexOf("/") != -1) ||
			(dstName.indexOf("*") != -1))
			throw new RequestFailException
			("cpSingleFile() take pure fileName.");

		int srcMFTIndex = srcDir.getValue(srcName);

		int dstMFTIndex;

		if (MFT.isDirectory(srcMFTIndex))
		{
			dstMFTIndex = newFile(dstName, MFT.getLinkName(srcMFTIndex),
				MFT.getLinkIndex(srcMFTIndex), MFT.getType(srcMFTIndex), 1);

			DirFile subSrcDir = new DirFile
				(readFile(srcMFTIndex),
				srcDir.getAbsolutePath(),
				 srcName);
			DirFile subDstDir = new DirFile 
				(dstDir.getValue("."), dstMFTIndex, 
				dstDir.getAbsolutePath(), dstName);

			subDstDir = cpAllFilesAndDirUnder(subSrcDir, subDstDir);
			writeFile(subDstDir.getValue("."), subDstDir.getContent());
		}
		else
		{
			if (dstDir.isExist(dstName))
				throw new FileAlreadyExistException
				(dstDir.getAbsolutePath() + dstName);

			String type = isSystem(srcMFTIndex) ? "t" :
				MFT.getType(srcMFTIndex);
			
			dstMFTIndex = newFile(dstName, MFT.getLinkName(srcMFTIndex),
				MFT.getLinkIndex(srcMFTIndex), type, 1);

			writeFile(dstMFTIndex, readFile(srcMFTIndex));
		}

		dstDir.newEntry(dstName, dstMFTIndex);
		writeFile(dstDir.getValue("."), dstDir.getContent());
		updateMFT();
		return dstDir;
	}



//--------------------------------------------------------------------
// Method cd
//
// Input	: currentDir, nextDirName
// Output   : content of nextDir
// Purpose  : to get content of next directory
//--------------------------------------------------------------------

	public String cd(DirFile currentDir, String nextDirName)
	{
		int nextDirIndex = currentDir.getValue(nextDirName);
		String nextDirContent = new String();

		if (!MFT.isDirectory(nextDirIndex))
		{
			return nextDirName + " is not directory.";
		}
		
		return readFile(nextDirIndex);
	}


//--------------------------------------------------------------------
// Method ls
//
// Input	: command(option) and dirFile
// Output   : content of dirFile
// Purpose  : list directory
//--------------------------------------------------------------------

	public String ls (String opt, String fileArgs, DirFile relDir)
	{
		String dataPackage = new String();
		int noOfFiles = relDir.getNoOfFiles();
		int bytesUsed = 0;
		int i = 0, j = 0 , k = 0; 
		int MFTIndex;

		// long option is requested
		if (opt.startsWith("-l"))
		{
			if ((j = fileArgs.indexOf("*")) == -1)
			{
				// wildcard(*) not used
				for (k = 0; k < noOfFiles ; k++)
				{
					MFTIndex = relDir.getMFTIndexAt(k);
					if (fileArgs.equals(relDir.getFileName(k)) &&
						(opt.equals("-la") || !isSystem(MFTIndex)))
					{
						dataPackage += MFT.getFileAttribute(MFTIndex).getDescr();
						if (MFT.isLink(MFTIndex))
						{
							dataPackage += "      <LINK>      "
								+ relDir.getFileName(k) + " "
								+ MFT.getLinkName(MFTIndex) + "\n";
						}
						else if (MFT.isDirectory(MFTIndex))
						{
							dataPackage += "      <DIR>       "
								+ relDir.getFileName(k) + "\n";
						}
						else
						{
							String size = Integer.toString(MFT.getSize(MFTIndex));
							while (size.length() < 10)
								size = " " + size;
							dataPackage += size + " bytes  "
								+ relDir.getFileName(k) + "\n";
							bytesUsed += 
								MFT.getFileAttribute(MFTIndex).size;
						}
						++i;
					}
				}
			}
			else
			{
				// wildcard(*) used
				if (j == 0)
				{
					fileArgs = fileArgs.substring(j+1); 
					for (k = 0; k < noOfFiles ; k++)
					{
						MFTIndex = relDir.getMFTIndexAt(k);
						if (relDir.getFileName(k).endsWith(fileArgs) &&
							(opt.equals("-la") || !isSystem(MFTIndex)))
						{
							dataPackage += MFT.getFileAttribute(MFTIndex).getDescr();
							if (MFT.isLink(MFTIndex))
							{
								dataPackage += "      <LINK>      "
									+ relDir.getFileName(k) + " "
									+ MFT.getLinkName(MFTIndex) + "\n";
							}
							else if (MFT.isDirectory(MFTIndex))
							{
								dataPackage += "      <DIR>       "
									+ relDir.getFileName(k) + "\n";
							}
							else
							{
								String size = Integer.toString(MFT.getSize(MFTIndex));
								while (size.length() < 10)
									size = " " + size;
								dataPackage += size + " bytes  "
									+ relDir.getFileName(k) + "\n";
								bytesUsed += 
									MFT.getFileAttribute(MFTIndex).size;
							}
						++i;
						}
					}
				}
				else
				{
					fileArgs = fileArgs.substring(0, j); 
					for (k = 0; k < noOfFiles ; k++)
					{
						MFTIndex = relDir.getMFTIndexAt(k);
						if (relDir.getFileName(k).startsWith(fileArgs) &&
							(opt.equals("-la") || !isSystem(MFTIndex)))
						{
							dataPackage += MFT.getFileAttribute(MFTIndex).getDescr();
							if (MFT.isLink(MFTIndex))
							{
								dataPackage += "      <LINK>      "
									+ relDir.getFileName(k) + " "
									+ MFT.getLinkName(MFTIndex) + "\n";
							}
							else if (MFT.isDirectory(MFTIndex))
							{
								dataPackage += "      <DIR>       "
									+ relDir.getFileName(k) + "\n";
							}
							else
							{
								String size = Integer.toString(MFT.getSize(MFTIndex));
								while (size.length() < 10)
									size = " " + size;
								dataPackage += size + " bytes  "
									+ relDir.getFileName(k) + "\n";
								bytesUsed += 
									MFT.getFileAttribute(MFTIndex).size;
							}
						++i;
						}
					}
				}
			}
		}
		else
		{
			if ((j = fileArgs.indexOf("*")) == -1)
			{
				for (k = 0; k < noOfFiles ; k++)
				{
					MFTIndex = relDir.getMFTIndexAt(k);
					if (fileArgs.equals(relDir.getFileName(k)) &&
						!isSystem(MFTIndex))
					{
						String fn = relDir.getFileName(k);
						while (fn.length() < 20)
							fn += " ";
						dataPackage += fn;
						if (i%4 == 3) dataPackage += "\n";
						if (!MFT.isLink(MFTIndex) &&
							!MFT.isDirectory(MFTIndex))
							bytesUsed += MFT.getFileAttribute(MFTIndex).size;
						++i;
					}
				}
			}
			else
			{
				if (j == 0)
				{
					fileArgs = fileArgs.substring(j+1); 
					for (k = 0; k < noOfFiles ; k++)
					{
						MFTIndex = relDir.getMFTIndexAt(k);
						if (((fileArgs.length() == 0) ||
							(relDir.getFileName(k).endsWith(fileArgs))) &&
							!isSystem(MFTIndex))
						{
							String fn = relDir.getFileName(k);
							while (fn.length() < 20)
								fn += " ";
							dataPackage += fn;
							if (i%4 == 3) dataPackage += "\n";
							if (!MFT.isLink(MFTIndex) &&
								!MFT.isDirectory(MFTIndex))
								bytesUsed += MFT.getFileAttribute(MFTIndex).size;
							++i;
						}
					}
				}
				else
				{
					fileArgs = fileArgs.substring(0, j); 
					for (k = 0; k < noOfFiles ; k++)
					{
						MFTIndex = relDir.getMFTIndexAt(k);
						if (relDir.getFileName(k).startsWith(fileArgs) &&
							!isSystem(MFTIndex))
						{
							String fn = relDir.getFileName(k);
							while (fn.length() < 20)
								fn += " ";
							dataPackage += fn;
							if (!MFT.isLink(MFTIndex) &&
								!MFT.isDirectory(MFTIndex))
								bytesUsed += MFT.getFileAttribute(MFTIndex).size;
							++i;
						}
					}
				}
			}
			dataPackage += "\n";
		}

		String nooffiles = Integer.toString(i);
		while (nooffiles.length() < 30)
			nooffiles = " " + nooffiles;
		String noOfBytesUsed = Integer.toString(bytesUsed);
		while (noOfBytesUsed.length() < 23)
			noOfBytesUsed = " " + noOfBytesUsed;
		String noOfBytesFree = Integer.toString(MFT.getNoOfFreeBlocks() * diskBlockSize);
		while (noOfBytesFree.length() < 61)
			noOfBytesFree = " " + noOfBytesFree;

		return "Directory = " + relDir.getAbsolutePath() + "\n" +
			   dataPackage +
			   nooffiles + " file(s)" +
			   noOfBytesUsed + " bytes used\n" +
			   noOfBytesFree + " bytes free";
	}


//--------------------------------------------------------------------
// Method printAllCaches
//
// 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 ServerCacheMgr
//--------------------------------------------------------------------

