/*
 * JournalEntry.java
 *
 * Status: Tentatively complete, minus some documentation
 */

package finance;

import java.nio.ByteBuffer;
import java.util.*;

import utility.*;

/**
 * A JournalEntry represents a single entry in a journal.  It contains the
 * date of the transaction, a list of all accounts debited and all accounts
 * credited, with amounts, and a description of the transaction.
 */
public final class JournalEntry
 {
  /**
   * Comparator class to compare journal entries by date and ID.
   */
  public static class DateComparator<T extends JournalEntry> implements java.util.Comparator<T>
   {
    public int compare(T o1, T o2)
     {
      if(o1 == null || o2 == null)
        throw new NullPointerException();

      if(o1 == o2)
        return 0;

      int ret = o1.date.compareTo(o2.date);
      if(ret != 0)
        return ret;
      else
        return (new Long(o1.id)).compareTo(new Long(o2.id));
     }


    public boolean equals(Object obj)
     { return (obj != null) && (this.getClass() == obj.getClass()); }
   }


  /**
   * Comparator class to compare journal entries by ID only.
   */
  public static class IDComparator<T extends JournalEntry> implements java.util.Comparator<T>
   {
    public int compare(T o1, T o2)
     {
      if(o1 == null || o2 == null)
        throw new NullPointerException();

      return (new Long(o1.id)).compareTo(o2.id);
     }


    public boolean equals(Object obj)
     { return (obj != null) && (this.getClass() == obj.getClass()); }
   }


  /** Comparator to order journal entries by date and ID. */
  public static final Comparator<JournalEntry> COMP_DATE = new DateComparator<JournalEntry>();
  /** Comparator to order journal entries only by ID. */
  public static final Comparator<JournalEntry> COMP_ID = new IDComparator<JournalEntry>();

  /**
   * Error status to indicate that an edit was attempted after the {@code
   * JournalEntry}'s "new" state was cleared.
   */
  public static final Status NOT_NEW = new Status("JournalEntry.NOT_NEW");


  /**
   * Converts an {@code JournalEntry} object into an array of bytes suitable for
   * writing to a file.
   *
   * @param obj the {@code JournalEntry} object to convert.
   *
   * @return an array of bytes representing the given {@code JournalEntry}
   * object.
   */
  public static byte[] convertToBytes(JournalEntry obj)
   {
    int size = 20;

    byte[] idBytes = new byte[8];
    ByteBuffer.wrap(idBytes).putLong(obj.id);
    byte[] dateBytes = new byte[8];
    ByteBuffer.wrap(dateBytes).putLong(obj.date.getTime());
    byte[] countBytes = new byte[4];
    ByteBuffer.wrap(countBytes).putInt(obj.amounts.size());
    for(AccountEntry e : obj.amounts)
      size += AccountEntry.convertToBytes(e).length + 4;
    byte[] descBytes = obj.description.getBytes();
    size += descBytes.length + 4;

    byte[] bytes = new byte[size];
    byte[] intBytes = new byte[4];
    ByteBuffer intBuf = ByteBuffer.wrap(intBytes);
    int index = 0;

    System.arraycopy(idBytes, 0, bytes, index, 8);
    index += 8;
    System.arraycopy(dateBytes, 0, bytes, index, 8);
    index += 8;
    System.arraycopy(countBytes, 0, bytes, index, 4);
    index += 4;

    for(AccountEntry e : obj.amounts)
     {
      byte[] entryBytes = AccountEntry.convertToBytes(e);
      intBuf.putInt(0, entryBytes.length);
      System.arraycopy(intBytes, 0, bytes, index, 4);
      index += 4;
      System.arraycopy(entryBytes, 0, bytes, index, entryBytes.length);
      index += entryBytes.length;
     }

    intBuf.putInt(0, descBytes.length);
    System.arraycopy(intBytes, 0, bytes, index, 4);
    index += 4;
    System.arraycopy(descBytes, 0, bytes, index, descBytes.length);
    //index += descBytes.length;

    return bytes;
   }


  /**
   * Converts an array of bytes into an {@code JournalEntry} object.
   *
   * @param bytes the array of bytes to convert.
   *
   * @return an {@code JournalEntry} object constructed from the given array of
   * bytes.
   */
  public static JournalEntry convertFromBytes(byte[] bytes)
   {
    JournalEntry ret = gui.Main.getDB().getJournal().newEntry(null, null);

    ret.id = ByteBuffer.wrap(bytes, 0, 8).getLong();
    ret.date = new Date(ByteBuffer.wrap(bytes, 8, 8).getLong());

    int count = ByteBuffer.wrap(bytes, 16, 4).getInt();
    int index = 20;
    for(int i=0; i<count; i++)
     {
      int size = ByteBuffer.wrap(bytes, index, 4).getInt();
      index += 4;
      byte[] entryBytes = new byte[size];
      System.arraycopy(bytes, index, entryBytes, 0, entryBytes.length);
      ret.add(AccountEntry.convertFromBytes(entryBytes));
      index += entryBytes.length;
     }

    byte[] descBytes = new byte[ByteBuffer.wrap(bytes, index, 4).getInt()];
    index += 4;
    System.arraycopy(bytes, index, descBytes, 0, descBytes.length);
    ret.description = new String(descBytes);

    return ret;
   }


  /** The {@code Journal} in which this {@code JournalEntry} is entered. */
  Journal journal = null;
  /** The unique ID of this {@code JournalEntry}.*/
  private long id = 0;
  /** The date of this {@code JournalEntry}.*/
  private Date date = new Date();
  /** The list of amounts for this {@code JournalEntry}.*/
  private LinkedList<AccountEntry> amounts = new LinkedList<AccountEntry>();
  /** The description of this {@code JournalEntry}.*/
  private String description = "";

  /** Identifies if this is a new entry. */
  boolean isNew = true;


  /**
   * Constructs a new {@code JournalEntry}.
   */
  JournalEntry(Journal journal, long id, Date date, String description)
   {
    this.journal = journal;
    this.id = id;
    this.date = date;
    this.description = description;
   }


  public long getID()
   { return id; }


  public Date getDate()
   { return date; }


  public String getDescription()
   { return description; }


  public List<AccountEntry> getAmounts()
   { return Collections.unmodifiableList(amounts); }


  /**
   * Adds an {@code AccountEntry} to this {@code JournalEntry}.  Only valid
   * until this {@code JournalEntry} is added to a {@link Journal Journal}.
   *
   * @param amount the {@code AccountEntry} to add.
   *
   * @return {@code Status.OK} if the add was successful, {@code
   * JournalEntry.NOT_NEW} if this {@code JournalEntry} has already been added
   * to a {@code Journal}.
   */
  public Status add(AccountEntry amount)
   {
    if(!isNew)
      return NOT_NEW;
    if(amount == null)
      return Status.ERROR;

    amount.entry = this;
    amounts.add(amount);

    return Status.OK;
   }
 }