/**
 * License Agreement.
 *
 *  JBoss RichFaces - Ajax4jsf Component Library
 *
 * Copyright (C) 2007  Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.richfaces.component;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIInput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.convert.DateTimeConverter;
import javax.faces.el.MethodBinding;
import javax.faces.el.ValueBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.event.ValueChangeEvent;

import org.ajax4jsf.component.AjaxComponent;
import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.event.AjaxEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.event.CurrentDateChangeEvent;
import org.richfaces.event.CurrentDateChangeListener;
import org.richfaces.model.CalendarDataModel;
import org.richfaces.model.CalendarDataModelItem;
import org.richfaces.renderkit.CalendarRendererBase;


// import org.richfaces.renderkit.html.BaseGradient.Data;

/**
 * JSF component class
 * 
 */
public abstract class UICalendar extends UIInput implements AjaxComponent {

	/**
	 * firstWeekDay
	 * Gets what the first day of the week is; e.g., SUNDAY in the U.S., MONDAY in France.
	 */
	private int  _firstWeekDay = getDefaultFirstWeekDay();		
	/**
	 * Flag indicated what firstWeekDay is set.
	 */
	private boolean _firstWeekDaySet = false;	

	/**
	 * minDaysInFirstWeek
	 * Gets what the minimal days required in the first week of the year 
				are; e.g., if the first week is defined as one that contains the first 
				day of the first month of a year, this method returns 1. If the 
				minimal days required must be a full week, this method returns 7.
	 */
	private int  _minDaysInFirstWeek = getDefaultMinDaysInFirstWeek();		
	/**
	 * Flag indicated what minDaysInFirstWeek is set.
	 */
	private boolean _minDaysInFirstWeekSet = false;	

	public static final String COMPONENT_TYPE = "org.richfaces.Calendar";

	private static final String COMPONENT_FAMILY = "org.richfaces.Calendar";

	public static final String AJAX_MODE = "ajax";

	public static final String CLIENT_MODE = "client";

	private final static Log log = LogFactory.getLog(UICalendar.class);

	public abstract Object getLocale();

	public abstract void setLocale(Object locale);

	public abstract TimeZone getTimeZone();

	public abstract void setTimeZone(TimeZone timeZone);

	public abstract Object getPreloadDateRangeBegin();

	public abstract void setPreloadDateRangeBegin(Object date);

	public abstract Object getPreloadDateRangeEnd();

	public abstract void setPreloadDateRangeEnd(Object date);

	public abstract Object getCurrentDate();

	public abstract void setCurrentDate(Object date);

	public abstract CalendarDataModel getDataModel();

	public abstract void setDataModel(CalendarDataModel dataModel);

	public abstract String getDatePattern();

	public abstract void setDatePattern(String pattern);

	public abstract Object getMonthLabels();

	public abstract void setMonthLabels(Object labels);

	public abstract Object getMonthLabelsShort();

	public abstract void setMonthLabelsShort(Object labels);

	public abstract Object getWeekDayLabels();

	public abstract void setWeekDayLabels(Object labels);

	public abstract Object getWeekDayLabelsShort();

	public abstract void setWeekDayLabelsShort(Object labels);

	public abstract String getJointPoint();

	public abstract void setJointPoint(String jointPoint);

	public abstract String getDirection();

	public abstract void setDirection(String direction);

	public abstract boolean isPopup();

	public abstract void setPopup(boolean popup);

	public abstract boolean isDisabled();

	public abstract void setDisabled(boolean disabled);

	public abstract String getButtonLabel();

	public abstract void setButtonLabel(String buttonLabel);

	public abstract String getToolTipMode();

	public abstract void setToolTipMode(String toolTipMode);

	public abstract String getBoundaryDatesMode();

	public abstract void setBoundaryDatesMode(String boundaryDatesMode);

	public abstract MethodBinding getCurrentDateChangeListener();

	public abstract void setCurrentDateChangeListener(
			MethodBinding scrollerListener);

	public abstract String getMode();

	public abstract void setMode(String mode);

	public abstract int getVerticalOffset();

	public abstract void setVerticalOffset(int verticalOffset);
	
	public abstract int getHorizontalOffset();

