/*
 * FileManager.java
 *
 * Status: In progress
 *
 * Notes:
 *   This class should not be referred to by any class outside of the fileio
 *     package, except for the FinancialDatabase class.
 */

package fileio;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;

import finance.*;
import utility.Status;

/**
 * This class manages the file I/O for the financial database.
 */
public final class FileManager
 {
  /**
   * Error status to indicate {@code null} was passed as a
   * {@link FinancialDatabase FinancialDatabase} parameter.
   */
  public static final Status NULL_DB = new Status("FileManager.NULL_DB");
  /**
   * Error status to indicate that the financial database file was not found.
   */
  public static final Status FILE_NOT_FOUND = new Status("FileManager.FILE_NOT_FOUND");
  /**
   * Error status to indicate that the file is not a valid financial database.
   */
  public static final Status BAD_FILE_TYPE = new Status("FileManager.BAD_FILE_TYPE");
  /**
   * Error status to indicate that the financial database file may be corrupted.
   */
  public static final Status FILE_CORRUPTED = new Status("FileManager.FILE_CORRUPTED");


  /** File version identifier. */
  private static final int FILE_VERSION = 1;
  /** Marker value to indicate the beginning of an AccountType entry. */
  private static final int ACCOUNT_TYPE_BEGIN = 1;
  /** Marker value to indicate the end of an AccountType entry. */
  private static final int ACCOUNT_TYPE_END = 2;
  /** Marker value to indicate the beginning of an Account entry. */
  private static final int ACCOUNT_BEGIN = 3;
  /** Marker value to indicate the end of an Account entry. */
  private static final int ACCOUNT_END = 4;
  /** Marker value to indicate the beginning of a JournalEntry entry. */
  private static final int JOURNAL_ENTRY_BEGIN = 5;
  /** Marker value to indicate the end of a JournalEntry entry. */
  private static final int JOURNAL_ENTRY_END = 6;
  /** Marker value to indicate the beginning of a deletion record. */
  private static final int DELETION_RECORD_BEGIN = 7;
  /** Marker value to indicate the end of a deletion record. */
  private static final int DELETION_RECORD_END = 8;


  /**
   * Creates a new financial database file.
   */
  public static Status createNew(File file)
   {
    if(file.exists())
      return Status.ERROR;

    BufferedOutputStream out = null;

    try
     { out = new BufferedOutputStream(new FileOutputStream(file)); }
    catch(Exception e)
     { return Status.ERROR; }

    try
     {
      byte[] bytes = null;
      byte[] intBytes = new byte[4];
      ByteBuffer intBuf = ByteBuffer.wrap(intBytes);

      out.write('D'); out.write('E'); out.write('A'); out.write('P');
      out.write(FILE_VERSION);

      for(AccountType e : AccountType.getList())
       {
        out.write(ACCOUNT_TYPE_BEGIN);
        bytes = AccountType.convertToBytes(e);
        intBuf.putInt(0, bytes.length);
        out.write(intBytes);
        out.write(bytes);
        out.write(ACCOUNT_TYPE_END);
       }

      out.close();
      return Status.OK;
     }
    catch(Exception ignored)
     { }

    //If we get here there was a problem.
    try
     { out.close(); }
    catch(Exception ignored)
     { }

    file.delete();
    return Status.ERROR;
   }


  /** The financial database file managed by this {@code FileManager}. */
  private File dbFile = null;

  private boolean isLoading = false;


  /**
   * Constructs a new {@code FileManager}.
   */
  public FileManager(File file)
   {
    dbFile = file;
   }


//Incomplete
  public Status loadFile(FinancialDatabase db)
   {
    if(db == null)
      return NULL_DB;
    if(!dbFile.exists())
      return FILE_NOT_FOUND;

    RandomAccessFile in = null;
    int checkedLength = 0;

    try
     { in = new RandomAccessFile(dbFile, "rwd"); }
    catch(Exception e)
     { return Status.ERROR; }

    try
     {
      if(in.read() == 'D') checkedLength++;
      if(in.read() == 'E') checkedLength++;
      if(in.read() == 'A') checkedLength++;
      if(in.read() == 'P') checkedLength++;
      if(in.read() == FILE_VERSION) checkedLength++;
      if(checkedLength != 5)
       {
        in.close();
        return BAD_FILE_TYPE;
       }

      isLoading = true;

      int type = 0;
      int length = 0;
      while((type = in.read()) != -1)
       {
        if(type == ACCOUNT_TYPE_BEGIN)
          length = loadAccountType(in, db);
        else if(type == ACCOUNT_BEGIN)
          length = loadAccount(in, db);
        else if(type == JOURNAL_ENTRY_BEGIN)
          length = loadJournalEntry(in, db);
        else if(type == DELETION_RECORD_BEGIN)
          length = loadDeletion(in, db);

        if(length != -1)
          checkedLength += length;
        else
          break;
       }

      isLoading = false;
      in.close();
      if(length != -1)
        return Status.OK;
      else
        return FILE_CORRUPTED;
     }
    catch(Exception e)
     { System.out.println(e); e.printStackTrace(); }

    //If we get here there was a problem.
    try
     { in.close(); }
    catch(Exception ignored)
     { }
    return Status.ERROR;
   }


  /**
   * Loads an {@code AccountType} from a file into a {@code FinancialDatabase}.
   *
   * @param in the file to read from.
   * @param db the {@code FinancialDatabase} to load to.
   *
   * @return the number of bytes read, or -1 if an error occurs.
   */
  private int loadAccountType(RandomAccessFile in, FinancialDatabase db)
   {
    try
     {
      byte[] intBytes = new byte[4];
      in.readFully(intBytes);
      int length = ByteBuffer.wrap(intBytes).getInt(0);
      byte[] bytes = new byte[length];
      in.readFully(bytes);

      if(in.read() != ACCOUNT_TYPE_END)
        return -1;
      else
       {
        db.addAccountType(AccountType.convertFromBytes(bytes));
        return bytes.length + 5;
       }
     }
    catch(IOException e)
     { return -1; }
   }


  /**
   * Writes an {@code AccountType} to the financial database file.
   *
   * @param obj the {@code AccountType} to write to the file.
   *
   * @return {@code Status.OK} if the data was recorded successfully, {@code
   * Status.ERROR} if the data could not be recorded and the file was returned
   * to its original state, or {@code FileManager.FILE_CORRUPTED} if the data
   * could not be recorded and the file may not have been returned to its
   * original state.
   */
  public Status recordAccountType(AccountType obj)
   {
    byte[] objBytes = AccountType.convertToBytes(obj);
    byte[] bytes = new byte[objBytes.length + 6];
    byte[] intBytes = new byte[4];
    ByteBuffer intBuf = ByteBuffer.wrap(intBytes);
    intBuf.putInt(0, objBytes.length);

    bytes[0] = (byte)ACCOUNT_TYPE_BEGIN;
    System.arraycopy(intBytes, 0, bytes, 1, intBytes.length);
    System.arraycopy(objBytes, 0, bytes, 5, objBytes.length);
    bytes[bytes.length-1] = (byte)ACCOUNT_TYPE_END;

    return writeBytes(bytes);
   }


  /**
   * Loads an {@code Account} from a file into a {@code FinancialDatabase}.
   *
   * @param in the file to read from.
   * @param db the {@code FinancialDatabase} to load to.
   *
   * @return the number of bytes read, or -1 if an error occurs.
   */
  private int loadAccount(RandomAccessFile in, FinancialDatabase db)
   {
    try
     {
      byte[] intBytes = new byte[4];
      in.readFully(intBytes);
      int length = ByteBuffer.wrap(intBytes).getInt(0);
      byte[] bytes = new byte[length];
      in.readFully(bytes);

      if(in.read() != ACCOUNT_END)
        return -1;
      else
       {
        db.addAccount(Account.convertFromBytes(bytes));
        return bytes.length + 5;
       }
     }
    catch(IOException e)
     { return -1; }
   }


  /**
   * Writes an {@code Account} to the financial database file.
   *
   * @param obj the {@code Account} to write to the file.
   *
   * @return {@code Status.OK} if the data was recorded successfully, {@code
   * Status.ERROR} if the data could not be recorded and the file was returned
   * to its original state, or {@code FileManager.FILE_CORRUPTED} if the data
   * could not be recorded and the file may not have been returned to its
   * original state.
   */
  public Status recordAccount(Account obj)
   {
    byte[] objBytes = Account.convertToBytes(obj);
    byte[] bytes = new byte[objBytes.length + 6];
    byte[] intBytes = new byte[4];
    ByteBuffer intBuf = ByteBuffer.wrap(intBytes);
    intBuf.putInt(0, objBytes.length);

    bytes[0] = (byte)ACCOUNT_BEGIN;
    System.arraycopy(intBytes, 0, bytes, 1, intBytes.length);
    System.arraycopy(objBytes, 0, bytes, 5, objBytes.length);
    bytes[bytes.length-1] = (byte)ACCOUNT_END;

    return writeBytes(bytes);
   }


  /**
   * Loads a {@code JournalEntry} from a file into a {@code FinancialDatabase}.
   *
   * @param in the file to read from.
   * @param db the {@code FinancialDatabase} to load to.
   *
   * @return the number of bytes read, or -1 if an error occurs.
   */
  private int loadJournalEntry(RandomAccessFile in, FinancialDatabase db)
   {
    try
     {
      byte[] intBytes = new byte[4];
      in.readFully(intBytes);
      int length = ByteBuffer.wrap(intBytes).getInt(0);
      byte[] bytes = new byte[length];
      in.readFully(bytes);

      if(in.read() != JOURNAL_ENTRY_END)
        return -1;
      else
       {
        db.addJournalEntry(JournalEntry.convertFromBytes(bytes));
        return bytes.length + 5;
       }
     }
    catch(IOException e)
     { return -1; }
   }


  /**
   * Writes a {@code JournalEntry} to the financial database file.
   *
   * @param obj the {@code JournalEntry} to write to the file.
   *
   * @return {@code Status.OK} if the data was recorded successfully, {@code
   * Status.ERROR} if the data could not be recorded and the file was returned
   * to its original state, or {@code FileManager.FILE_CORRUPTED} if the data
   * could not be recorded and the file may not have been returned to its
   * original state.
   */
  public Status recordJournalEntry(JournalEntry obj)
   {
    byte[] objBytes = JournalEntry.convertToBytes(obj);
    byte[] bytes = new byte[objBytes.length + 6];
    byte[] intBytes = new byte[4];
    ByteBuffer intBuf = ByteBuffer.wrap(intBytes);
    intBuf.putInt(0, objBytes.length);

    bytes[0] = (byte)JOURNAL_ENTRY_BEGIN;
    System.arraycopy(intBytes, 0, bytes, 1, intBytes.length);
    System.arraycopy(objBytes, 0, bytes, 5, objBytes.length);
    bytes[bytes.length-1] = (byte)JOURNAL_ENTRY_END;

    return writeBytes(bytes);
   }


  /**
   * Loads a deletion record from a file into a {@code FinancialDatabase}.
   *
   * @param in the file to read from.
   * @param db the {@code FinancialDatabase} to load to.
   *
   * @return the number of bytes read, or -1 if an error occurs.
   */
  private int loadDeletion(RandomAccessFile in, FinancialDatabase db)
   {
    try
     {
      byte[] intBytes = new byte[4];
      in.readFully(intBytes);
      int length = ByteBuffer.wrap(intBytes).getInt(0);
      byte[] bytes = new byte[length];
      in.readFully(bytes);

      if(in.read() != DELETION_RECORD_END)
        return -1;
      else
       {
        db.addDeletion(bytes);
        return bytes.length + 5;
       }
     }
    catch(IOException e)
     { return -1; }
   }


  /**
   * Writes a deletion record to the financial database file.  Because the
   * internal structure of a deletion record may be specific to the type of item
   * being deleted, the caller is responsible for constructing the byte array
   * that makes up the record.
   *
   * @param objBytes the deletion record to write to the file.
   *
   * @return {@code Status.OK} if the data was recorded successfully, {@code
   * Status.ERROR} if the data could not be recorded and the file was returned
   * to its original state, or {@code FileManager.FILE_CORRUPTED} if the data
   * could not be recorded and the file may not have been returned to its
   * original state.
   */
  public Status recordDeletion(byte[] objBytes)
   {
    byte[] bytes = new byte[objBytes.length + 6];
    byte[] intBytes = new byte[4];
    ByteBuffer intBuf = ByteBuffer.wrap(intBytes);
    intBuf.putInt(0, objBytes.length);

    bytes[0] = (byte)DELETION_RECORD_BEGIN;
    System.arraycopy(intBytes, 0, bytes, 1, intBytes.length);
    System.arraycopy(objBytes, 0, bytes, 5, objBytes.length);
    bytes[bytes.length-1] = (byte)DELETION_RECORD_END;

    return writeBytes(bytes);
   }


  /**
   * Writes a byte array to the financial database file.
   *
   * @param bytes the byte array to write to the file.
   *
   * @return {@code Status.OK} if the data was recorded successfully, {@code
   * Status.ERROR} if the data could not be recorded and the file was returned
   * to its original state, or {@code FileManager.FILE_CORRUPTED} if the data
   * could not be recorded and the file may not have been returned to its
   * original state.
   */
  private Status writeBytes(byte[] bytes)
   {
    if(isLoading)
      return Status.OK;

    if(!dbFile.exists())
      return FILE_NOT_FOUND;

    RandomAccessFile out = null;
    long fileLength = 0;

    try
     {
      out = new RandomAccessFile(dbFile, "rwd");
      fileLength = out.length();
      out.seek(fileLength);
     }
    catch(Exception e)
     { return Status.ERROR; }

    try
     {
      out.write(bytes);
      out.close();
      return Status.OK;
     }
    catch(Exception ignored)
     { System.out.println(ignored); }

    //If we get here there was a problem.
    try
     {
      //Attempt to clean up; delete anything we wrote.
      out.setLength(fileLength);
      out.close();
      return Status.ERROR;
     }
    catch(Exception ignored)
     { }

    //Major problem.
    try
     { out.close(); }
    catch(Exception ignored)
     { }

    return FILE_CORRUPTED;
   }
 }