/*
 * ICESOFT COMMERCIAL SOURCE CODE LICENSE V 1.1
 *
 * The contents of this file are subject to the ICEsoft Commercial Source
 * Code License Agreement V1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the
 * License at
 * http://www.icesoft.com/license/commercial-source-v1.1.html
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * Copyright 2009-2015 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 */
package com.icesoft.faces.facelets.component.select;

import javax.faces.event.PhaseId;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * <p>Backing bean that should be used with the SelectInputCalendar component.
 * The intended use is to only use the bean with the SelectInputCalendar
 * and ideally only manipulate the calendar instance variable.  If the api
 * you are interacting with uses Date object then it can be easily be changed
 * to a GregorianCalendar object with the method setTime().  For example:
 * </p>
 * <ul>someSelectInputCalendar.setDate(myDate);</ul>
 * <p>This class doesn't currently have any JSF validation facilities but it
 * could easily be extended. However the GregorianCalendar object will automatically
 * correct most dates.</p>
 *
 * @author ICEfaces Technologies Inc.
 */
public class SelectInputCalendar implements Serializable {

    public static final int NUMBER_OF_YEARS_DISPLAY = 8;

    // main calendar store, should be passed back to Beans that interact with
    // this bean.
    protected GregorianCalendar calendar;

    // value bindings for the three selectOneMenu items.
    private int month;
    private int day;
    private int year;

    // value binding for the three selectOneMenu items.
    private static SelectItem[] months;
    private SelectItem[] years;
    private SelectItem[] days;

    static {
        // months never change so they can be final.
        months = new SelectItem[12];
        months[0] = new SelectItem(Calendar.JANUARY, "January");
        months[1] = new SelectItem(Calendar.FEBRUARY, "February");
        months[2] = new SelectItem(Calendar.MARCH, "March");
        months[3] = new SelectItem(Calendar.APRIL, "April");
        months[4] = new SelectItem(Calendar.MAY, "May");
        months[5] = new SelectItem(Calendar.JUNE, "June");
        months[6] = new SelectItem(Calendar.JULY, "July");
        months[7] = new SelectItem(Calendar.AUGUST, "August");
        months[8] = new SelectItem(Calendar.SEPTEMBER, "September");
        months[9] = new SelectItem(Calendar.OCTOBER, "October");
        months[10] = new SelectItem(Calendar.NOVEMBER, "November");
        months[11] = new SelectItem(Calendar.DECEMBER, "December");
    }

    /**
     * Creates a new SelectInputCalendar object.
     *
     * @param timezone timezone that the calendar resides in.
     */
    public SelectInputCalendar(TimeZone timezone) {

        if (timezone != null) {
            calendar = new GregorianCalendar(timezone);
        } else {
            calendar = new GregorianCalendar(TimeZone.getDefault());
        }

        // Ensure beginning of the day.
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        setDefaultMaxDays();
        // update values of year, month day
        updateCalendarValues();
        setMaxYears();
    }

    /**
     * Creates a new SelectInputCalendar object.  The server's current timezone
     * is used for this instance.
     */
    public SelectInputCalendar() {
        this(null);
    }

    /**
     * Sets the timezone used by the GregorianCalendar instance.
     *
     * @param timezone timezone to be applied the the GregorianCalendar instance.
     */
    public void setTimeZone(TimeZone timezone) {
        calendar.setTimeZone(timezone);
    }

    /**
     * When the month selection component is changed, this method makes sure
     * that the calendar is correctly updated. As a side effect of a month
     * change, the day value may be incremented to compensate for the correct
     * number of days in the given month.
     *
     * @param event jsf value change event.
     */
    public void monthChanged(ValueChangeEvent event) {
        if (event.getPhaseId() != PhaseId.INVOKE_APPLICATION) {
            event.setPhaseId(PhaseId.INVOKE_APPLICATION);
            event.queue();
        } else {
            int newMonth = (Integer) event.getNewValue();
            calendar.set(Calendar.MONTH, newMonth);
            setDefaultMaxDays();
            // update values of year, month day
            updateCalendarValues();
        }
    }

    /**
     * When the day selection component is changed, this method makes sure
     * the calendar object is correctly updated.
     *
     * @param event jsf value change event.
     */
    public void dayChanged(ValueChangeEvent event) {
        if (event.getPhaseId() != PhaseId.INVOKE_APPLICATION) {
            event.setPhaseId(PhaseId.INVOKE_APPLICATION);
            event.queue();
        } else {
            int newDay = (Integer) event.getNewValue();
            calendar.set(Calendar.DAY_OF_MONTH, newDay);
            // update values of year, month day
            updateCalendarValues();
        }
    }

    /**
     * When the year selection component is changed, this method makes sure that
     * the calendar is correctly updated.  A day check also takes place to
     * ensure that the leap year is accounted for, adjustments are made as needed.
     *
     * @param event jsf value change event.
     */
    public void yearChanged(ValueChangeEvent event) {
        if (event.getPhaseId() != PhaseId.INVOKE_APPLICATION) {
            event.setPhaseId(PhaseId.INVOKE_APPLICATION);
            event.queue();
        } else {
            int newMonth = (Integer) event.getNewValue();
            calendar.set(Calendar.YEAR, newMonth);
            setDefaultMaxDays();
            // update values of year, month day
            updateCalendarValues();
        }
    }

    public void setDate(Date date) {
        calendar.setTime(date);
        // update values of year, month day
        updateCalendarValues();
        // update days and years, in case it was selected by popup
        setDefaultMaxDays();
        setMaxYears();
    }

    /**
     * Converts the calendar values into three primary values, year, month and
     * day, represented by ints.
     */
    protected void updateCalendarValues() {
        year = calendar.get(Calendar.YEAR);
        month = calendar.get(Calendar.MONTH);
        day = calendar.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * Sets the default max number of days to display in the selection
     * component. The number of days is based on the GregorianCalendar
     * actual maximum for the current day of month which takes into account
     * leap years and other calendar oddities.
     */
    protected void setDefaultMaxDays() {
        int maxDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
        days = new SelectItem[maxDays];
        for (int i = 0, j = 1; i < maxDays; i++, j++) {
            days[i] = new SelectItem(j, String.valueOf(j));
        }
    }

    /**
     * Sets the max number of years to display in the selection component. The
     * max number of years is defined by the constant NUMBER_OF_YEARS_DISPLAY.
     */
    protected void setMaxYears() {
        int currentYear = new GregorianCalendar().get(Calendar.YEAR);
        years = new SelectItem[NUMBER_OF_YEARS_DISPLAY];
        for (int i = currentYear, j = 0, max = years.length;
             j < max; i++, j++) {
            years[j] = new SelectItem(i, String.valueOf(i));
        }
    }

    public GregorianCalendar getCalendar() {
        return calendar;
    }

    public void setCalendar(GregorianCalendar calendar) {
        this.calendar = calendar;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public SelectItem[] getMonths() {
        return months;
    }

    public SelectItem[] getDays() {
        return days;
    }

    public SelectItem[] getYears() {
        return years;
    }
}