	public abstract void setHorizontalOffset(int horizontalOffset);

	public abstract String getDayStyleClass();

	public abstract void setDayStyleClass(String DayStyleClass);

	public abstract String getIsDayEnabled();

	public abstract void setIsDayEnabled(String isDayEnabled);
	
	public abstract String getCellHeight();

	public abstract void setCellHeight(String cellHeight);
	
	public abstract String getCellWidth();

	public abstract void setCellWidth(String cellWidth);
	
	public abstract boolean isShowWeekDaysBar();
	
	public abstract void setShowWeekDaysBar(boolean showWeekDaysBar);
	
	public abstract boolean isShowWeeksBar();
	
	public abstract void setShowWeeksBar(boolean showWeeksBar);
	
	public abstract boolean isShowHeader();

	public abstract void setShowHeader(boolean showScrollerBar);

	public abstract boolean isShowFooter();

	public abstract void setShowFooter(boolean showScrollerBar);
	
	public abstract String getTodayControlMode();
	
	public abstract void setTodayControlMode(String todayControlMode);
	
	public abstract boolean isShowApplyButton();
	
	public abstract void setShowApplyButton(boolean showApplyButton);

	// TODO onclick add users onclick

	// currentDate processing -------------------------------------------------

	public Calendar getCalendar() {
		return Calendar.getInstance(getTimeZone(), getAsLocale(getLocale()));
	}

	public Date getConvertedValue(FacesContext context, String currentDateString)
			throws ConverterException {

		DateTimeConverter datetime = new DateTimeConverter();
		datetime.setPattern("m/y");
		Date newCurrentDate = (Date) datetime.getAsObject(context, this,
				currentDateString);
		return newCurrentDate;
	}

	protected void validateValue(FacesContext context, Object newValue) {
		// TODO nick - nick - do we need this?
		// store converted value in submitted value to ease client-side code's
		// life
		// see
		// org.richfaces.renderkit.CalendarRendererBase.getSelectedDate(FacesContext,
		// UICalendar) for more
		setSubmittedValue(newValue);

		super.validateValue(context, newValue);
	}

	public void updateCurrentDate(FacesContext context, Object currentDate) {

		if (context == null) {
			throw new NullPointerException();
		}
		//RF-1073
		try {
			ValueBinding vb = getValueBinding("currentDate");
			if (vb != null) {
				if(vb.getType(context).equals(String.class)){
					DateTimeConverter convert = new DateTimeConverter();
					convert.setLocale(getAsLocale(getLocale()));
					convert.setPattern(getDatePattern());
					vb.setValue(context, convert.getAsString(context, this,
							currentDate));
					return;
				}else if(vb.getType(context).equals(Calendar.class)){
					Calendar c = Calendar.getInstance();
					c.setTime((Date) currentDate);
					vb.setValue(context, c);
					return;
				}else{
					vb.setValue(context, currentDate);
					return;
				}				
			} else {
				setCurrentDate(currentDate);
			}

		} catch (Exception e) {
			setValid(false);
			// XXX nick - kaa - add log.debug(...)
			if (log.isDebugEnabled()) {
				log.debug(" updateCurrentDate method throws exception: "
						+ e.toString(), e);
			}
			e.printStackTrace();
			String messageString = e.toString();
			FacesMessage message = new FacesMessage(messageString);
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
		}
	}

	public Date getCurrentDateOrDefault() {

		Date date = getAsDate(getCurrentDate());

		if (date != null) {
			return date;
		} else {
			Date value = getAsDate(this.getValue());
			if (value != null) {
				return value;
			} else {
				return java.util.Calendar.getInstance(getTimeZone()).getTime();
			}

		}
	}

	public Date getAsDate(Object date) {

		if (date == null) {
			return null;
		} else {
			if (date instanceof Date) {
				return (Date) date;
			} else {
				if (date instanceof String) {
					DateTimeConverter converter = new DateTimeConverter();
					converter.setPattern(this.getDatePattern());
					converter.setLocale(getAsLocale(this.getLocale()));
					converter.setTimeZone(this.getTimeZone());
					FacesContext context = FacesContext.getCurrentInstance();
					return (Date) converter.getAsObject(context, this,
							(String) date);
				} else {
					if (date instanceof Calendar) {
						return ((Calendar) date).getTime();
					} else {

						throw new FacesException("Wrong attibute type");
					}
				}
			}
		}

	}

