/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.extensions.yui.calendar;

import java.text.DateFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.datetime.markup.html.form.DateTextField;
import org.apache.wicket.extensions.yui.YuiLib;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.resource.JavaScriptResourceReference;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.convert.converter.DateConverter;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.template.PackageTextTemplate;
import org.apache.wicket.util.template.TextTemplate;
import org.joda.time.DateTime;

/**
 * Pops up a YUI calendar component so that the user can select a date. On selection, the date is
 * set in the component it is coupled to, after which the popup is closed again. This behavior can
 * only be used with components that either implement {@link ITextFormatProvider} or that use
 * {@link DateConverter} configured with an instance of {@link SimpleDateFormat} (like Wicket's
 * default configuration has).<br/>
 * 
 * To use, simply add a new instance to your component, which would typically a TextField, like
 * {@link DateTextField}.<br/>
 * 
 * The CalendarNavigator can be configured by overriding {@link #configure(java.util.Map, org.apache.wicket.markup.head.IHeaderResponse, java.util.Map)} and setting the
 * property or by returning <code>true</code> for {@link #enableMonthYearSelection()}.
 * 
 * @see <a
 *      href="http://developer.yahoo.com/yui/calendar/">http://developer.yahoo.com/yui/calendar/</a>
 * 
 * @author eelcohillenius
 */
public class DatePicker extends Behavior
{

	/**
	 * Exception thrown when the bound component does not produce a format this date picker can work
	 * with.
	 */
	private static final class UnableToDetermineFormatException extends WicketRuntimeException
	{
		private static final long serialVersionUID = 1L;

		public UnableToDetermineFormatException()
		{
			super("This behavior can only be added to components that either implement " +
				ITextFormatProvider.class.getName() +
				" AND produce a non-null format, or that use" +
				" converters that this DatePicker can use to determine" +
				" the pattern being used. Alternatively, you can extend " +
				" the DatePicker and override getDatePattern to provide your own.");
		}
	}

	/**
	 * Format to be used when configuring YUI calendar. Can be used when using the
	 * &quot;selected&quot; property.
	 */
	// See wicket-1988: SimpleDateFormat is not thread safe. Do not use static final
	// See wicket-2525: SimpleDateFormat consumes a lot of memory
	public static String FORMAT_DATE = "MM/dd/yyyy";

	/**
	 * For specifying which page (month/year) to show in the calendar, use this format for the date.
	 * This is to be used together with the property &quot;pagedate&quot;
	 */
	// See wicket-1988: SimpleDateFormat is not thread safe. Do not use static final
	// See wicket-2525: SimpleDateFormat consumes a lot of memory
	public static String FORMAT_PAGEDATE = "MM/yyyy";

	private static final ResourceReference YUI = new JavaScriptResourceReference(YuiLib.class, "");

	private static final ResourceReference WICKET_DATE = new JavaScriptResourceReference(
		DatePicker.class, "wicket-date.js");

	private static final long serialVersionUID = 1L;

	/** The target component. */
	private Component component;

	private boolean showOnFieldClick = false;

	/**
	 * A setting that decides whether to close the date picker when the user clicks somewhere else
	 * on the document.
	 */
	private boolean autoHide = false;

	/**
	 *  The string to use for the close button label.
	 */
	private String closeLabel = "";

