/*
 * FinancialDatabase.java
 *
 * Status: In progress
 *
 * Notes:
 *   This should be the only class outside the fileio package that references
 *     the FileManager class.
 */

package finance;

import java.io.File;
import java.nio.ByteBuffer;
import java.util.*;

import fileio.FileManager;
import utility.Status;

public final class FinancialDatabase
 {
  private static final int DELETE_ACCOUNT_TYPE = 1;
  private static final int DELETE_ACCOUNT = 2;
  private static final int DELETE_JOURNAL_ENTRY = 3;


  /**
   * Creates a new financial database file.  Delegates to {@link
   * FileManager#createNew FileManager.createNew}.
   */
  public static Status createNew(File file)
   { return FileManager.createNew(file); }


  /** The list of all account types in this database. */
  LinkedList<AccountType> accountTypes = new LinkedList<AccountType>();

  /** The General Journal, which records all transactions. */
  Journal journal = new Journal(this, 0, "General Journal");
  /** The General Ledger, which contains all accounts. */
  Ledger ledger = new Ledger(this);

  /** The {@code FileManager} that manages the file for this database. */
  FileManager fileManager = null;


  /**
   * Constructs a new {@code FinancialDatabase}.
   *
   * @param file the financial database file to use.
   */
  public FinancialDatabase(File file)
   {
    //accountTypes = new LinkedList<AccountType>();
    //journal = new Journal(this, 0, "General Journal");
    //ledger = new Ledger(this);
    fileManager = new FileManager(file);
   }


  public Status loadFile()
   { return fileManager.loadFile(this); }


  public Status saveFileAs(File file)
   {
    if(file.exists() && !file.delete())
      return Status.ERROR;

    Status status = FileManager.createNew(file);
    if(status != Status.OK)
      return status;

    FileManager fm = new FileManager(file);

    //Duplicates the default types, rewrite later....
    for(AccountType e : accountTypes)
      if(status == Status.OK)
        fm.recordAccountType(e);
      else
        break;

    for(Account e : ledger.getAccounts())
      if(status == Status.OK)
        fm.recordAccount(e);
      else
        break;

    for(JournalEntry e : journal.getEntries())
      if(status == Status.OK)
        fm.recordJournalEntry(e);
      else
        break;

    if(status != Status.OK)
     {
      file.delete();
      return status;
     }

    fileManager = fm;
    return Status.OK;
   }


  public void parseAccountTypes(String types)
   {
    if(types == null)
      return;

    String[] strings = types.split(";");
    for(String s : strings)
     {
      int i = s.lastIndexOf(",");
      if(i == -1)
        continue;

      String name = s.substring(0, i).trim();
      String t = s.substring(i+1).trim();

      BalanceType type = null;
      if(t.equalsIgnoreCase("debit"))
        type = BalanceType.DEBIT;
      else if(t.equalsIgnoreCase("credit"))
        type = BalanceType.CREDIT;

      if(name.equals("") || type == null)
        continue;

      addAccountType(new AccountType(name, type));
     }
   }



  /**
   * Gets the General Journal.
   *
   * @return the General Journal for this financial database.
   */
  public Journal getJournal()
   { return journal; }


  /**
   * Gets the General Ledger.
   *
   * @return the General Ledger for this financial database.
   */
  public Ledger getLedger()
   { return ledger; }

  //
  // Begin Account Types
  //

  /**
   * Adds an account type to this database.
   *
   * @param accountType the {@code AccountType} to add.
   */
  public void addAccountType(AccountType accountType)
   {
    //If accountType is null or already in database, do nothing.
    if(accountType == null || getAccountType(accountType.getName()) != null)
      return;

    accountTypes.add(accountType);
    ledger.addAccountType(accountType);

    fileManager.recordAccountType(accountType);
   }


  /**
   * Removes an account type from this database.
   *
   * @param accountType the {@code AccountType} to remove.
   */
  public void removeAccountType(AccountType accountType)
   {
    //If accountType is null or not in database, do nothing.
    if(accountType == null || getAccountType(accountType.getName()) == null)
      return;

    accountTypes.remove(accountType);
    ledger.removeAccountType(accountType);

    recordDeletion(accountType);
   }


  /**
   * Gets the list of account types in this database.
   */
  public List<AccountType> getAccountTypes()
   { return Collections.unmodifiableList(accountTypes); }


  /**
   * Gets a reference to the {@code AccountType} instance with the specified
   * name. The name is case-insensitive, i.e. "asset" equals "Asset".
   *
   * @param name the name of the {@code AccountType} to return.
   *
   * @return the {@code AccountType} with the specified name, or {@code null} if
   * no {@code AccountType} with the specified name exists.
   */
  public AccountType getAccountType(String name)
   {
    if(name == null)
      return null;

    for(AccountType e : accountTypes)
      if(name.equalsIgnoreCase(e.getName()))
        return e;

    return null;
   }


  /**
   * Gets a reference to the {@code AccountType} instance with the specified ID.
   *
   * @param id the ID of the {@code AccountType} to return.
   *
   * @return the {@code AccountType} with the specified ID, or {@code null} if
   * no {@code AccountType} with the specified ID exists.
   */
  public AccountType getAccountType(int id)
   {
    for(AccountType e : accountTypes)
      if(id == e.getID())
        return e;

    return null;
   }

  //
  // End Account Types
  //
  // Begin Accounts
  //

  /**
   * Adds an account to this database.
   *
   * @param account the {@code Account} to add.
   */
  public void addAccount(Account account)
   {
    if(account == null)
      return;

    Account oldAccount = getAccountByID(account.getID());
    if(oldAccount == null)
      oldAccount = account;
    else
      oldAccount.update(account);
    ledger.addOrUpdate(oldAccount);

    recordAccount(oldAccount);
   }


  /**
   * Removes an account from this database.
   *
   * @param account the {@code Account} to remove.
   */
  public void removeAccount(Account account)
   {
    if(account == null || getAccountByID(account.getID()) == null)
      return;

    ledger.removeAccount(account);

    recordDeletion(account);
   }


  /**
   * Records the entry of an {@code Account} in the database.  Delegates to
   * {@link FileManager#recordAccount FileManager.recordAccount}.
   */
  public void recordAccount(Account account)
   {
    fileManager.recordAccount(account);
   }


  /**
   * Gets a reference to the {@code Account} instance with the specified name.
   * The name is case-insensitive, i.e. "savings" equals "Savings".
   *
   * @param name the name of the {@code Account} to return.
   *
   * @return the {@code Account} with the specified name, or {@code null} if no
   * {@code Account} with the specified name exists.
   */
  public Account getAccountByName(String name)
   {
    for(Account a : ledger.getAccounts())
      if(a.getName().equalsIgnoreCase(name))
        return a;
    return null;
   }


  /**
   * Gets a reference to the {@code Account} instance with the specified number.
   * The number is case-insensitive, i.e. "1001-s01" equals "1001-S01".
   *
   * @param number the number of the {@code Account} to return.
   *
   * @return the {@code Account} with the specified number, or {@code null} if
   * no {@code Account} with the specified number exists.
   */
  public Account getAccountByNumber(String number)
   {
    if(number == null || number.equals(""))
      return null;
    for(Account a : ledger.getAccounts())
      if(a.getNumber().equalsIgnoreCase(number))
        return a;
    return null;
   }


  /**
   * Gets a reference to the {@code Account} instance with the specified ID.
   *
   * @param id the ID of the {@code Account} to return.
   *
   * @return the {@code Account} with the specified ID, or {@code null} if no
   * {@code Account} with the specified ID exists.
   */
  public Account getAccountByID(int id)
   {
    for(Account a : ledger.getAccounts())
      if(a.getID() == id)
        return a;
    return null;
   }


  public String[] getAccountNames()
   {
    String[] ret = new String[ledger.getAccounts().size()];
    int i = 0;
    for(Account a : ledger.getAccounts())
      ret[i++] = a.getName();
    return ret;
   }

  //
  // End Accounts
  //
  // Begin Journal Entries
  //

  /**
   * Adds a journal entry to this database.
   *
   * @param entry the {@code JournalEntry} to add.
   */
  public void addJournalEntry(JournalEntry entry)
   { journal.addOrUpdate(entry); }


  /**
   * Records the entry of a {@code JournalEntry} in the database.  Delegates to
   * {@link FileManager#recordJournalEntry FileManager.recordJournalEntry}.
   */
  public void recordEntry(JournalEntry entry)
   {
    fileManager.recordJournalEntry(entry);
   }

  //
  // End Journal Entries
  //
  // Begin Deletions
  //

  /**
   * Adds a deletion record to this database.  Note that a deletion record is
   * not actually an addition, as it only indicates that something else is to be
   * removed.
   *
   * @param bytes the deletion record to add.
   */
  public void addDeletion(byte[] bytes)
   {
    int intID = 0;
    switch(bytes[0])
     {
      case DELETE_ACCOUNT_TYPE:
        intID = ByteBuffer.wrap(bytes, 1, 4).getInt();
        removeAccountType(getAccountType(intID));
        break;
      case DELETE_ACCOUNT:
        intID = ByteBuffer.wrap(bytes, 1, 4).getInt();
        ledger.removeAccount(intID);
        break;
      case DELETE_JOURNAL_ENTRY:
        long longID = ByteBuffer.wrap(bytes, 1, 8).getLong();
        journal.remove(longID);
        break;
     }
   }


  /**
   * Records the deletion of an {@code AccountType} from the database.
   */
  public void recordDeletion(AccountType type)
   {
    byte[] bytes = new byte[5];
    bytes[0] = (byte)DELETE_ACCOUNT_TYPE;
    ByteBuffer.wrap(bytes, 1, 4).putInt(type.getID());

    fileManager.recordDeletion(bytes);
   }


  /**
   * Records the deletion of an {@code Account} from the database.
   */
  public void recordDeletion(Account account)
   {
    byte[] bytes = new byte[5];
    bytes[0] = (byte)DELETE_ACCOUNT;
    ByteBuffer.wrap(bytes, 1, 4).putInt(account.getID());

    fileManager.recordDeletion(bytes);
   }


  /**
   * Records the deletion of a {@code JournalEntry} from the database.
   */
  public void recordDeletion(JournalEntry entry)
   {
    byte[] bytes = new byte[9];
    bytes[0] = (byte)DELETE_JOURNAL_ENTRY;
    ByteBuffer.wrap(bytes, 1, 8).putLong(entry.getID());

    fileManager.recordDeletion(bytes);
   }
}