/*
 * Journal.java
 *
 * Status: Tentatively complete, minus some documentation
 */

package finance;

import java.util.*;
import javax.swing.table.TableModel;
import javax.swing.table.AbstractTableModel;

/**
 * A journal stores a record of transactions.  The General Journal records all
 * transactions; subsidiary journals may record only a select subset of all
 * transactions.  Support for subsidiary journals has not been implemented in
 * this version.
 */
public final class Journal
 {
  public static class DataModel extends AbstractTableModel
   {
    /** The index of the date column. */
    public static final int DATE_COL = 0;
    /** The index of the description column. */
    public static final int DESC_COL = 1;
    /** The index of the debit column. */
    public static final int DEBIT_COL = 2;
    /** The index of the credit column. */
    public static final int CREDIT_COL = 3;
    /** The number of columns in the table. */
    public static final int COLUMN_COUNT = 4;


    /** The {@code Journal} supplying this model's data. */
    private Journal journal = null;
    /** Local {@code List} representation of the {@code Journal}'s entries. */
    private ArrayList<JournalEntry> list = null;


    /**
     * Constructs a new {@code DataModel} for the specified {@code Journal}.
     *
     * @param journal the {@code Journal} that will supply this model's data.
     */
    private DataModel(Journal journal)
     {
      this.journal = journal;
      updateList();
     }


    /**
     * Updates the local {@code List} copy of the {@code Journal}'s entries.
     */
    private void updateList()
     { list = new ArrayList<JournalEntry>(journal.entriesByDate); }


    /**
     * Gets the number of rows in this data model.
     */
    public int getRowCount()
     { return list.size(); }


    /**
     * Gets the number of columns in this data model.
     */
    public int getColumnCount()
     { return COLUMN_COUNT; }


    /**
     * Gets the value at the specified coordinates.
     *
     * @param row the row coordinate of the desired value.
     * @param column the column coordinate of the desired value.
     */
    public Object getValueAt(int row, int column)
     {
      if(column >= 0 && column < COLUMN_COUNT && row >= 0 && row < getRowCount())
        return list.get(row);
      else
        return null;
     }


    /**
     * Gets the name of the specified column.
     *
     * @param column the column whose name is desired.
     */
    public String getColumnName(int column)
     {
      switch(column)
       {
        case DATE_COL:
          return "Date";
        case DESC_COL:
          return "Account Titles and Description";
        case DEBIT_COL:
          return "Debit";
        case CREDIT_COL:
          return "Credit";
        default:
          return super.getColumnName(column);
       }
     }
   }


  /** A unique identifier for this journal. */
  int id;
  /** The name of this journal, e.g.&nbsp;General Journal. */
  String name;
  /** The entries in this journal, sorted by date and ID. */
  TreeSet<JournalEntry> entriesByDate = new TreeSet<JournalEntry>(JournalEntry.COMP_DATE);
  /** The entries in this journal, sorted only by ID. */
  TreeSet<JournalEntry> entriesByID = new TreeSet<JournalEntry>(JournalEntry.COMP_ID);

  /** Used to provide unique identifiers for journal entries. */
  long newEntryID = 0;
  /** The {@code FinancialDatabase} where this journal is stored. */
  FinancialDatabase DB = null;

  DataModel dataModel = null;


  /**
   * Constructs a new {@code Journal}.
   */
  Journal(FinancialDatabase db, int id, String name)
   {
    DB = db;
    this.id = id;
    this.name = name;
   }


  /**
   * Gets the name of this journal.
   */
  public String getName()
   { return name; }


  /**
   * Gets the ID of this journal.
   */
  public int getID()
   { return id; }


  /**
   * Creates a new {@code JournalEntry} with the specified date and description,
   * and a new unique ID.  Used for adding new entries.
   */
  public JournalEntry newEntry(Date date, String description)
   { return new JournalEntry(this, newEntryID, date, description); }


  /**
   * Creates a new {@code JournalEntry} with the specified date and description,
   * and the unique ID of the specified {@code JournalEntry}.  Used for editing
   * existing entries.
   */
  public JournalEntry newEntry(JournalEntry entry, Date date, String description)
   { return new JournalEntry(this, entry.getID(), date, description); }


  /**
   * Gets the entries in this journal, sorted by date and ID.  The returned
   * collection is read-only.
   */
  public Collection<JournalEntry> getEntries()
   { return Collections.unmodifiableCollection(entriesByDate); }


  /**
   * Adds a new entry, or updates an existing one.
   *
   * @param entry the entry to add or update.
   */
  public void addOrUpdate(JournalEntry entry)
   {
    if(entry == null)
     {
      System.err.println("Unexpected null entry updating journal.");
      return;
     }

    if(entry.getID() >= newEntryID)
      newEntryID = entry.getID() + 1;

    if(entriesByID.contains(entry))
      update(entry);
    else
      add(entry);

    DB.recordEntry(entry);
   }


  /**
   * Adds an entry to this journal.
   *
   * @param entry the entry to add.
   */
  private void add(JournalEntry entry)
   {
    entriesByDate.add(entry);
    entriesByID.add(entry);

    entry.isNew = false;

    for(AccountEntry e : entry.getAmounts())
      e.account.add(e);

    fireAddEvent(getIndex(entry));
   }


  /**
   * Updates an entry in this journal by deleting the old entry and adding the
   * new one.
   *
   * @param entry the new {@code JournalEntry} to replace the old one.
   */
  private void update(JournalEntry entry)
   {
    //Get the entry being replaced - this is round-about, but SortedSet does not
    //have a get(Object) method.  This approach relies on the comparator comparing
    //by ID only, so that entry is equal to oldEntry.
    JournalEntry oldEntry = null;
    SortedSet<JournalEntry> tail = entriesByID.tailSet(entry);
    oldEntry = tail.first();

    if(oldEntry.getID() != entry.getID())
     {
      System.err.println("Entry not found updating entry. Expected " + entry.getID() +"  Found " + oldEntry.getID());
      return;
     }

    remove(oldEntry, false);
    add(entry);
   }


  /**
   * Removes an entry from this journal.
   *
   * @param entry the entry to remove.
   */
  public void remove(JournalEntry entry)
   { remove(entry, true); }


  /**
   * Removes an entry from this journal.
   *
   * @param id the ID of the entry to remove.
   */
  void remove(long id)
   {
    //Get the entry being replaced - this is round-about, but SortedSet does not
    //have a get(Object) method.  This approach relies on the comparator comparing
    //by ID only, so that entry is equal to oldEntry.
    JournalEntry oldEntry = new JournalEntry(this, id, null, null);
    SortedSet<JournalEntry> tail = entriesByID.tailSet(oldEntry);
    oldEntry = tail.first();

    if(oldEntry.getID() != id)
      return;

    remove(oldEntry, false);
   }


  /**
   * Removes an entry from this journal.
   *
   * @param entry the entry to remove.
   * @param record specifies if this deletion should be recorded in the database.
   */
  private void remove(JournalEntry entry, boolean record)
   {
    int index = getIndex(entry);

    entriesByDate.remove(entry);
    entriesByID.remove(entry);

    for(AccountEntry e : entry.getAmounts())
      e.account.remove(e);

    fireRemoveEvent(index);

    if(record)
      DB.recordDeletion(entry);
   }


  /**
   * Gets the index of the specified entry.
   */
  private int getIndex(JournalEntry entry)
   {
    SortedSet<JournalEntry> set = entriesByDate.headSet(entry);
    return set.size();
   }


  public TableModel getDataModel()
   {
    if(dataModel == null)
      dataModel = new DataModel(this);

    return dataModel;
   }


  /**
   * Notifies the data model that an entry has been added.
   *
   * @param index the index of the added entry.
   */
  private void fireAddEvent(int index)
   {
    if(dataModel == null)
      return;

    dataModel.updateList();
    dataModel.fireTableRowsInserted(index, index);
   }


  /**
   * Notifies the data model that an entry has been deleted.
   *
   * @param index the index of the deleted entry.
   */
  private void fireRemoveEvent(int index)
   {
    if(dataModel == null)
      return;

    dataModel.updateList();
    dataModel.fireTableRowsDeleted(index, index);
   }
 }