/**
 * 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.renderkit;

import java.io.IOException;
import java.text.DateFormatSymbols;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.ConverterException;
import javax.faces.convert.DateTimeConverter;
import javax.faces.event.PhaseId;

import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.event.AjaxEvent;
import org.ajax4jsf.javascript.JSFunction;
import org.ajax4jsf.javascript.JSFunctionDefinition;
import org.ajax4jsf.javascript.JSReference;
import org.ajax4jsf.javascript.ScriptUtils;
import org.ajax4jsf.renderkit.AjaxRendererUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.richfaces.component.UICalendar;
import org.richfaces.component.util.ComponentUtil;
import org.richfaces.event.CurrentDateChangeEvent;

/**
 * @author Nick Belaevski - mailto:nbelaevski@exadel.com created 08.06.2007
 * 
 */
public class CalendarRendererBase extends TemplateEncoderRendererBase {

	protected static final String MONTH_LABELS_SHORT = "monthLabelsShort";

	protected static final String MONTH_LABELS = "monthLabels";

	protected static final String WEEK_DAY_LABELS_SHORT = "weekDayLabelsShort";

	protected static final String WEEK_DAY_LABELS = "weekDayLabels";

	/**
	 * The constant used to resolve id of hidden input placed on the page
	 * for storing current date in "MM/yyyy" format.
	 * Actual id of hidden input used on the page is #{clientId}InputCurrentDate
	 */
	public static final String CURRENT_DATE_INPUT = "InputCurrentDate";

	protected static final String MARKUP_SUFFIX = "Markup";

	public static final String CALENDAR_BUNDLE = "org.richfaces.renderkit.calendar";

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.ajax4jsf.framework.renderer.RendererBase#getComponentClass()
	 */
	protected Class getComponentClass() {
		return UICalendar.class;
	}

	public void addPopupToAjaxRendered(FacesContext context,
			UICalendar component) {

		AjaxContext ajaxContext = AjaxContext.getCurrentInstance(context);
		Set ajaxRenderedAreas = ajaxContext.getAjaxRenderedAreas();
		String clientId = component.getClientId(context);

		if (ajaxContext.isAjaxRequest() && ajaxRenderedAreas.contains(clientId)) {
			ajaxRenderedAreas.add(clientId + "Popup");

			ajaxRenderedAreas.add(clientId + "IFrame");

			ajaxRenderedAreas.add(clientId + "Script");
		}
	}

	public Object getConvertedValue(FacesContext context,
			UIComponent component, Object submittedValue)
			throws ConverterException {
		if (submittedValue instanceof Date) {
			return (Date) submittedValue;
		}
		UICalendar calendar = (UICalendar) component;
		if (calendar.getConverter() != null) {

			return calendar.getConverter().getAsObject(context, component,
					(String) submittedValue);

		} else {
			DateTimeConverter converter = new DateTimeConverter();
			converter.setPattern(calendar.getDatePattern());
			converter.setLocale(calendar.getAsLocale(calendar.getLocale()));
			converter.setTimeZone(calendar.getTimeZone());
			return converter.getAsObject(context, component,
					(String) submittedValue);
		}
	}

	protected void doDecode(FacesContext context, UIComponent component) {
		// TODO Auto-generated method stub
		super.doDecode(context, component);

		String clientId = component.getClientId(context);

		Map<String, String> requestParameterMap = context.getExternalContext()
				.getRequestParameterMap();

		String currentDateString = (String) requestParameterMap.get(clientId + CURRENT_DATE_INPUT);

		if (currentDateString != null) {
			CurrentDateChangeEvent ev = new CurrentDateChangeEvent(component,
					currentDateString);
			ev.setPhaseId(PhaseId.PROCESS_VALIDATIONS);
			ev.queue();

			// TODO nick - nick - queue this event when ValueChangeEvent is
			// queued?
			new AjaxEvent(component).queue();

		}

		String selectedDateString = (String) requestParameterMap.get(clientId
				+ "InputDate");
		if (selectedDateString != null) {
		    	((UICalendar) component).setSubmittedValue(selectedDateString);
		}
	}

	public void encodeChildren(FacesContext context, UIComponent calendar)
			throws IOException {

	}

