/**
 * An implementation of the file system.
 */

import java.util.*;
import java.io.*;

class FileSystem 
{
  // Set up the constants for the whence field in seek
  public static final int SEEK_SET    = 0;
  public static final int SEEK_CUR    = 1;
  public static final int SEEK_END    = 2;

  Disk disk;
  FileTable ft;
  SuperBlock sb;

  FileSystem() 
  {
    disk = new Disk();                  // create the disk
    sb = new SuperBlock();              // cashe the superblock
    disk.read(sb);                 	// fill superblock if disk exists
    ft = new FileTable();               // create file table
  }

  // Initialize the the disk to a state representing an empty file-system.
  // Fill in the super block, mark all inodes as "unused", and link
  // all data blocks into the free list.
  public int formatDisk(int size, int iSize) 
  {
    if (iSize > size -1)
    {
      error("Incorrect formatting parameters");
      return(-1);
    }

    sb.size = size;			// compose SuperBlock
    sb.iSize = iSize;
    sb.freeList = iSize + 1;
    disk.write(sb);

    InodeBlock ib = new InodeBlock();   // create empty inode blocks
    for (int i=1; i<=iSize; i++)
      disk.write(i, ib); 

    IndirectBlock indb = new IndirectBlock();
    for (int i=iSize+1; i<size; i++)    // create linked list of free blocks
    {
      if (i < size-1) 
        indb.pointer[0] = i+1;
      else
        indb.pointer[0] = 0;
      disk.write(i, indb);
    }   
    return(0);
  } 


  // Close all files and shut down the simulated disk.
  public int shutdown() 
  {
    throw new RuntimeException("not implemented");
  } 

  // Create a new (empty) file and return a file descriptor.
  public int create(int iNumber) 
  {
    InodeBlock ib = new InodeBlock();
    for (int i=1; i<=sb.iSize; i++)
    {
      disk.read(i, ib);
      for (int j=0; j<ib.node.length; j++)   // look for a free inode 
      {  
        Inode inode = ib.node[j];
        if (inode.flags == 0)                // free inode is found
        {
          int fileDescr = ft.allocate();     // request place in FT
          if (fileDescr >= 0)                // alloc is success
          {                                  // write iNode back to disk
            inode.flags = 1;                 // mark iNode as used
            inode.name = iNumber;
            inode.fileSize = 0;
            for (int k=0; k<inode.pointer.length; k++)
              inode.pointer[k] = 0;          // init the inode block pointers
            disk.write(i, ib);               // save changes on disk

//            ft.bitmap[fileDescr] = 1;        // hold this file descriptor
            ft.fdArray[fileDescr] = new FileDescriptor(inode, iNumber);
          }      
          return(fileDescr);
        }
      }
    }
    error("No inode for a new file");
    return(-1);
  } 

  // Return the iNumber of an open file
  public int inumber(int fd) 
  {
    throw new RuntimeException("not implemented");
  }

  // Open an existing file identified by its iNumber
  public int open(int iNumber) 
  {
    for (int i=0; i<ft.fdArray.length; i++)  // check file for being opened
    {
        if (ft.fdArray[i] != null &&	     // an opened file entry
            ft.fdArray[i].inumber == iNumber)
          return(-1);                        // the file is already opened
    }

    InodeBlock ib = new InodeBlock();
    for (int i=1; i<=sb.iSize; i++)          // loop over all inode blocks
    {
      disk.read(i, ib);
      for (int j=0; j<ib.node.length; j++)   // loop over inodes in the block
      {
        Inode inode = ib.node[j];
        if (inode.flags != 0 && inode.name == iNumber)  // file exists
        {
          int ind = ft.allocate();            // allocate place in FileDescr.  table
          if (ind >= 0) 
          {
            FileDescriptor fd = new FileDescriptor(inode, iNumber);
//            ft.bitmap[ind] = 1;               // mark place as occupied
            ft.fdArray[ind] = fd;
            return(ind);
          }
          error("Too many opened files");
          return(-1);
        }
      }
    }
    error("File does not exist");
    return(-1);
  } 


// convert logical block number into a physical one
  private int getBlockNumber(Inode inode, int blockNo)
  {
    final int N = Disk.BLOCK_SIZE / 4;
    int physBlockNo = 0;
    if (blockNo < 10)                     // get block # directly from inode
    {
      physBlockNo = inode.pointer[blockNo];
    }
    else if (blockNo <10 + N)
    {
      IndirectBlock ib = new IndirectBlock();
      physBlockNo = inode.pointer[10];    // ref to indirect block
      disk.read(physBlockNo, ib);
      physBlockNo = ib.pointer[blockNo - 10];
    }
    else if (blockNo < 10 + N + N*N)      // second order indirect block
    {
      IndirectBlock ib = new IndirectBlock();
      physBlockNo = inode.pointer[11];    // ref to indirect blobk
      disk.read(physBlockNo, ib);
      physBlockNo = ib.pointer[(blockNo - 10 - N) / N];
      disk.read(physBlockNo, ib);
      physBlockNo = ib.pointer[(blockNo - 10 - N) % N];
    }
    else if (blockNo < 10 + N + N*N + N*N*N) // third order indirect block
    {
       // not implemented yet
    }
    else
    {
      error("Too large block number");
      return(-1);
    }
    return(physBlockNo);
  }