	public Object getTooltip(Date date) {

		CalendarDataModel calendarDM = (CalendarDataModel) getDataModel();
		if (calendarDM != null) {
			return calendarDM.getToolTip(date);
		} else {
			return null;
		}

	}

	protected Date getDefaultPreloadBegin(Date date) {
		Calendar calendar = Calendar.getInstance(getTimeZone(), getAsLocale(getLocale()));
		calendar.setTime(date);
		calendar.set(Calendar.DATE, calendar.getActualMinimum(Calendar.DATE));
		/*
		 * //force recalculation calendar.getTimeInMillis();
		 * calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek());
		 */
		return calendar.getTime();
	}

	protected Date getDefaultPreloadEnd(Date date) {
		Calendar calendar = Calendar.getInstance(getTimeZone(), getAsLocale(getLocale()));
		calendar.setTime(date);
		calendar.set(Calendar.DATE, calendar.getActualMaximum(Calendar.DATE));
		/*
		 * //force recalculation calendar.getTimeInMillis();
		 * calendar.set(Calendar.DAY_OF_WEEK, getLastDayOfWeek(calendar));
		 */
		return calendar.getTime();
	}

	protected Locale getDefaultLocale() {
		FacesContext facesContext = FacesContext.getCurrentInstance();
		if (facesContext != null) {
			UIViewRoot viewRoot = facesContext.getViewRoot();
			if (viewRoot != null) {
				Locale locale = viewRoot.getLocale();
				if (locale != null) {
					return locale;
				}
			}
		}

		return Locale.US;
	}

	protected TimeZone getDefaultTimeZone() {
		return TimeZone.getDefault();
	}

	public Date convertCurrentDate(String currentDateString) {

		Calendar calendar = Calendar.getInstance();
		calendar.clear();
		calendar.set(Calendar.DATE, 1);
		int idx = currentDateString.indexOf('/');
		if (idx != -1) {
			calendar.set(Calendar.MONTH, Integer.parseInt(currentDateString
					.substring(0, idx)) - 1);
			calendar.set(Calendar.YEAR, Integer.parseInt(currentDateString
					.substring(idx + 1)));

			return calendar.getTime();
		} else {
			return null;
		}

	}

	public void broadcast(FacesEvent event) throws AbortProcessingException {
		// TODO Auto-generated method stub
		if (event instanceof AjaxEvent) {
			FacesContext facesContext = FacesContext.getCurrentInstance();
			AjaxContext ajaxContext = AjaxContext
					.getCurrentInstance(facesContext);
			ajaxContext.addRegionsFromComponent(this);
			if (getPreload() != null) {
				ajaxContext.setResponseData(getPreload());
			}
		} else {
			if (event instanceof CurrentDateChangeEvent) {
				FacesContext facesContext = FacesContext.getCurrentInstance();
				CurrentDateChangeEvent dateChangeEvent = (CurrentDateChangeEvent) event;
				String currentDateString = dateChangeEvent
						.getCurrentDateString();

				if (currentDateString != null) {
					// if currentDateString is not null then event cames from
					// apply request phase
					try {
						// XXX nick - kaa - we should use datePattern
						// attribute-based converter only for selectedDate
						// current date string always has predefined format: m/y
						// review
						// org.richfaces.renderkit.CalendarRendererBase.convertCurrentDate(String)
						// method
						// for more

						// XX nick - kaa - throw exception and review resulting
						// message :)

						Date currentDate = convertCurrentDate(currentDateString);
						CurrentDateChangeEvent newDateChangeEvent = new CurrentDateChangeEvent(
								this, currentDate);
						newDateChangeEvent.queue();
						MethodBinding binding = getCurrentDateChangeListener();

						if (binding != null) {

							binding
									.invoke(facesContext,
											new Object[] { event });
						}

					} catch (Exception e) {
						// XXX nick - kaa - add log.debug(...)
						// XXX nick - kaa - we should stop processing on exc.
						// setValid(false) and then call
						// FacesContext.renderResponse(...)
						// update model phase shouldn't start
						if (log.isDebugEnabled()) {
							log.debug(
									" currentDate convertion fails with following exception: "
											+ e.toString(), e);
						}
						setValid(false);
						String messageString = e.toString();
						e.printStackTrace();
						FacesMessage message = new FacesMessage(messageString);
						message.setSeverity(FacesMessage.SEVERITY_ERROR);
						facesContext.addMessage(getClientId(facesContext),
								message);
						facesContext.renderResponse();
					}

				} else {
					Date currentDate1 = dateChangeEvent.getCurrentDate();
					Date currentDate2 = getAsDate(getCurrentDate());
					if (!currentDate1.equals(currentDate2)) {
						updateCurrentDate(facesContext, currentDate1);
						ValueChangeEvent changeEvent = new ValueChangeEvent(
								this, currentDate2, currentDate1);
						changeEvent.queue();

					}

				}
			} else {
				super.broadcast(event);
			}

		}

	}