	public void writeClass(FacesContext context, UIComponent component)
			throws IOException {

		UICalendar calendar = (UICalendar) component;
		String styleClass = (String) calendar.getAttributes().get("styleClass");
		if (styleClass != null && styleClass.length() != 0) {
			ResponseWriter writer = context.getResponseWriter();
			writer.writeText(",\n className: '" + styleClass + "'", null);
		}
	}

	public void writeDayStyleClass(FacesContext context, UIComponent component)
			throws IOException {

		UICalendar calendar = (UICalendar) component;
		String dayStyleClass = (String) calendar.getAttributes().get(
				"dayStyleClass");
		if (dayStyleClass != null && dayStyleClass.length() != 0) {
			ResponseWriter writer = context.getResponseWriter();
			writer.writeText(",\n dayStyleClass: " + dayStyleClass, null);
		}

	}

	public void writeIsDayEnabled(FacesContext context, UIComponent component)
			throws IOException {
		UICalendar calendar = (UICalendar) component;
		String isDayEnabled = (String) calendar.getAttributes().get(
				"isDayEnabled");
		if (isDayEnabled != null && isDayEnabled.length() != 0) {
			ResponseWriter writer = context.getResponseWriter();
			writer.writeText(",\n isDayEnabled: " + isDayEnabled, null);
		}
	}

	public void writeMarkupScriptBody(FacesContext context,
			UIComponent component, boolean children) throws IOException {
		writeScriptBody(context, component, children);
	}

	public void writeOptionalFacetMarkupScriptBody(FacesContext context,
			UIComponent component, String facetName) throws IOException {

		UIComponent facet = component.getFacet(facetName);
		if (facet != null && facet.isRendered()) {
			ResponseWriter writer = context.getResponseWriter();
			writer.writeText(",\n " + facetName + MARKUP_SUFFIX + ": ", null);
			writeMarkupScriptBody(context, facet, false);
		}
	}

	public void dayCellClass(FacesContext context, UIComponent component)
			throws IOException {
		// if cellWidth/Height is set send dayCellClass to script
		String cellwidth = (String) component.getAttributes().get("cellWidth");
		String cellheight = (String) component.getAttributes()
				.get("cellHeight");
		ResponseWriter writer = context.getResponseWriter();
		String clientId = component.getClientId(context);
		String divStyle = "";
		if (cellwidth != null && cellwidth.length() != 0) {
			if (cellwidth.contains("px") || cellwidth.contains("%")) {
				divStyle = divStyle + "width:" + cellwidth + ";";
			} else {
				divStyle = divStyle + "width:" + cellwidth + "px;";
			}
		}
		if (cellheight != null && cellheight.length() != 0) {
			if (cellheight.contains("px") || cellheight.contains("%")) {
				divStyle = divStyle + "height:" + cellheight.toString() + ";";
			} else {
				divStyle = divStyle + "height:" + cellheight.toString() + "px;";
			}
		}

		if (divStyle.length() != 0) {
			writer.startElement("style", component);
			getUtils().writeAttribute(writer, "type", "text/css");
			writer.writeText("." + clientId.replace(':', '_') + "DayCell{"
					+ divStyle + "}", null);
			writer.endElement("style");
		}
	}

	public void writeDayCellClass(FacesContext context, UIComponent component)
			throws IOException {

		String cellwidth = (String) component.getAttributes().get("cellWidth");
		String cellheight = (String) component.getAttributes()
				.get("cellHeight");
		ResponseWriter writer = context.getResponseWriter();
		if (cellwidth != null && cellwidth.length() != 0 || cellheight != null
				&& cellheight.length() != 0) {
			String clientId = component.getClientId(context);
			writer.writeText(",\n dayCellClass: '" + clientId.replace(':', '_')
					+ "DayCell'", null);
		}
	}

	public void writeFacetMarkupScriptBody(FacesContext context,
			UIComponent component, String facetName) throws IOException {

		UIComponent facet = component.getFacet(facetName);
		if (facet != null && facet.isRendered()) {
			ResponseWriter writer = context.getResponseWriter();
			writer.writeText(",\n " + facetName + MARKUP_SUFFIX + ": ", null);
			writeMarkupScriptBody(context, facet, false);
		}
	}