  // Read up to buffer.length bytes from the open file indicated by fd,
  // starting at the current seek pointer, and update the seek pointer.
  // Return the number of bytes read, which may be less than buffer.length
  // if the seek pointer is near the current end of the file.
  // In particular, return 0 if the seek pointer is greater than or
  // equal to the size of the file.
  public int read(int fd, byte[] buffer) 
  {
     if (! ft.isValid(fd))                    // check fd for validity
     {
       error("File is not opened");
       return(-1);       // check fd for validity
     }

     int seekPtr = ft.getSeekPointer(fd);
     int blockNo = seekPtr / Disk.BLOCK_SIZE; // logical block #
     int offset = seekPtr % Disk.BLOCK_SIZE;  // offset in block

     Inode inode = ft.getInode(fd);           // get file's inode
     
     byte[] blockTMP = new byte[Disk.BLOCK_SIZE];
     int bufferPtr = 0;                       // position in buffer

     int bytesRead = 0;
     while(bufferPtr < buffer.length && seekPtr < inode.fileSize)
     {
       int physBlockNo = getBlockNumber(inode, blockNo);
       if (physBlockNo < 0) return(-1);
       disk.read(physBlockNo, blockTMP);      // reading file data

       int readSize = Math.min(Disk.BLOCK_SIZE - offset, buffer.length - bytesRead);
       for (int i=0; i<readSize; i++)
         buffer[bufferPtr++] = blockTMP[offset+i];  // copy the file data

       bytesRead += readSize;
       seekPtr += readSize;                   // update the seek pointer      
       offset += readSize;                    // modify the data offset
       if (offset >= Disk.BLOCK_SIZE)
       {
         offset = 0;
         blockNo++;
       }
       ft.setSeekPointer(fd, seekPtr);        // save new offset in file table
     }
     return(bytesRead);
  } 


  // Transfer buffer.length bytes from the buffer to the file, starting
  // at the current seek pointer, and add buffer.length to the seek pointer.
  public int write(int fd, byte[] buffer) 
  {
     if (! ft.isValid(fd))                    // check fd for validity
     {
       error("File is not opened");
       return(-1);       // check fd for validity
     }

     int seekPtr = ft.getSeekPointer(fd);
     int blockNo = seekPtr / Disk.BLOCK_SIZE; // logical block #
     int offset = seekPtr % Disk.BLOCK_SIZE;  // offset in block

     Inode inode = ft.getInode(fd);           // get file's inode

     byte[] blockTMP = new byte[Disk.BLOCK_SIZE];
     int bufferPtr = 0;                       // position in buffer

     int bytesWritten = 0;
     while(bufferPtr < buffer.length)
     {
       int physBlockNo = 0;
       if (blockNo >= Math.ceil(inode.fileSize / Disk.BLOCK_SIZE))
         physBlockNo = allocateNewBlock(inode, blockNo); 
       else
         physBlockNo = getBlockNumber(inode, blockNo);
       if (physBlockNo < 0) return(-1);
       disk.read(physBlockNo, blockTMP);      // reading file data

       int writeSize = Math.min(Disk.BLOCK_SIZE - offset, buffer.length - bytesWritten);
       for (int i=0; i<writeSize; i++)
         blockTMP[offset+i] = buffer[bufferPtr++];  // copy the file data
       disk.write(physBlockNo, blockTMP);     // write changes to file   

       bytesWritten += writeSize;
       seekPtr += writeSize;                  // update the seek pointer
       offset += writeSize;                   // modify the data offset
       if (offset >= Disk.BLOCK_SIZE)
       {
         offset = 0;
         blockNo++;
       }
       ft.setSeekPointer(fd, seekPtr);        // save new offset in file table
       inode.fileSize += writeSize;
     }
     return(bytesWritten);
  } 