	public Object getPreload() {
		Date[] preloadDateRange = getPreloadDateRange();
		if (preloadDateRange != null && preloadDateRange.length != 0) {
			CalendarDataModel calendarDataModel = (CalendarDataModel) getDataModel();
			if (calendarDataModel != null) {
				CalendarDataModelItem[] calendarDataModelItems = calendarDataModel
						.getData(preloadDateRange);

				HashMap args = new HashMap();
				args.put("startDate", CalendarRendererBase
						.formatDate(preloadDateRange[0]));
				args.put("days", calendarDataModelItems);
				return args;
			}
		}

		return null;
	}

	public Date[] getPreloadDateRange() {
		Date dateRangeBegin = getAsDate(this.getPreloadDateRangeBegin());
		Date dateRangeEnd = getAsDate(this.getPreloadDateRangeEnd());

		if (dateRangeBegin == null && dateRangeEnd == null) {
			return null;
		} else {
			if (dateRangeBegin.after(dateRangeEnd)) {
				// XXX add message
				FacesMessage message = new FacesMessage(
						"preloadDateRangeBegin is greater than preloadDateRangeEnd");
				message.setSeverity(FacesMessage.SEVERITY_ERROR);
				FacesContext context = FacesContext.getCurrentInstance();
				context.addMessage(getClientId(context), message);
				throw new IllegalArgumentException();
			}

			List dates = new ArrayList();

			Calendar calendar = Calendar.getInstance(this.getTimeZone(), getAsLocale(this.getLocale()));
			Calendar calendar2 = (Calendar) calendar.clone();
			calendar.setTime(dateRangeBegin);
			calendar2.setTime(dateRangeEnd);

			do {
				dates.add(calendar.getTime());
				calendar.add(Calendar.DATE, 1);
			} while (!calendar.after(calendar2));

			return (Date[]) dates.toArray(new Date[dates.size()]);
		}
	}

	public void addCurrentDateChangeListener(CurrentDateChangeListener listener) {
		addFacesListener(listener);
	}

	public CurrentDateChangeListener[] getCurrentDateChangeListeners() {
		return (CurrentDateChangeListener[]) getFacesListeners(CurrentDateChangeListener.class);
	}

	public void removeCurrentDateChangeListener(
			CurrentDateChangeListener listener) {
		removeFacesListener(listener);
	}

	 /**
	  *Parse Locale from String.
	  *String must be represented as Locale.toString(); xx_XX_XXXX
     */
	
	public Locale parseLocale(String localeStr){
		
		int length = localeStr.length();
		if(null==localeStr||length<2){
			return Locale.getDefault();
		}
		
		//Lookup index of first '_' in string locale representation.
		int index1 = localeStr.indexOf("_");
		//Get first charters (if exist) from string
		String language = null; 
		if(index1!=-1){
			language = localeStr.substring(0, index1);
		}else{
			return new Locale(localeStr);
		}
		//Lookup index of second '_' in string locale representation.
		int index2 = localeStr.indexOf("_", index1+1);
		String country = null;
		if(index2!=-1){
			country = localeStr.substring(index1+1, index2);
			String variant = localeStr.substring(index2+1);
			return new Locale(language, country, variant);
		}else{
			country = localeStr.substring(index1+1);
			return new Locale(language, country);
		}		
	}
	