	public void writePreloadBody(FacesContext context, UICalendar calendar)
			throws IOException {
		Object preload = calendar.getPreload();
		if (preload != null) {
			ResponseWriter writer = context.getResponseWriter();
			writer.write(ScriptUtils.toScript(preload));
		}
	}

	public void writeSubmitFunction(FacesContext context, UICalendar calendar)
			throws IOException {
		ResponseWriter writer = context.getResponseWriter();

		String clientId = calendar.getClientId(context);

		JSFunction ajaxFunction = AjaxRendererUtils.buildAjaxFunction(calendar,
				context, AjaxRendererUtils.AJAX_FUNCTION_NAME);
		ajaxFunction.addParameter(JSReference.NULL);
		Map options = AjaxRendererUtils.buildEventOptions(context, calendar);
		options.put("calendar", JSReference.THIS);
		boolean isSingle = ((Boolean) calendar.getAttributes()
				.get("ajaxSingle")).booleanValue();
		if (isSingle) {
			options.put("single", JSReference.TRUE);
		}

		String oncomplete = AjaxRendererUtils.getAjaxOncomplete(calendar);
		JSFunctionDefinition oncompleteDefinition = new JSFunctionDefinition();
		oncompleteDefinition.addParameter("request");
		oncompleteDefinition.addParameter("event");
		oncompleteDefinition.addParameter("data");
		oncompleteDefinition.addToBody("this.calendar.load(data, true);");
		if (oncomplete != null) {
			oncompleteDefinition.addToBody(oncomplete);
		}

		options.put("oncomplete", oncompleteDefinition);
		JSReference requestValue = new JSReference("requestValue");
		ajaxFunction.addParameter(options);
		JSFunctionDefinition definition = new JSFunctionDefinition();
		definition.addParameter(requestValue);
		definition.addToBody(ajaxFunction);
		writer.write(definition.toScript());
	}

	public void writeEventHandlerFunction(FacesContext context,
			UIComponent component, String eventName) throws IOException {

		ResponseWriter writer = context.getResponseWriter();
		Object script = component.getAttributes().get(eventName);
		if (script != null && !script.equals("")) {
			JSFunctionDefinition onEventDefinition = new JSFunctionDefinition();
			onEventDefinition.addParameter("event");
			onEventDefinition.addToBody(script);
			writer.writeText(",\n" + eventName + ": "
					+ onEventDefinition.toScript(), null);
		}
	}

	public String getInputValue(FacesContext context, UIComponent component) {
		UICalendar input = (UICalendar) component;
		// XXX nick - nick - can contain either Date or String instance
		// Fix for myFaces 1.1.x RF-997
		Date value = null;
		try {
			value = input.getAsDate(input.getSubmittedValue());
		} catch (Exception e) {
			if (log.isDebugEnabled()) {
				log.debug(" InputValue: " + e.toString(), e);
			}
			value = null;
		}
		Date curVal = input.getAsDate(input.getValue());

		DateTimeConverter converter = new DateTimeConverter();
		converter.setPattern(input.getDatePattern());
		Object locale = input.getLocale();
		converter.setLocale(input.getAsLocale(locale));
		converter.setTimeZone(input.getTimeZone());
		if (value == null) {
			return converter.getAsString(context, input, curVal);
		} else {
			return converter.getAsString(context, input, value);
		}
	}

	public void writeSymbols(FacesContext facesContext, UICalendar calendar)
			throws IOException {
		ResponseWriter writer = facesContext.getResponseWriter();
		Map symbolsMap = getSymbolsMap(facesContext, calendar);
		Iterator entryIterator = symbolsMap.entrySet().iterator();
		writer.writeText(", \n", null);
		while (entryIterator.hasNext()) {
			Map.Entry entry = (Map.Entry) entryIterator.next();

			writer.writeText(ScriptUtils.toScript(entry.getKey()), null);
			writer.writeText(": ", null);
			writer.writeText(ScriptUtils.toScript(entry.getValue()), null);

			if (entryIterator.hasNext()) {
				writer.writeText(", \n", null);
			}
		}
	}

	private static String[] shiftDates(int minimum, int maximum, String[] labels) {
		if (minimum == 0 && (maximum - minimum == labels.length - 1)) {
			return labels;
		}

		String[] shiftedLabels = new String[maximum - minimum + 1];
		System.arraycopy(labels, minimum, shiftedLabels, 0, maximum - minimum
				+ 1);

		return shiftedLabels;
	}

