/**
 * 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.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.el.ELContext;
import javax.el.MethodExpression;
import javax.el.ValueExpression;
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.event.AbortProcessingException;
import javax.faces.event.FacesEvent;

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;

/**
 * JSF component class
 * 
 */
public abstract class UICalendar extends UIRichInput 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 = 0;		
	/**
	 * 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 = 0;		
	/**
	 * Flag indicated what minDaysInFirstWeek is set.
	 */
	private boolean _minDaysInFirstWeekSet = false;	

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

	public static final String COMPONENT_FAMILY = "org.richfaces.Calendar";
	
	/**
	 * Default pattern for defaultTime
	 */
	private static String DEFAULT_TIME_PATTERN = "HH:mm";
	
	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 getDefaultTime();

	public abstract void setDefaultTime(Object defaultTime);
	
	public abstract boolean isResetTimeOnDateSelect();

	public abstract void setResetTimeOnDateSelect(boolean resetTimeOnDateSelect);

	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 MethodExpression getCurrentDateChangeListener();

	public abstract void setCurrentDateChangeListener(
			MethodExpression scrollerListener);

	public abstract String getMode();

	public abstract void setMode(String mode);

	public abstract int getVerticalOffset();

	public abstract void setVerticalOffset(int verticalOffset);

	public abstract int getFirstWeekDay();

	public abstract void setFirstWeekDay(int firstWeekDay);

	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);
	
	public abstract String getTabindex();

	public abstract void setTabindex(String tabindex);
	

	// 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;
	}

	/**
	 * Returns default value of "defaultTime" attribute
	 * @return default value of "defaultTime" attribute
	 */
	protected Object getDefaultValueOfDefaultTime() {
	    Calendar calendar = getCalendar();
	    calendar.set(Calendar.HOUR_OF_DAY, 12);
	    calendar.set(Calendar.MINUTE, 0);
	    
	    return calendar.getTime();
	}

	/**
	 * Returns default time as a <code>Date</code> value.
	 * Hours and minutes values should be taken from the returned date.
	 * 
	 * @return default time as a <code>Date</code> value
	 */
	public Date getFormattedDefaultTime() {
	    Date result = null;
	    Object defaultTime = getDefaultTime();
	    
	    if (defaultTime instanceof Calendar) {
		result = ((Calendar) defaultTime).getTime();
	    } else if (defaultTime instanceof Date) {
		result = (Date) defaultTime;
	    } else {
		String defaultTimeString = defaultTime.toString();
		String datePattern = getDatePattern();
		
		String timePattern = "\\s*[hHkKma]+[\\W&&\\S]+[hHkKma]+\\s*";
		Pattern pattern = Pattern.compile(timePattern);
		Matcher matcher = pattern.matcher(datePattern);
		
		String subTimePattern = DEFAULT_TIME_PATTERN;
		if(matcher.find()) {
		    subTimePattern = matcher.group().trim();
		}
		
		DateFormat format = new SimpleDateFormat(subTimePattern);

		try {
		    result = format.parse(defaultTimeString);
		} catch (ParseException parseException) {
		    // parse exception
		    result = null;
		}
	    }
	    return result;
	}
	
	public void updateCurrentDate(FacesContext context, Object currentDate) {

		if (context == null) {
			throw new NullPointerException();
		}
		// RF-1073
		try {
			ValueExpression ve = getValueExpression("currentDate");
			if (ve != null) {
			    	ELContext elContext = context.getELContext(); 
			    	if (ve.getType(elContext).equals(String.class)) {
					DateTimeConverter convert = new DateTimeConverter();
					convert.setLocale(getAsLocale(getLocale()));
					convert.setPattern(getDatePattern());
					ve.setValue(context.getELContext(), convert.getAsString(context, this,
							currentDate));
					return;
				} else if (ve.getType(elContext).equals(Calendar.class)) {
					Calendar c = Calendar.getInstance();
					c.setTime((Date) currentDate);
					ve.setValue(elContext, c);
					return;
				} else {
					ve.setValue(elContext, 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 {
					    	
					    	FacesContext context = FacesContext.getCurrentInstance();
					    	Converter converter = getConverter();
					    	
					    	if(converter != null) {
					    	    return getAsDate(converter.getAsString(context, this, date));
					    	}
					    	
						Application application = context.getApplication();
						converter = application.createConverter(date.getClass());
						if (null != converter) {
							return getAsDate(converter.getAsString(context,	this, date));
						} else {
							throw new FacesException("Wrong attibute type or there is no converter for custom 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 = getFacesContext();
				CurrentDateChangeEvent dateChangeEvent = (CurrentDateChangeEvent) event;
				String currentDateString = dateChangeEvent
						.getCurrentDateString();

				// 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 = getAsDate(getCurrentDate());
					Date submittedCurrentDate = convertCurrentDate(currentDateString);
					dateChangeEvent.setCurrentDate(submittedCurrentDate);

					if (!submittedCurrentDate.equals(currentDate)) {
						updateCurrentDate(facesContext, submittedCurrentDate);
						MethodExpression methodExpression = getCurrentDateChangeListener();
						if (methodExpression != null) {
						    methodExpression.invoke(facesContext.getELContext(), new Object[] { dateChangeEvent });
						}
					}
					
				} 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 {
				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 <String, Object> args = new HashMap<String,Object>();
				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 <Date> dates = new ArrayList<Date>();

			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() {
		return getAsLocale(getLocale());
	}
	
	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");
			}
		}
	}

	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();	
	}
}