	public Locale getAsLocale(Object locale) {

		if (locale instanceof Locale) {

			return (Locale) locale;

		} else if (locale instanceof String) {

			return parseLocale((String) locale);

		} else {

			FacesContext context = FacesContext.getCurrentInstance();
			Application application = context.getApplication();
			Converter converter = application
					.createConverter(locale.getClass());
			if (null != converter) {
				return parseLocale(converter.getAsString(context, this, locale));
			} else {
				throw new FacesException(
						"Wrong locale attibute type or there is no converter for custom attibute type");
			}
		}
	}
	
	protected int getDefaultFirstWeekDay() {
		Calendar cal = getCalendar();
		return cal.getFirstDayOfWeek() - cal.getActualMinimum(Calendar.DAY_OF_WEEK);
	}
	
	protected int getDefaultMinDaysInFirstWeek() {
		return getCalendar().getMinimalDaysInFirstWeek();
	}

	/**
	 * Gets what the minimal days required in the first week of the year 
			are; e.g., if the first week is defined as one that contains the first 
			day of the first month of a year, this method returns 1. If the 
			minimal days required must be a full week, this method returns 7.
	 * Setter for minDaysInFirstWeek
	 * @param minDaysInFirstWeek - new value
	 */
	public void setMinDaysInFirstWeek( int  __minDaysInFirstWeek ){
		this._minDaysInFirstWeek = __minDaysInFirstWeek;
		this._minDaysInFirstWeekSet = true;
	}


	/**
	 * Gets what the minimal days required in the first week of the year 
			are; e.g., if the first week is defined as one that contains the first 
			day of the first month of a year, this method returns 1. If the 
			minimal days required must be a full week, this method returns 7.
	 * Getter for minDaysInFirstWeek
	 * @return minDaysInFirstWeek value from local variable or value bindings
	 */
	public int getMinDaysInFirstWeek(  ){
		if(this._minDaysInFirstWeekSet){
			return this._minDaysInFirstWeek;
		}
		ValueBinding vb = getValueBinding("minDaysInFirstWeek");
		if (vb != null) {
			Integer value = (Integer) vb.getValue(getFacesContext());
			if (null == value) {
				return getDefaultMinDaysInFirstWeek();
			}
			return (value.intValue());
		} else {
			return getDefaultMinDaysInFirstWeek();
		}
	}
	/**
	 * Gets what the first day of the week is; e.g., SUNDAY in the U.S., MONDAY in France.
	 * Setter for firstWeekDay
	 * @param firstWeekDay - new value
	 */
	public void setFirstWeekDay( int  __firstWeekDay ){
		this._firstWeekDay = __firstWeekDay;
		this._firstWeekDaySet = true;
	}


	/**
	 * Gets what the first day of the week is; e.g., SUNDAY in the U.S., MONDAY in France.
	 * Getter for firstWeekDay
	 * @return firstWeekDay value from local variable or value bindings
	 */
	public int getFirstWeekDay(  ){
		if(this._firstWeekDaySet){
			return this._firstWeekDay;
		}
		ValueBinding vb = getValueBinding("firstWeekDay");
		if (vb != null) {
			Integer value = (Integer) vb.getValue(getFacesContext());
			if (null == value) {
				return getDefaultFirstWeekDay();
			}
			return (value.intValue());
		} else {
			return getDefaultFirstWeekDay();
		}
	}
	
	public Object saveState(FacesContext context) {
		return new Object [] {
			super.saveState(context),
			
			new Integer(_firstWeekDay),
			new Boolean(_firstWeekDaySet),

			new Integer(_minDaysInFirstWeek),
			new Boolean(_minDaysInFirstWeekSet)
		};
	}

	public void restoreState(FacesContext context, Object state) {
		Object[] states = (Object[]) state;
		super.restoreState(context, states[0]);
		
		_firstWeekDay = ((Integer)states[1]).intValue();
		_firstWeekDaySet = ((Boolean)states[2]).booleanValue();	

		_minDaysInFirstWeek = ((Integer)states[3]).intValue();
		_minDaysInFirstWeekSet = ((Boolean)states[4]).booleanValue();	
	}
}