	protected Map getSymbolsMap(FacesContext facesContext, UICalendar calendar) {
		Map map = new HashMap();

		Locale locale = calendar.getAsLocale(calendar.getLocale());
		Calendar cal = calendar.getCalendar();
		int maximum = cal.getActualMaximum(Calendar.DAY_OF_WEEK);
		int minimum = cal.getActualMinimum(Calendar.DAY_OF_WEEK);

		int monthMax = cal.getActualMaximum(Calendar.MONTH);
		int monthMin = cal.getActualMinimum(Calendar.MONTH);

		DateFormatSymbols symbols = new DateFormatSymbols(locale);
		String[] weekDayLabels = ComponentUtil.asArray(calendar
				.getWeekDayLabels());
		if (weekDayLabels == null) {
			weekDayLabels = symbols.getWeekdays();
			weekDayLabels = shiftDates(minimum, maximum, weekDayLabels);
		}

		String[] weekDayLabelsShort = ComponentUtil.asArray(calendar
				.getWeekDayLabelsShort());
		if (weekDayLabelsShort == null) {
			weekDayLabelsShort = symbols.getShortWeekdays();
			weekDayLabelsShort = shiftDates(minimum, maximum,
					weekDayLabelsShort);
		}

		String[] monthLabels = ComponentUtil.asArray(calendar.getMonthLabels());
		if (monthLabels == null) {
			monthLabels = symbols.getMonths();
			monthLabels = shiftDates(monthMin, monthMax, monthLabels);
		}

		String[] monthLabelsShort = ComponentUtil.asArray(calendar
				.getMonthLabelsShort());
		if (monthLabelsShort == null) {
			monthLabelsShort = symbols.getShortMonths();
			monthLabelsShort = shiftDates(monthMin, monthMax, monthLabelsShort);
		}

		map.put(WEEK_DAY_LABELS, weekDayLabels);
		map.put(WEEK_DAY_LABELS_SHORT, weekDayLabelsShort);
		map.put(MONTH_LABELS, monthLabels);
		map.put(MONTH_LABELS_SHORT, monthLabelsShort);

		return map;
	}

	public String getFirstWeekDay(FacesContext context, UICalendar calendar)
			throws IOException {
		return String.valueOf(calendar.getFirstWeekDay());
	}

	public String getMinDaysInFirstWeek(FacesContext context,
			UICalendar calendar) throws IOException {
		return String.valueOf(calendar.getMinDaysInFirstWeek());
	}

	public String getCurrentDateAsString(FacesContext context,
			UICalendar calendar, Date date) throws IOException {

		Format formatter = new SimpleDateFormat("MM/yyyy");
		return formatter.format(date);
	}

	public String getCurrentDate(FacesContext context, UICalendar calendar,
			Date date) throws IOException {
		return ScriptUtils.toScript(formatDate(date));
	}

	public String getSelectedDate(FacesContext context, UICalendar calendar)
			throws IOException {

		// Fix for myFaces 1.1.x RF-997
		Date date;
		if (calendar.isValid()) {
		    	// Hack. Use converter in "Render response" phase
		    	// in order to have java.util.Date object to highlight
		    	// selected date in Calendar popup. 
			Date submittedValue = null;
			try {
			    submittedValue = calendar.getAsDate(calendar.getSubmittedValue());
			} catch(ConverterException ce) {
			    // ignore ConverterException for the case when the user
			    // has entered invalid date and there was no validation
			    // (and conversion) before, i.e. the form submit has attribute
			    // "immediate" = true; and Calendar has "immediate" = false
			    submittedValue = null;
			}
			if (submittedValue != null) {
				date = submittedValue;
			} else {
				date = calendar.getAsDate(calendar.getValue());
			}
		} else {
			date = calendar.getAsDate(calendar.getValue());
			// Date submittedValue = null;
		}

		if (date != null) {
			return ScriptUtils.toScript(formatSelectedDate(date));
			// XXX KAA Add ServerTime Processing;
		}

		return ScriptUtils.toScript(null);
	}

