package nxm.ucl.libg;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import javax.swing.JButton;
import javax.swing.JPanel;

/** A class that creates a Calendar-based "date picker".
    @author crs
    @since NeXtMidas 2.9.1
 **/
public class MJDayPicker extends JPanel implements ActionListener, KeyListener, FocusListener {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2012122809331L; // for Serializable interface

  /** The days of the calendar */
  protected JButton[] days;
  /** The weeks of the calendar */
  protected JButton[] weeks;
  /** The selected day */
  protected JButton selectedDay;
  /** The Panel containing the calendar weeks */
  protected JPanel weekPanel;
  /** dayPanel stores the days of the month that are selectable */
  protected JPanel dayPanel;
  /** The currently selected day */
  int day;
  /** The default background color for each Day button */
  protected Color oldDayBackgroundColor;
  /** The color of the selected Day button */
  protected Color selectedColor;
  /** The foreground color for the current day */
  protected Color currentDayForeground;
  /** The foreground color for Sundays */
  protected Color sundayForeground;
  /** The foreground color for weekdays */
  protected Color weekdayForeground;
  /** The background color for each day button */
  protected Color decorationBackgroundColor;

  /** Holds the names of each day of the week */
  protected String[] dayNames;
  /** The main calendar created based on the current Locale */
  protected Calendar calendar;
  /** Copy of the main calendar based on the current Locale */
  protected Calendar today;
  /** Holds the locale representing the current date and time */
  protected Locale locale;

  /** Flag for whether or not MJDayPicker is initialized */
  protected boolean initialized;
  /** Flag for displaying the week of the year */
  protected boolean weekOfYearVisible = false;      // Do not change
  /** Flag for displaying the background color of each day and week component */
  protected boolean decorationBackgroundVisible = true;
  /** Flag for display the button borders of each day and week component */
  protected boolean decorationBordersVisible;
  /** Flag for displaying the day button borders */
  protected boolean dayBordersVisible;
  /** Flag for always reporting changes in Day properties */
  private boolean alwaysFireDayProperty;

  /** The latest selectable date */
  private Date maxSelectableDate;
  /** The earliest selectable date */
  private Date minSelectableDate;
  /** The default value for the latest selectable date */
  private Date defaultMaxSelectableDate;
  /** The default value for the earliest selectable date */
  private Date defaultMinSelectableDate;

  /** GDatePicker object for selecting the Year **/
  private MJYearPicker  yearPicker;
  /** GDatePicker object for selecting the Month **/
  private MJMonthPicker monthPicker;