	/**
	 * Construct.
	 */
	public DatePicker()
	{
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void bind(final Component component)
	{
		this.component = component;
		checkComponentProvidesDateFormat(component);
		component.setOutputMarkupId(true);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void afterRender(final Component component)
	{
		super.afterRender(component);

		// Append the span and img icon right after the rendering of the
		// component. Not as pretty as working with a panel etc, but works
		// for behaviors and is more efficient
		Response response = component.getResponse();
		response.write("\n<span class=\"yui-skin-sam\">&nbsp;<span style=\"");

		if (renderOnLoad())
		{
			response.write("display:block;");
		}
		else
		{
			response.write("display:none;");
			response.write("position:absolute;");
		}

		response.write("z-index: 99999;\" id=\"");
		response.write(getEscapedComponentMarkupId());
		response.write("Dp\"></span><img style=\"");
		response.write(getIconStyle());
		response.write("\" id=\"");
		response.write(getIconId());
		response.write("\" src=\"");
		CharSequence iconUrl = getIconUrl();
		response.write(Strings.escapeMarkup(iconUrl != null ? iconUrl.toString() : ""));
		response.write("\" alt=\"");
		CharSequence alt = getIconAltText();
		response.write(Strings.escapeMarkup((alt != null) ? alt.toString() : ""));
		response.write("\" title=\"");
		CharSequence title = getIconTitle();
		response.write(Strings.escapeMarkup((title != null) ? title.toString() : ""));
		response.write("\"/>");

		if (renderOnLoad())
		{
			response.write("<br style=\"clear:left;\"/>");
		}
		response.write("</span>");
	}

	/**
	 * Controls whether or not datepicker will contribute YUI libraries to the page as part of its
	 * rendering lifecycle.
	 * 
	 * There may be cases when the user wants to use their own version of YUI contribution code, in
	 * those cases this method should be overridden to return <code>false</code>.
	 * 
	 * @return a flag whether to contribute YUI libraries to the page. {@code true} by default.
	 */
	protected boolean includeYUILibraries()
	{
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void renderHead(Component component, IHeaderResponse response)
	{
		super.renderHead(component, response);

		if (includeYUILibraries())
		{
			YuiLib.load(response);
		}

		renderHeadInit(response);

		// variables for the initialization script
		Map<String, Object> variables = new HashMap<>();
		String widgetId = getEscapedComponentMarkupId();
		variables.put("componentId", getComponentMarkupId());
		variables.put("widgetId", widgetId);
		variables.put("datePattern", getDatePattern());
		variables.put("fireChangeEvent", notifyComponentOnDateSelected());
		variables.put("alignWithIcon", alignWithIcon());
		variables.put("hideOnSelect", hideOnSelect());
		variables.put("showOnFieldClick", showOnFieldClick());
		variables.put("autoHide", autoHide());
		variables.put("closeLabel", closeLabel());

		String script = getAdditionalJavaScript();
		if (script != null)
		{
			variables.put("additionalJavascript",
				Strings.replaceAll(script, "${calendar}", "YAHOO.wicket." + widgetId + "DpJs"));
		}

		// print out the initialization properties
		Map<String, Object> p = new LinkedHashMap<>();
		configure(p, response, variables);
		if (!p.containsKey("navigator") && enableMonthYearSelection())
		{
			p.put("navigator", Boolean.TRUE);
		}

		if (enableMonthYearSelection() && p.containsKey("pages") &&
			Objects.longValue(p.get("pages")) > 1)
		{
			throw new IllegalStateException(
				"You cannot use a CalendarGroup with month/year selection!");
		}

		// ${calendarInit}
		StringBuilder calendarInit = new StringBuilder();
		appendMapping(p, calendarInit);
		variables.put("calendarInit", calendarInit.toString());

		// render initialization script with the variables interpolated
		TextTemplate datePickerJs = new PackageTextTemplate(DatePicker.class, "DatePicker.js");
		datePickerJs.interpolate(variables);
		response.render(OnDomReadyHeaderItem.forScript(datePickerJs.asString()));

		// remove previously generated markup (see onRendered) via javascript in
		// ajax requests to not render the yui calendar multiple times
		IPartialPageRequestHandler target = component.getRequestCycle().find(IPartialPageRequestHandler.class);
		if (target != null)
		{
			String escapedComponentMarkupId = getEscapedComponentMarkupId();
			String javascript = "var e = Wicket.$('" + escapedComponentMarkupId + "Dp" +
				"'); if (e != null && typeof(e.parentNode) != 'undefined' && " +
				"typeof(e.parentNode.parentNode != 'undefined')) {" +
				"e.parentNode.parentNode.removeChild(e.parentNode);" + "YAHOO.wicket." +
				escapedComponentMarkupId + "DpJs.destroy(); delete YAHOO.wicket." +
				escapedComponentMarkupId + "DpJs;}";

			target.prependJavaScript(javascript);
		}
	}

	/**
	 * Renders yui & wicket calendar js module loading. It is done only once per page.
	 * 
	 * @param response
	 *            header response
	 */
	protected void renderHeadInit(IHeaderResponse response)
	{
		String key = "DatePickerInit.js";
		if (response.wasRendered(key))
		{
			return;
		}

		// variables for YUILoader
		Map<String, Object> variables = new HashMap<>();
		variables.put("basePath",
			Strings.stripJSessionId(RequestCycle.get().urlFor(YUI, null).toString()) + "/");
		variables.put("Wicket.DateTimeInit.DatePath", RequestCycle.get().urlFor(WICKET_DATE, null));

		if (Application.get().usesDevelopmentConfig())
		{
			variables.put("filter", "filter: \"RAW\",");
			variables.put("allowRollup", false);
		}
		else
		{
			variables.put("filter", "");
			variables.put("allowRollup", true);
		}

		TextTemplate template = new PackageTextTemplate(DatePicker.class, key);
		response.render(OnDomReadyHeaderItem.forScript(template.asString(variables)));

		response.markRendered(key);
	}

	/**
	 * Check that this behavior can get a date format out of the component it is coupled to. It
	 * checks whether {@link #getDatePattern()} produces a non-null value. If that method returns
	 * null, and exception will be thrown
	 * 
	 * @param component
	 *            the component this behavior is being coupled to
	 * @throws UnableToDetermineFormatException
	 *             if this date picker is unable to determine a format.
	 */
	private void checkComponentProvidesDateFormat(final Component component)
	{
		if (getDatePattern() == null)
		{
			throw new UnableToDetermineFormatException();
		}
	}

	/**
	 * Set widget property if the array is null and has a length greater than 0.
	 * 
	 * @param widgetProperties
	 * @param key
	 * @param array
	 */
	private void setWidgetProperty(final Map<String, Object> widgetProperties, final String key,
		final String[] array)
	{
		if (array != null && array.length > 0)
		{
			widgetProperties.put(key, array);
		}
	}

	/**
	 * Whether to position the date picker relative to the trigger icon.
	 * 
	 * @return If true, the date picker is aligned with the left position of the icon, and with the
	 *         top right under. If false, the date picker will skip positioning and will let you do
	 *         the positioning yourself. Returns true by default.
	 */
	protected boolean alignWithIcon()
	{
		return true;
	}

	/**
	 * Gives overriding classes the option of adding (or even changing/ removing) configuration
	 * properties for the javascript widget. See <a
	 * href="http://developer.yahoo.com/yui/calendar/">the widget's documentation</a> for the
	 * available options. If you want to override/ remove properties, you should call
	 * super.configure(properties) first. If you don't call that, be aware that you will have to
	 * call {@link #localize(java.util.Map, org.apache.wicket.markup.head.IHeaderResponse, java.util.Map)} manually if you like localized strings to be added.
	 * 
	 * @param widgetProperties
	 *            the current widget properties
	 * @param response
	 *            the header response
	 * @param initVariables
	 *            variables passed to the Wicket.DateTime.init() js method
	 */
	protected void configure(final Map<String, Object> widgetProperties,
		final IHeaderResponse response, final Map<String, Object> initVariables)
	{
		widgetProperties.put("close", true);

		// localize date fields
		localize(widgetProperties, response, initVariables);

		Object modelObject = component.getDefaultModelObject();
		// null and cast check
		if (modelObject instanceof Date)
		{
			Date date = (Date)modelObject;
			widgetProperties.put("selected", new SimpleDateFormat(FORMAT_DATE).format(date));
			widgetProperties.put("pagedate", new SimpleDateFormat(FORMAT_PAGEDATE).format(date));
		}
	}

	/**
	 * Filter all empty elements (workaround for {@link DateFormatSymbols} returning arrays with
	 * empty elements).
	 * 
	 * @param stringArray
	 *            array to filter
	 * @return filtered array (without null or empty string elements)
	 */
	protected final String[] filterEmpty(String[] stringArray)
	{
		if (stringArray == null)
		{
			return null;
		}

		List<String> list = new ArrayList<>(stringArray.length);
		for (String string : stringArray)
		{
			if (!Strings.isEmpty(string))
			{
				list.add(string);
			}
		}
		return list.toArray(new String[list.size()]);
	}

	/**
	 * Gets the id of the component that the calendar widget will get attached to.
	 * 
	 * @return The DOM id of the component
	 */
	protected final String getComponentMarkupId()
	{
		return component.getMarkupId();
	}

	/**
	 * Gets the date pattern to use for putting selected values in the coupled component.
	 * 
	 * @return The date pattern
	 */
	protected String getDatePattern()
	{
		String format = null;
		if (component instanceof ITextFormatProvider)
		{
			format = ((ITextFormatProvider)component).getTextFormat();
			// it is possible that components implement ITextFormatProvider but
			// don't provide a format
		}

		if (format == null)
		{
			IConverter<?> converter = component.getConverter(DateTime.class);
			if (!(converter instanceof DateConverter))
			{
				converter = component.getConverter(Date.class);
			}
			format = ((SimpleDateFormat)((DateConverter)converter).getDateFormat(component.getLocale())).toPattern();
		}

		return format;
	}

	/**
	 * Gets the escaped DOM id that the calendar widget will get attached to. All non word
	 * characters (\W) will be removed from the string.
	 * 
	 * @return The DOM id of the calendar widget - same as the component's markup id + 'Dp'}
	 */
	protected final String getEscapedComponentMarkupId()
	{
		return component.getMarkupId().replaceAll("\\W", "");
	}

	/**
	 * Gets the id of the icon that triggers the popup.
	 * 
	 * @return The id of the icon
	 */
	protected final String getIconId()
	{
		return getEscapedComponentMarkupId() + "Icon";
	}

	/**
	 * Gets the style of the icon that triggers the popup.
	 * 
	 * @return The style of the icon, e.g. 'cursor: point' etc.
	 */
	protected String getIconStyle()
	{
		return "cursor: pointer; border: none;";
	}

	/**
	 * Gets the title attribute of the datepicker icon
	 * 
	 * @return text
	 */
	protected CharSequence getIconTitle()
	{
		return "";
	}

	/**
	 * Gets the icon alt text for the datepicker icon
	 * 
	 * @return text
	 */
	protected CharSequence getIconAltText()
	{
		return "";
	}

	/**
	 * Gets the url for the popup button. Users can override to provide their own icon URL.
	 * 
	 * @return the url to use for the popup button/ icon
	 */
	protected CharSequence getIconUrl()
	{
		return RequestCycle.get().urlFor(
			new ResourceReferenceRequestHandler(new PackageResourceReference(DatePicker.class,
				"icon1.gif")));
	}

	/**
	 * Gets the locale that should be used to configure this widget.
	 * 
	 * @return By default the locale of the bound component.
	 */
	protected Locale getLocale()
	{
		return component.getLocale();
	}

	/**
	 * Configure the localized strings for the datepicker widget. This implementation uses
	 * {@link DateFormatSymbols} and some slight string manipulation to get the strings for months
	 * and week days. Also, the first week day is set according to the {@link Locale} returned by
	 * {@link #getLocale()}. It should work well for most locales.
	 * <p>
	 * This method is called from {@link #configure(java.util.Map, org.apache.wicket.markup.head.IHeaderResponse, java.util.Map)} and can be overridden if
	 * you want to customize setting up the localized strings but are happy with the rest of
	 * {@link #configure(java.util.Map, org.apache.wicket.markup.head.IHeaderResponse, java.util.Map)}'s behavior. Note that you can call (overridable)
	 * method {@link #getLocale()} to get the locale that should be used for setting up the widget.
	 * </p>
	 * <p>
	 * See YUI Calendar's <a href="http://developer.yahoo.com/yui/examples/calendar/germany/1.html">
	 * German</a> and <a
	 * href="http://developer.yahoo.com/yui/examples/calendar/japan/1.html">Japanese</a> examples
	 * for more info.
	 * </p>
	 * 
	 * @param widgetProperties
	 *            the current widget properties
	 * @param response
	 *            the header response
	 * @param initVariables
	 *            variables passed to the Wicket.DateTime.init() js method
	 */
	protected void localize(Map<String, Object> widgetProperties, IHeaderResponse response,
		Map<String, Object> initVariables)
	{
		Locale locale = getLocale();
		String key = "Wicket.DateTimeInit.CalendarI18n[\"" + locale.toString() + "\"]";
		initVariables.put("i18n", key);

		if (response.wasRendered(key))
		{
			return;
		}

		DateFormatSymbols dfSymbols = DateFormatSymbols.getInstance(locale);
		if (dfSymbols == null)
		{
			dfSymbols = new DateFormatSymbols(locale);
		}

		Map<String, Object> i18nVariables = new LinkedHashMap<>();
		setWidgetProperty(i18nVariables, "MONTHS_SHORT", filterEmpty(dfSymbols.getShortMonths()));
		setWidgetProperty(i18nVariables, "MONTHS_LONG", filterEmpty(dfSymbols.getMonths()));
		setWidgetProperty(i18nVariables, "WEEKDAYS_MEDIUM",
			filterEmpty(dfSymbols.getShortWeekdays()));
		setWidgetProperty(i18nVariables, "WEEKDAYS_LONG", filterEmpty(dfSymbols.getWeekdays()));

		i18nVariables.put("START_WEEKDAY", getFirstDayOfWeek(locale));

		if (Locale.SIMPLIFIED_CHINESE.equals(locale) || Locale.TRADITIONAL_CHINESE.equals(locale))
		{
			setWidgetProperty(i18nVariables, "WEEKDAYS_1CHAR",
				filterEmpty(substring(dfSymbols.getShortWeekdays(), 2, 1)));
			i18nVariables.put("WEEKDAYS_SHORT",
				filterEmpty(substring(dfSymbols.getShortWeekdays(), 2, 1)));
		}
		else
		{
			setWidgetProperty(i18nVariables, "WEEKDAYS_1CHAR",
				filterEmpty(substring(dfSymbols.getShortWeekdays(), 0, 1)));
			setWidgetProperty(i18nVariables, "WEEKDAYS_SHORT",
				filterEmpty(substring(dfSymbols.getShortWeekdays(), 0, 2)));
		}

		StringBuilder i18n = new StringBuilder(key);
		i18n.append('=');
		appendMapping(i18nVariables, i18n);
		i18n.append(';');

		response.render(OnDomReadyHeaderItem.forScript(i18n.toString()));

		response.wasRendered(key);
	}

	/**
	  * Gets the first day of week of a given locale.
	  *
	  * @return By default the first day of week accordingly to Calendar class.
	  */
	protected int getFirstDayOfWeek(Locale locale)
	{
		return Calendar.getInstance(locale).getFirstDayOfWeek() - 1;
	}

	/**
	 * Whether to notify the associated component when a date is selected. Notifying is done by
	 * calling the associated component's onchange JavaScript event handler. You can for instance
	 * attach an {@link AjaxEventBehavior} to that component to get a call back to the server. The
	 * default is true.
	 * 
	 * @return if true, notifies the associated component when a date is selected
	 */
	protected boolean notifyComponentOnDateSelected()
	{
		return true;
	}

	/**
	 * Makes a copy of the provided array and for each element copy the substring 0..len to the new
	 * array
	 * 
	 * @param array
	 *            array to copy from
	 * @param len
	 *            size of substring for each element to copy
	 * @return copy of the array filled with substrings.
	 */
	protected final String[] substring(final String[] array, final int len)
	{
		return substring(array, 0, len);
	}

	/**
	 * Makes a copy of the provided array and for each element copy the substring 0..len to the new
	 * array
	 * 
	 * @param array
	 *            array to copy from
	 * @param start
	 *            start position of the substring
	 * @param len
	 *            size of substring for each element to copy
	 * @return copy of the array filled with substrings.
	 */
	protected final String[] substring(final String[] array, final int start, final int len)
	{
		if (array != null)
		{
			String[] copy = new String[array.length];
			for (int i = 0; i < array.length; i++)
			{
				String el = array[i];
				if (el != null)
				{
					if (el.length() > (start + len))
					{
						copy[i] = el.substring(start, start + len);
					}
					else
					{
						copy[i] = el;
					}
				}
			}
			return copy;
		}
		return null;
	}

	/**
	 * Indicates whether plain text is rendered or two select boxes are used to allow direct
	 * selection of month and year.
	 * 
	 * @return <code>true</code> if select boxes should be rendered to allow month and year
	 *         selection.<br/>
	 *         <code>false</code> to render just plain text.
	 */
	protected boolean enableMonthYearSelection()
	{
		return false;
	}

	/**
	 * Indicates whether the calendar should be hidden after a date was selected.
	 * 
	 * @return <code>true</code> (default) if the calendar should be hidden after the date selection <br/>
	 *         <code>false</code> if the calendar should remain visible after the date selection.
	 */
	protected boolean hideOnSelect()
	{
		return true;
	}

	/**
	 * Indicates whether the calendar should be shown when corresponding text input is clicked.
	 * 
	 * @return <code>true</code> <br/>
	 *         <code>false</code> (default)
	 */
	protected boolean showOnFieldClick()
	{
		return showOnFieldClick;
	}

	/**
	 * @param show
	 *            a flag indicating whether to show the picker on click event
	 * @return {@code this} instance to be able to chain calls
	 * @see {@link #showOnFieldClick()}
	 */
	public DatePicker setShowOnFieldClick(boolean show)
	{
		showOnFieldClick = show;
		return this;
	}


	/**
	 * Indicates whether the calendar should be hidden when the user clicks on an area of the
	 * document outside of the dialog.
	 * 
	 * @return <code>true</code> <br/>
	 *         <code>false</code> (default)
	 */
	protected boolean autoHide()
	{
		return autoHide;
	}

	/**
	 * @param autoHide
	 *            a flag indicating whether to hide the picker on click event
	 * @return {@code this} instance to be able to chain calls
	 * @see {@link #autoHide()}
	 */
	public DatePicker setAutoHide(boolean autoHide)
	{
		this.autoHide = autoHide;
		return this;
	}

	/**
	 * The string to use for the close button label.
	 *
	 * @return label
	 */
	protected String closeLabel()
	{
		return closeLabel;
	}

	/**
	 * @param closeLabel
	 *            The string to use for the close button label.
	 */
	public void setCloseLabel(String closeLabel)
	{
		this.closeLabel = closeLabel;
	}

	/**
	 * Indicates whether the calendar should be rendered after it has been loaded.
	 * 
	 * @return <code>true</code> if the calendar should be rendered after it has been loaded.<br/>
	 *         <code>false</code> (default) if it's initially hidden.
	 */
	protected boolean renderOnLoad()
	{
		return false;
	}

	/**
	 * Override this method to further customize the YUI Calendar with additional JavaScript code.
	 * The code returned by this method is executed right after the Calendar has been constructed
	 * and initialized. To refer to the actual Calendar DOM object, use <code>${calendar}</code> in
	 * your code.<br/>
	 * See <a href="http://developer.yahoo.com/yui/calendar/">the widget's documentation</a> for
	 * more information about the YUI Calendar.<br/>
	 * Example:
	 * 
	 * <pre>
	 * protected String getAdditionalJavaScript()
	 * {
	 * 	return &quot;${calendar}.addRenderer(\&quot;10/3\&quot;, ${calendar}.renderCellStyleHighlight1);&quot;;
	 * }
	 * </pre>
	 * 
	 * @return a String containing additional JavaScript code
	 */
	protected String getAdditionalJavaScript()
	{
		return "";
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isEnabled(final Component component)
	{
		return component.isEnabledInHierarchy();
	}

	/**
	 * 
	 * @param map
	 *            the key-value pairs to be serialized
	 * @param json
	 *            the buffer holding the constructed json
	 */
	private void appendMapping(final Map<String, Object> map, final StringBuilder json)
	{
		json.append('{');
		for (Iterator<Entry<String, Object>> i = map.entrySet().iterator(); i.hasNext();)
		{
			Entry<String, Object> entry = i.next();
			json.append(entry.getKey());
			Object value = entry.getValue();
			if (value instanceof CharSequence)
			{
				json.append(":\"");
				json.append(Strings.toEscapedUnicode(value.toString()));
				json.append('"');
			}
			else if (value instanceof CharSequence[])
			{
				json.append(":[");
				CharSequence[] valueArray = (CharSequence[])value;
				for (int j = 0; j < valueArray.length; j++)
				{
					CharSequence tmpValue = valueArray[j];
					if (j > 0)
					{
						json.append(',');
					}
					if (tmpValue != null)
					{
						json.append('"');
						json.append(Strings.toEscapedUnicode(tmpValue.toString()));
						json.append('"');
					}
				}
				json.append(']');
			}
			else if (value instanceof Map)
			{
				json.append(':');
				@SuppressWarnings("unchecked")
				Map<String, Object> nmap = (Map<String, Object>)value;
				appendMapping(nmap, json);
			}
			else
			{
				json.append(':');
				json.append(Strings.toEscapedUnicode(String.valueOf(value)));
			}
			if (i.hasNext())
			{
				json.append(',');
			}
		}
		json.append('}');
	}
}