	public static Object formatDate(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		JSFunction result = new JSFunction("new Date");
		result.addParameter(Integer.valueOf(calendar.get(Calendar.YEAR)));
		result.addParameter(Integer.valueOf(calendar.get(Calendar.MONTH)));
		result.addParameter(Integer.valueOf(calendar.get(Calendar.DATE)));

		return result;
	}

	public static Object formatSelectedDate(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		JSFunction result = new JSFunction("new Date");
		result.addParameter(Integer.valueOf(calendar.get(Calendar.YEAR)));
		result.addParameter(Integer.valueOf(calendar.get(Calendar.MONTH)));
		result.addParameter(Integer.valueOf(calendar.get(Calendar.DATE)));
		result
				.addParameter(Integer.valueOf(calendar
						.get(Calendar.HOUR_OF_DAY)));
		result.addParameter(Integer.valueOf(calendar.get(Calendar.MINUTE)));
		result.addParameter(new Integer(0));
		return result;
	}

	/**
	 * Write labels used in the Calendar component, taken from message bundles.
	 * Try to use bundle1 at first. If the 1st bundle is null or it doesn't
	 * contain requested message key, use the bundle2.
	 * @param bundle1 - 1st bundle to be used as a source for messages
	 * @param bundle2 - 2nd bundle to be used as a source for messages
	 * @param name - name of the requested label
	 * @param writer - response writer
	 * @throws IOException
	 */
	public void writeStringsFromBundle(ResourceBundle bundle1, ResourceBundle bundle2, String name,
		ResponseWriter writer) throws IOException {
	    String label = null;
	    String bundleKey = "RICH_CALENDAR_" + name.toUpperCase() + "_LABEL";
	    
	    if (bundle1 != null) {
		try {
		    label = bundle1.getString(bundleKey);
		} catch (MissingResourceException mre) {
		    // Current key was not found, ignore this exception;
		}
	    }
	    // Current key wasn't found in application bundle, use CALENDAR_BUNDLE,
	    // if it is not null
	    if((label == null) && (bundle2 != null)) {
		try {
		    label = bundle2.getString(bundleKey);
		} catch (MissingResourceException mre) {
		    // Current key was not found, ignore this exception;
		}
	    }
	    
	    writeStringFoundInBundle(name, label, writer);
	}
	
	public void writeStringFoundInBundle(String name, String value, ResponseWriter writer) throws IOException {
		if(null!=value){
			if (!("close").equals(name.toLowerCase())) {
				writer.writeText(name.toLowerCase() + ":'" + value + "', ",null);
			} else {
				writer.writeText("close:'" + value + "'", null);					
			}
		}else{
			if (!("close").equals(name.toLowerCase())) {
				writer.writeText(name.toLowerCase() + ":'" + name + "', ",null);
			} else {
				writer.writeText("close:'x'", null);					
			}
		}
		
	}	

	public void writeLabels(FacesContext context, UICalendar calendar)
			throws IOException {

		ResourceBundle bundle1 = null;
		ResourceBundle bundle2 = null;
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		String messageBundle = context.getApplication().getMessageBundle();
		Object locale = calendar.getLocale();
		if (null != messageBundle) {
			bundle1 = ResourceBundle.getBundle(messageBundle, calendar.getAsLocale(locale), loader);
		} 
		try {
			bundle2 = ResourceBundle.getBundle(CALENDAR_BUNDLE, calendar.getAsLocale(locale), loader);

		} catch (MissingResourceException e) {
				//No external bundle was found, ignore this exception.				
		}
		
		ResponseWriter writer = context.getResponseWriter();		
		writer.writeText(",\n labels:{", null);
		if (null != bundle1 || null != bundle2) {
			writeStringsFromBundle(bundle1, bundle2, "Apply", writer);
			writeStringsFromBundle(bundle1, bundle2, "Today", writer);
			writeStringsFromBundle(bundle1, bundle2, "Clean", writer);
			writeStringsFromBundle(bundle1, bundle2, "Cancel", writer);
			writeStringsFromBundle(bundle1, bundle2, "OK", writer);
			writeStringsFromBundle(bundle1, bundle2, "Close", writer);
		}else{
			// No bundles were found, use default labels.
			writer.writeText("apply:'Apply', today:'Today', clean:'Clean', ok:'OK', cancel:'Cancel', close:'x'", null);			
		}
		writer.writeText("}", null);

	}
}