  /**
    Constructor to create a new MJDayPicker object. The MJDayPicker creates a
    selectable calendar, similar to a Web-based airline reservation system.
   */
  @SuppressWarnings("serial")
  public MJDayPicker () {
    setName("MJDayPicker");

    locale = Locale.getDefault();
    days = new JButton[49];
    selectedDay = null;                     // At first use, selected day is null

    calendar = Calendar.getInstance(locale);
    today = (Calendar) calendar.clone();

    setLayout(new BorderLayout());

    dayPanel = new JPanel();
    dayPanel.setLayout(new GridLayout(7, 7));

    // Set decoration on Sundays and weekdays
    currentDayForeground = new Color(45, 233, 45);
    sundayForeground = new Color(164, 0, 0);
    weekdayForeground = new Color(0, 90, 164);
    decorationBackgroundColor = new Color(210, 228, 238);

    // Create the days buttons
    for (int y = 0; y < 7; y++) {
      for (int x = 0; x < 7; x++) {
        int index = x + (7 * y);

        if (y == 0)
          days[index] = new DecoratorButton();        // Button has no focus
        else {
          days[index] = new JButton("x") {

            public void paint(Graphics g) {
              if (selectedDay == this) {
                g.setColor(selectedColor);
                g.fillRect(0, 0, getWidth(), getHeight());
              }
              super.paint(g);
            }
          };

          days[index].addActionListener(this);
          days[index].addKeyListener(this);
          days[index].addFocusListener(this);
        }

        days[index].setMargin(new Insets(0, 0, 0, 0));
        days[index].setFocusPainted(false);
        dayPanel.add(days[index]);
      }
    }

    weekPanel = new JPanel();
    weekPanel.setLayout(new GridLayout(7, 1));
    weeks = new JButton[7];

    for (int i = 0; i < 7; i++) {
      weeks[i] = new DecoratorButton();
      weeks[i].setMargin(new Insets(0, 0, 0, 0));
      weeks[i].setFocusPainted(false);
      weeks[i].setForeground(new Color(100, 100, 100));

      if (i != 0) {
        weeks[i].setText("0" + (i + 1));
      }

      weekPanel.add(weeks[i]);
    }

    Calendar tmpCalendar = Calendar.getInstance();
    tmpCalendar.set(1, 0, 1, 1, 1);
    defaultMinSelectableDate = tmpCalendar.getTime();
    minSelectableDate = defaultMinSelectableDate;
    tmpCalendar.set(9999, 0, 1, 1, 1);
    defaultMaxSelectableDate = tmpCalendar.getTime();
    maxSelectableDate = defaultMaxSelectableDate;

    init();

    setDay(Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
    add(dayPanel, BorderLayout.CENTER);

    if (weekOfYearVisible) {
      add(weekPanel, BorderLayout.WEST);
    }
    initialized = true;
    updateUI();

  }

  /** Initializes the locale specific names for the days of the week. */
  protected void init () {
    JButton testButton = new JButton();
    oldDayBackgroundColor = testButton.getBackground();
    selectedColor = new Color(220, 220, 220);

    Date date = calendar.getTime();
    calendar = Calendar.getInstance(locale);
    calendar.setTime(date);

    drawDayNames();
    drawDays();
  }

  /**
    The decoration background is the background color of the day titles and the weeks of the year.
    @param decorationBackgroundVisible true, if the decoration background shall be painted.
   */
  public void setDecorationBackgroundVisible (boolean decorationBackgroundVisible) {
    this.decorationBackgroundVisible = decorationBackgroundVisible;
    initDecorations();
  }

  /**
    The decoration border is the button border of the day titles and the weeks of the year.
    @param decorationBordersVisible true, if the decoration border shall be painted.
   */
  public void setDecorationBordersVisible (boolean decorationBordersVisible) {
    this.decorationBordersVisible = decorationBordersVisible;
    initDecorations();
  }

  /**
    The day border outlines each day button.
    @param dayBordersVisible true, if the day border shall be painted.
   */
  public void setDayBordersVisible (boolean dayBordersVisible) {
    this.dayBordersVisible = dayBordersVisible;
    if (initialized) {
      for (int x = 7; x < 49; x++) {
        days[x].setContentAreaFilled(true);
        days[x].setBorderPainted(dayBordersVisible);
      }
    }
  }

  /**
    Sets the given Font for each day button.
    @param font The new Font
   */
  public void setFont (Font font) {
    if (days != null) {
      for (int i = 0; i < 49; i++) {
        days[i].setFont(font);
      }
    }
    if (weeks != null) {
      for (int i = 0; i < 7; i++) {
        weeks[i].setFont(font);
      }
    }
  }


  /** Updates the UI and sets the day button preferences. */
  public void updateUI () {
    super.updateUI();
    setFont(Font.decode("Dialog Plain 10"));

    if (weekPanel != null) {
      weekPanel.updateUI();
    }
    if (initialized) {
      setDayBordersVisible(true);
      setDecorationBackgroundVisible(decorationBackgroundVisible);
      setDecorationBordersVisible(decorationBordersVisible);
    }
  }

  /** Draws the day names of the day columns. */
  private void drawDayNames () {
    int firstDayOfWeek = calendar.getFirstDayOfWeek();
    DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(locale);
    dayNames = dateFormatSymbols.getShortWeekdays();

    int day = firstDayOfWeek;

    for (int i = 0; i < 7; i++) {
      days[i].setText(dayNames[day]);

      if (day == 1) {
        days[i].setForeground(sundayForeground);
      }
      else {
        days[i].setForeground(weekdayForeground);
      }

      if (day < 7) {
        day++;
      } else {
        day -= 6;
      }
    }
  }

  /**
   * Initializes both day names and weeks of the year.
   */
  protected void initDecorations () {
    for (int x = 0; x < 7; x++) {
      days[x].setContentAreaFilled(decorationBackgroundVisible);
      days[x].setBorderPainted(decorationBordersVisible);
      days[x].invalidate();
      days[x].repaint();
      weeks[x].setContentAreaFilled(decorationBackgroundVisible);
      weeks[x].setBorderPainted(decorationBordersVisible);
      weeks[x].invalidate();
      weeks[x].repaint();
    }
  }

  /**
   * Hides and shows the week buttons.
   */
  protected void drawWeeks () {
    Calendar tmpCalendar = (Calendar) calendar.clone();

    for (int i = 1; i < 7; i++) {
      tmpCalendar.set(Calendar.DAY_OF_MONTH, (i * 7) - 6);

      int week = tmpCalendar.get(Calendar.WEEK_OF_YEAR);
      String buttonText = Integer.toString(week);

      if (week < 10) {
        buttonText = "0" + buttonText;
      }

      weeks[i].setText(buttonText);

      if ((i == 5) || (i == 6)) {
        weeks[i].setVisible(days[i * 7].isVisible());
      }
    }
  }

  /**
   * Hides and shows the day buttons.
   */
  protected void drawDays () {
    Calendar tmpCalendar = (Calendar) calendar.clone();
    tmpCalendar.set(Calendar.HOUR_OF_DAY, 0);
    tmpCalendar.set(Calendar.MINUTE, 0);
    tmpCalendar.set(Calendar.SECOND, 0);
    tmpCalendar.set(Calendar.MILLISECOND, 0);

    Calendar minCal = Calendar.getInstance();
    minCal.setTime(minSelectableDate);
    minCal.set(Calendar.HOUR_OF_DAY, 0);
    minCal.set(Calendar.MINUTE, 0);
    minCal.set(Calendar.SECOND, 0);
    minCal.set(Calendar.MILLISECOND, 0);

    Calendar maxCal = Calendar.getInstance();
    maxCal.setTime(maxSelectableDate);
    maxCal.set(Calendar.HOUR_OF_DAY, 0);
    maxCal.set(Calendar.MINUTE, 0);
    maxCal.set(Calendar.SECOND, 0);
    maxCal.set(Calendar.MILLISECOND, 0);

    int firstDayOfWeek = tmpCalendar.getFirstDayOfWeek();
    tmpCalendar.set(Calendar.DAY_OF_MONTH, 1);

    int firstDay = tmpCalendar.get(Calendar.DAY_OF_WEEK) - firstDayOfWeek;

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

    int i;

    for (i = 0; i < firstDay; i++) {
      days[i + 7].setVisible(false);
      days[i + 7].setText("");
    }

    tmpCalendar.add(Calendar.MONTH, 1);

    Date firstDayInNextMonth = tmpCalendar.getTime();
    tmpCalendar.add(Calendar.MONTH, -1);

    Date day = tmpCalendar.getTime();
    int n = 0;
    Color foregroundColor = getForeground();

    while (day.before(firstDayInNextMonth)) {
      days[i + n + 7].setText(Integer.toString(n + 1));
      days[i + n + 7].setVisible(true);

      if ((tmpCalendar.get(Calendar.DAY_OF_YEAR) == today
          .get(Calendar.DAY_OF_YEAR))
          && (tmpCalendar.get(Calendar.YEAR) == today
          .get(Calendar.YEAR))) {
        days[i + n + 7].setForeground(currentDayForeground);
      } else {
        days[i + n + 7].setForeground(foregroundColor);
      }

      if ((n + 1) == this.day) {
        days[i + n + 7].setBackground(selectedColor);
        selectedDay = days[i + n + 7];
      } else {
        days[i + n + 7].setBackground(oldDayBackgroundColor);
      }

      if (tmpCalendar.before(minCal) || tmpCalendar.after(maxCal)) {
        days[i + n + 7].setEnabled(false);
      } else {
        days[i + n + 7].setEnabled(true);
      }

      n++;
      tmpCalendar.add(Calendar.DATE, 1);
      day = tmpCalendar.getTime();
    }

    for (int k = n + i + 7; k < 49; k++) {
      days[k].setVisible(false);
      days[k].setText("");
    }

    drawWeeks();
  }

  /**
    Sets the calendar's current day.
    @param d The new day value (1-31)
    @see #getDay
   */
  public void setDay (int d) {

    if (d < 1)
      d = 1;

    Calendar tmpCalendar = (Calendar) calendar.clone();
    tmpCalendar.set(Calendar.DAY_OF_MONTH, 1);
    tmpCalendar.add(Calendar.MONTH, 1);
    tmpCalendar.add(Calendar.DATE, -1);

    int maxDaysInMonth = tmpCalendar.get(Calendar.DATE);

    if (d > maxDaysInMonth) {
      d = maxDaysInMonth;
    }

    int oldDay = day;
    day = d;

    if (selectedDay != null) {
      selectedDay.setBackground(oldDayBackgroundColor);
      selectedDay.repaint();
    }

    for (int i = 7; i < 49; i++) {
      if (days[i].getText().equals(Integer.toString(day))) {
        selectedDay = days[i];
        selectedDay.setBackground(selectedColor);
        break;
      }
    }

    if (alwaysFireDayProperty) {
      firePropertyChange("day", 0, day);
    } else {
      firePropertyChange("day", oldDay, day);
    }

  }

  /** Get the selected day
   *  @return The currently selected day. */
  public int getDay () {
    return day;
  }

  /**
   * Rolls the currently selected date to account for change in year value.
   * @param year the new year value
   */
  public void setYear (int year) {
    calendar.set(Calendar.YEAR, year);
    drawDays();
  }

  /**
    Sets the current month of the calendar. The new value is used to propagate
    the date change to other components in the calendar.
    @param month The new month value.
   */
  public void setMonth (int month) {
    calendar.set(Calendar.MONTH, month);
    int maxDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

    int adjustedDay = day;

    // Adjust the day selection in the event that this month
    // does not have as many days as currently selected
    if (day > maxDays) {
      adjustedDay = maxDays;
      setDay(adjustedDay);
    }

    // Redraw the calendar
    drawDays();
  }

  /**
    Sets the foreground color of the day picker component. Iterates
    through the days of the current calendar and redraws the days.
    @param foreground The new foreground Color object
   */
  public void setForeground (Color foreground) {
    super.setForeground(foreground);

    if (days != null) {
      for (int i = 7; i < 49; i++) {
        days[i].setForeground(foreground);;
      }

      // Redraw the calendar
      drawDays();
    }
  }

  /** Currently not used. */
  @Override
  public void focusGained (FocusEvent evt) {}

  /** Currently not used */
  @Override
  public void focusLost (FocusEvent evt) {}

  /**
    Handles key presses from within this object. The day picker is able
    to detect user keys (up, down, left, right) as a means to select a new day.
   */
  @Override
  public void keyPressed (KeyEvent e) {
    int offset = (e.getKeyCode() == KeyEvent.VK_UP) ? (-7) : ((e
        .getKeyCode() == KeyEvent.VK_DOWN) ? (+7)
            : ((e.getKeyCode() == KeyEvent.VK_LEFT) ? (-1) : ((e
                .getKeyCode() == KeyEvent.VK_RIGHT) ? (+1) : 0)));

    int newDay = getDay() + offset;

    if ((newDay >= 1)
        && (newDay <= calendar.getMaximum(Calendar.DAY_OF_MONTH))) {
      setDay(newDay);
    }

  }

  /** Currently unused. */
  @Override
  public void keyTyped (KeyEvent evt) {}

  /** Currently unused. */
  @Override
  public void keyReleased (KeyEvent evt) {}

  /**
    Called when an ActionEvent is captured from within the day picker.
    Specifically, the user's selected day will be stored based on the incoming event.
   */
  @Override
  public void actionPerformed (ActionEvent evt) {

    JButton button = (JButton) evt.getSource();
    String buttonText = button.getText();

    int day = Integer.valueOf(buttonText).intValue(); // updated 3.9.0 to use valueOf instead of new instance
    setDay(day);
  }

  // ** Accessory Methods **

  /**
    Sets the MJYearPicker reference.
    This reference allows the MJDayPicker to know about changes to the currently selected year.
    @param yearPicker The MJYearPicker reference
   */
  public void setYearPicker (MJYearPicker yearPicker) {
    this.yearPicker = yearPicker;
  }

  /**
    Sets the MJMonthPicker reference.
    @param monthPicker the new MJMonthPicker to use
   */
  public void setMonthPicker (MJMonthPicker monthPicker) {
    this.monthPicker = monthPicker;
  }

  /** Inner class representing the buttons used for each day and week */
  class DecoratorButton extends JButton {
    private static final long serialVersionUID = -5306477668406547496L;

    public DecoratorButton () {
      setBackground(decorationBackgroundColor);
      setContentAreaFilled(decorationBackgroundVisible);
      setBorderPainted(decorationBordersVisible);
    }

    public void addMouseListener (MouseListener l) {
    }

    public boolean isFocusable () {
      return false;
    }

    public void paint (Graphics g) {

      if (decorationBackgroundVisible) {
        g.setColor(decorationBackgroundColor);
      }
      else {
        g.setColor(days[7].getBackground());
      }

      g.fillRect(0, 0, getWidth(), getHeight());

      if (isBorderPainted()) {
        setContentAreaFilled(true);
      }
      else {
        setContentAreaFilled(false);
      }

      super.paint(g);
    }
  } // end inner class DecoratorButton

}