  private int allocateNewBlock(Inode inode, int blockNo)
  {
    IndirectBlock ib = new IndirectBlock();
    int freeBlockNo = getFreeBlock(ib);       // find the first free block

    final int N = Disk.BLOCK_SIZE / 4;
    if (blockNo < 10)                         // get block # directly from inode
    {
      inode.pointer[blockNo] = freeBlockNo;
    }
    else if (blockNo <10 + N)
    {
      IndirectBlock ib2 = new IndirectBlock();
      int physBlockNo;
      if (inode.pointer[10] == 0)
      {
        physBlockNo = getFreeBlock(ib2);      // find the first free block
        inode.pointer[10] = physBlockNo;       
      } 
      else
      {
        physBlockNo = inode.pointer[10];      // ref to indirect block
        disk.read(physBlockNo, ib2);
      }
      ib2.pointer[blockNo - 10] = freeBlockNo;
      disk.write(physBlockNo, ib2);	      // save modified indirect block	
    }
    else if (blockNo < 10 + N + N*N)          // second order indirect block
    {
      IndirectBlock ib2 = new IndirectBlock();
      int physBlockNo = inode.pointer[11];    // ref to indirect block
      // add code for inode.pointer[11]==0
      disk.read(physBlockNo, ib2);
      physBlockNo = ib2.pointer[(blockNo - 10 - N) / N];
      disk.read(physBlockNo, ib2);
      ib2.pointer[(blockNo - 10 - N) % N] = freeBlockNo;
      // complete this part of code
    }
    else if (blockNo < 10 + N + N*N + N*N*N)  // third order indirect block
    {
       // not implemented yet
    }
    else
    {
      error("Too large block number");
      return(-1);
    }
    return(freeBlockNo);
  }

  public int getFreeBlock(IndirectBlock ib)
  {
    if (sb.freeList <= 0)
    {
      error("No free space on disk");
      return(-1);
    }
    int freeBlockNo = sb.freeList;	      // get the ist free block
    disk.read(freeBlockNo, ib);               // read indirect block
    sb.freeList = ib.pointer[0];              // reassign the free block ptr	
    return (freeBlockNo);            // find the first free block
  }

  // Update the seek pointer by offset, according to whence.
  // Return the new value of the seek pointer.
  // If the new seek pointer would be negative, leave it unchanged
  // and return -1.
  public int seek(int fd, int offset, int whence) 
  {
    throw new RuntimeException("not implemented");
  } 

  // Write the inode back to disk and free the file table entry
  public int close(int fd) 
  {
    Inode inode = ft.getInode(fd);           // get file's inode 
    InodeBlock ib = new InodeBlock();
    boolean updated = false;
    for (int i=1; i<=sb.iSize; i++)          // loop over all inode blocks
    {
      disk.read(i, ib);
      updated = false;
      for (int j=0; j<ib.node.length; j++)   // loop over inodes in the block
      {
        Inode inode2 = ib.node[j];
        if (inode2.flags != 0 && inode2.name == inode.name)  // file exists
        {
          ib.node[j] = inode;                // update the inode
          disk.write(i, ib);                 // save changes
          updated = true; 
          break;
        }
      }
      if (updated) break;
    }
    if (! updated)
    {
      error("Inode not found");
      return(-1);
    }

    ft.free(fd);                             // free the file table entry
    return(0);
  } 

  // Delete the file with the given iNumber, freeing all of its blocks.
  public int delete(int iNumber) 
  {
    throw new RuntimeException("not implemented");
  }

  public String toString() 
  {
    throw new RuntimeException("not implemented");
  }

  private void error(String msg) 
  {
    System.out.println("\n[ERROR] " + msg);
    System.out.flush();
  }
}

