// DayChooser

package org.javamoney.examples.ez.money.gui.chooser;

import static java.awt.event.MouseEvent.BUTTON1;
import static java.awt.event.MouseEvent.MOUSE_CLICKED;
import static java.awt.event.MouseEvent.MOUSE_ENTERED;
import static java.awt.event.MouseEvent.MOUSE_EXITED;
import static java.util.Calendar.DATE;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.DAY_OF_WEEK;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SUNDAY;
import static java.util.Calendar.YEAR;
import static javax.swing.BorderFactory.createLineBorder;

import java.awt.Color;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DateFormatSymbols;
import java.util.GregorianCalendar;

import javax.swing.JLabel;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.MatteBorder;

import org.javamoney.examples.ez.common.gui.Panel;
import org.javamoney.examples.ez.common.utility.ActionSignaler;

/**
 * This class facilitates choosing days in a month.
 */
public
final
class
DayChooser
extends Panel
{
  /**
   * Constructs a new chooser that will initialize the days according to the
   * specified calendar.
   *
   * @param calendar The calendar to initialize the chooser.
   * @param firstDayOfWeek The weekday the chooser should start with.
   */
  public
  DayChooser(GregorianCalendar calendar, int firstDayOfWeek)
  {
    setActionSignaler(new ActionSignaler());
    setCalendar(calendar);

    getCalendar().setFirstDayOfWeek(firstDayOfWeek);

    createMonthDays();

    buildPanel();

    displayDays();
  }

  /**
   * This method adds the action listener to the chooser.
   *
   * @param listener The action listener to add.
   */
  public
  void
  addActionListener(ActionListener listener)
  {
    getActionSignaler().addListener(listener);
  }

  /**
   * This method returns the selected day's month.
   *
   * @return The selected day's month.
   */
  public
  int
  getMonth()
  {
    return getCalendar().get(MONTH);
  }

  /**
   * This method returns the selected day.
   *
   * @return The selected day.
   */
  public
  int
  getSelectedDay()
  {
    return getCalendar().get(DATE);
  }

  /**
   * This method returns the selected day's year.
   *
   * @return The selected day's year.
   */
  public
  int
  getYear()
  {
    return getCalendar().get(YEAR);
  }

  /**
   * This method sets the selected day and configures the weekday given the
   * specified month and year. Invoking this method causes the chooser to
   * re-render the days.
   *
   * @param day The day to select.
   * @param month The month for configuring the weekday.
   * @param year The date's year for determining weekday.
   */
  public
  void
  setDate(int day, int month, int year)
  {
    int max = getDaysInMonth(year, month);

    if(day > max)
    {
      day = max;
    }

    getCalendar().set(DATE, day);
    getCalendar().set(MONTH, month);
    getCalendar().set(YEAR, year);

    displayDays();
  }

  /**
   * This method configures the weekday given the specified month. Invoking this
   * method causes the chooser to re-render the days.
   *
   * @param month The month for configuring the weekday.
   */
  public
  void
  setMonth(int month)
  {
    setDate(getSelectedDay(), month, getYear());
  }

  /**
   * This method sets the selected day. Invoking this method causes the chooser
   * to re-render the days.
   *
   * @param day The day to select.
   */
  public
  void
  setSelectedDay(int day)
  {
    setDate(day, getMonth(), getYear());
  }

  /**
   * This configures the weekday given the specified year. Invoking this method
   * causes the chooser to re-render the days.
   *
   * @param year The year for configuring the weekday.
   */
  public
  void
  setYear(int year)
  {
    setDate(getSelectedDay(), getMonth(), year);
  }

  //////////////////////////////////////////////////////////////////////////////
  // Start of private methods.
  //////////////////////////////////////////////////////////////////////////////

  private
  void
  buildPanel()
  {
    String[] days = new DateFormatSymbols().getShortWeekdays();
    int start = getCalendar().getFirstDayOfWeek();

    // Build panel.
    setFill(GridBagConstraints.BOTH);
    addSpacer(0, 0, 1, 1, 1, 16);

    // Weekdays.
    for(int len = 0, col = 1; len < MAX_COLUMNS; ++len)
    {
      add(createWeekdayLabel(days[start++]), col++, 0, 1, 1, 14, 0);

      if(start > MAX_COLUMNS)
      {
        start = SUNDAY; // Start the week over.
      }
    }

    addSpacer(8, 0, 1, 1, 1, 0);

    // Month days.
    for(int len = 0, row = 1; row <= MAX_ROWS; ++row)
    {
      for(int col = 1; col <= MAX_COLUMNS; ++col, ++len)
      {
        add(getMonthDays()[len], col, row, 1, 1, 0, 14);
      }
    }
  }

  private
  void
  clearMonthDays()
  {
    for(int len = 0; len < getMonthDays().length; ++len)
    {
      getMonthDays()[len].setBackground(getBackground());
      getMonthDays()[len].setBorder(BORDER_NORMAL);
      getMonthDays()[len].setEnabled(false);
      getMonthDays()[len].setForeground(getForeground());
    }
  }

  private
  void
  createMonthDays()
  {
    MouseHandler listener = new MouseHandler();

    itsMonthDays = new JLabel[MAX_COLUMNS * MAX_ROWS];

    for(int len = 0; len < getMonthDays().length; ++len)
    {
      getMonthDays()[len] = new JLabel("", SwingConstants.CENTER);
      getMonthDays()[len].addMouseListener(listener);
      getMonthDays()[len].setOpaque(true);
    }
  }

  private
  static
  JLabel
  createWeekdayLabel(String day)
  {
    JLabel label = new JLabel(day, SwingConstants.CENTER);

    label.setBorder(new MatteBorder(1, 0, 1, 0, Color.GRAY));
    label.setFont(label.getFont().deriveFont(Font.PLAIN));
    label.setOpaque(true);

    return label;
  }

  private
  void
  displayDays()
  {
    int dayOfWeek = getStartingWeekdayIndex();
    int max = getDaysInMonth(getYear(), getMonth());
    int day = getDaysInMonth(getYear(), getMonth() - 1);
    int len = 0;

    clearMonthDays();

    // Show trailing days in previous month.
    for(len = dayOfWeek - 1; len >= 0; --len, --day)
    {
      getMonthDays()[len].setText("" + day);
    }

    for(len = dayOfWeek, day = 1; day <= max; ++len, ++day)
    {
      getMonthDays()[len].setEnabled(true);
      getMonthDays()[len].setText("" + day);

      // If the day is the currently selected day, then highlight it.
      if(day == getSelectedDay())
      {
        selectLabel(getMonthDays()[len]);
      }
    }

    // Fill out the rest of the calendar with days from the next month.
    for(day = 1; len < getMonthDays().length; ++len, ++day)
    {
      getMonthDays()[len].setText("" + day);
    }
  }

  private
  ActionSignaler
  getActionSignaler()
  {
    return itsActionSignaler;
  }

  private
  GregorianCalendar
  getCalendar()
  {
    return itsCalendar;
  }

  private
  static
  int
  getDaysInMonth(int year, int month)
  {
    return new GregorianCalendar(year, month, 1).getActualMaximum(DAY_OF_MONTH);
  }

  private
  JLabel[]
  getMonthDays()
  {
    return itsMonthDays;
  }

  private
  int
  getStartingWeekdayIndex()
  {
    int day = new GregorianCalendar(getYear(), getMonth(), 1).get(DAY_OF_WEEK);

    // Put day in proper array index form.
    day -= getCalendar().getFirstDayOfWeek();

    if(day < 0)
    {
      day += 7;
    }

    return day;
  }

  private
  static
  void
  selectLabel(JLabel label)
  {
    label.setBackground(HIGHLIGHT_CELL_COLOR);
    label.setBorder(BORDER_SELECTED);
    label.setForeground(Color.WHITE);
  }

  private
  void
  setActionSignaler(ActionSignaler list)
  {
    itsActionSignaler = list;
  }

  private
  void
  setCalendar(GregorianCalendar calendar)
  {
    itsCalendar = new GregorianCalendar();

    getCalendar().set(DATE, calendar.get(DATE));
    getCalendar().set(MONTH, calendar.get(MONTH));
    getCalendar().set(YEAR, calendar.get(YEAR));
  }

  //////////////////////////////////////////////////////////////////////////////
  // Start of inner classes.
  //////////////////////////////////////////////////////////////////////////////

  private
  class
  MouseHandler
  extends MouseAdapter
  {
    /*
     * This method handles the specific mouse event. Firstly, it ignores all
     * events on days that are disabled or are already the selected day. If the
     * event is a click, then it sets that day to be the selected day. If the
     * event is a mouse entered or mouse exited, then it sets that day's border
     * for a UI effect.
     */
    public
    void
    doMouseEvent(MouseEvent event)
    {
      JLabel label = (JLabel)event.getSource();

      if(label.isEnabled() == true)
      {
        if(Integer.parseInt(label.getText()) != getSelectedDay())
        {
          if(event.getID() == MOUSE_CLICKED && event.getButton() == BUTTON1)
          {
            getCalendar().set(DATE, Integer.valueOf(label.getText()).intValue());
            displayDays();

            getActionSignaler().sendSignal(DayChooser.this, ACTION_DAY_SELECTED);
          }
          else if(event.getID() == MOUSE_ENTERED)
          {
            label.setBorder(BORDER_SELECTED);
          }
          else if(event.getID() == MOUSE_EXITED)
          {
            label.setBorder(BORDER_NORMAL);
          }
        }
        else if(event.getClickCount() == 2)
        {
          getActionSignaler().sendSignal(DayChooser.this, ACTION_DAY_CHOSEN);
        }
      }
    }

    @Override
    public
    void
    mouseClicked(MouseEvent event)
    {
      doMouseEvent(event);
    }

    @Override
    public
    void
    mouseEntered(MouseEvent event)
    {
      doMouseEvent(event);
    }

    @Override
    public
    void
    mouseExited(MouseEvent event)
    {
      doMouseEvent(event);
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // Start of class members.
  //////////////////////////////////////////////////////////////////////////////

  private ActionSignaler itsActionSignaler;
  private GregorianCalendar itsCalendar;
  private JLabel[] itsMonthDays;

  private static final Border BORDER_NORMAL = createLineBorder(new Panel().getBackground());
  private static final Border BORDER_SELECTED = createLineBorder(Color.BLACK);

  private static final Color HIGHLIGHT_CELL_COLOR = new Color(0, 51, 102);

  private static final int MAX_COLUMNS = 7;
  private static final int MAX_ROWS = 6;

  /**
   * This is an action constant that indicates a day was chosen via a double
   * click.
   */
  public static final String ACTION_DAY_CHOSEN = "Day Chosen";
  /**
   * This is an action constant that indicates a day was clicked on.
   */
  public static final String ACTION_DAY_SELECTED = "Day Selected";
}