/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
//         www.projectforge.org
//
// Copyright (C) 2001-2014 Kai Reinhard ([email protected])
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition 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 General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////

package org.projectforge.web.wicket;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.Date;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.extensions.ajax.markup.html.AjaxEditableLabel;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.image.ContextImage;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.protocol.http.RequestUtils;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.UrlUtils;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.string.StringValue;
import org.projectforge.calendar.DayHolder;
import org.projectforge.calendar.TimePeriod;
import org.projectforge.common.BeanHelper;
import org.projectforge.common.ClassHelper;
import org.projectforge.common.DateFormatType;
import org.projectforge.common.DateFormats;
import org.projectforge.common.DateHelper;
import org.projectforge.common.DateHolder;
import org.projectforge.common.NumberHelper;
import org.projectforge.common.StringHelper;
import org.projectforge.core.BaseDao;
import org.projectforge.core.ConfigXml;
import org.projectforge.web.HtmlHelper;
import org.projectforge.web.LoginPage;
import org.projectforge.web.URLHelper;
import org.projectforge.web.WebConfig;
import org.projectforge.web.calendar.DateTimeFormatter;
import org.projectforge.web.fibu.ISelectCallerPage;
import org.projectforge.web.mobile.AbstractSecuredMobilePage;
import org.projectforge.web.mobile.MenuMobilePage;
import org.projectforge.web.wicket.components.DatePanel;
import org.projectforge.web.wicket.components.LabelValueChoiceRenderer;
import org.projectforge.web.wicket.flowlayout.ComponentWrapperPanel;
import org.projectforge.web.wicket.flowlayout.FieldsetPanel;
import org.projectforge.web.wicket.flowlayout.IconPanel;
import org.projectforge.web.wicket.flowlayout.IconType;

public class WicketUtils
{
  private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(WicketUtils.class);

  private static String APPLICATION_CONTEXT = "/ProjectForge";

  private static String absoluteContextPath;

  public static final String WICKET_APPLICATION_PATH = "wa/";

  public static String getContextPath()
  {
    return APPLICATION_CONTEXT;
  }

  static void setContextPath(final String contextPath)
  {
    APPLICATION_CONTEXT = contextPath;
    absoluteContextPath = null;
  }

  /**
   * Examples: https://www.projectforge.org/demo or https://www.acme.com/ProjectForge.
   * @return Absolute context path of the web application.
   */
  public static String getAbsoluteContextPath()
  {
    if (absoluteContextPath == null) {
      final RequestCycle requestCycle = RequestCycle.get();
      final String url = requestCycle.getUrlRenderer().renderFullUrl(Url.parse(requestCycle.urlFor(LoginPage.class, null).toString()));
      final String basePath = "/" + WICKET_APPLICATION_PATH;
      final int pos = url.indexOf(basePath);
      if (pos < 0) {
        log.warn("Couln't get base url of '" + url + "'. Sub string '" + basePath + "' expected.");
        return url;
      }
      absoluteContextPath = url.substring(0, pos);
    }
    return absoluteContextPath;
  }

  public static HttpServletRequest getHttpServletRequest(final Request request)
  {
    return (HttpServletRequest) request.getContainerRequest();
  }

  public static HttpServletResponse getHttpServletResponse(final Response response)
  {
    return (HttpServletResponse) response.getContainerResponse();
  }

  public static boolean contains(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null) {
      return false;
    } else {
      return sval.isNull() == false;
    }
  }

  public static String getAsString(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return null;
    } else {
      return sval.toString();
    }
  }

  public static Integer getAsInteger(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return null;
    } else {
      return sval.toInteger();
    }
  }

  public static int getAsInt(final PageParameters parameters, final String name, final int defaultValue)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return defaultValue;
    } else {
      return sval.toInt();
    }
  }

  public static Long getAsLong(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return null;
    } else {
      return sval.toLong();
    }
  }

  public static Boolean getAsBooleanObject(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return null;
    } else {
      return sval.toBooleanObject();
    }
  }

  public static boolean getAsBoolean(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return false;
    } else {
      return sval.toBoolean();
    }
  }

  public static Object getAsObject(final PageParameters parameters, final String name, final Class< ? > type)
  {
    final StringValue sval = parameters.get(name);
    if (sval == null || sval.isNull() == true) {
      return null;
    } else {
      return sval.to(type);
    }
  }

  /**
   * Renders &lt;link type="image/x-icon" rel="shortcut icon" href="favicon.ico" /&gt;
   * @param favicon The favicon file, e. g. "/ProjectForge/favicon.ico".
   */
  public static String getCssForFavicon(final String favicon)
  {
    return "<link type=\"image/x-icon\" rel=\"shortcut icon\" href=\"" + favicon + "\" />";
  }

  /**
   * Prepends APPLICATION_CONTEXT if url starts with '/', otherwise url is returned unchanged.
   * @param url
   */
  public static final String getAbsoluteUrl(final String url)
  {
    if (url.startsWith("/") == true) {
      return APPLICATION_CONTEXT + url;
    }
    return url;
  }

  /**
   * Get the url for the given path (without image path). Later, the path of the images is changeable.
   * @param requestCycle Needed to encode url.
   * @param subpath
   * @return
   */
  public static String getImageUrl(final RequestCycle requestCycle, final String path)
  {
    return getUrl(requestCycle, path, true);
  }

  /**
   * Should be c:url equivalent, but isn't yet (works for now).
   * @param requestCycle Needed to encode url.
   * @param path
   * @param encodeUrl
   * @return path itself if not starts with '/' otherwise "/ProjectForge" + path with session id and params.
   */
  public static String getUrl(final RequestCycle requestCycle, final String path, final boolean encodeUrl)
  {
    String url = UrlUtils.rewriteToContextRelative(path, requestCycle);
    if (encodeUrl == true) {
      url = requestCycle.getResponse().encodeURL(url);
    }
    return url;
  }

  /**
   * Works for Wicket and non Wicket calling pages. For non Wicket callers the pageClass must be bookmarked in Wicket application.
   * @param pageClass
   * @param Optional list of params in tupel form: key, value, key, value...
   */
  public static String getBookmarkablePageUrl(final Class< ? extends Page> pageClass, final String... params)
  {
    final RequestCycle requestCylce = RequestCycle.get();
    if (requestCylce != null) {
      final PageParameters pageParameter = getPageParameters(params);
      return requestCylce.urlFor(pageClass, pageParameter).toString();
    } else {
      // RequestCycle.get().urlFor(pageClass, pageParameter).toString() can't be used for non wicket requests!
      final String alias = WicketApplication.getBookmarkableMountPath(pageClass);
      if (alias == null) {
        log.error("Given page class is not mounted. Please mount class in WicketApplication: " + pageClass);
        return getDefaultPageUrl();
      }
      if (params == null) {
        return WICKET_APPLICATION_PATH + alias;
      }
      final StringBuffer buf = new StringBuffer();
      buf.append(WICKET_APPLICATION_PATH).append(alias);
      try {
        for (int i = 0; i < params.length; i += 2) {
          if (i == 0) {
            buf.append("?");
          } else {
            buf.append("&");
          }
          buf.append(URLEncoder.encode(params[i], "UTF-8")).append("=");
          if (i + 1 < params.length) {
            buf.append(URLEncoder.encode(params[i + 1], "UTF-8"));
          }
        }
      } catch (final UnsupportedEncodingException ex) {
        log.error(ex.getMessage(), ex);
      }
      return buf.toString();
    }
  }

  /**
   * Tuples of parameters converted to Wicket parameters.
   * @param params
   * @return
   */
  public static PageParameters getPageParameters(final String[] params)
  {
    final PageParameters pageParameters = new PageParameters();
    if (params != null) {
      for (int i = 0; i < params.length; i += 2) {
        if (i + 1 < params.length) {
          pageParameters.add(params[i], params[i + 1]);
        } else {
          pageParameters.add(params[i], null);
        }
      }
    }
    return pageParameters;
  }

  /**
   * @param relativePagePath
   * @return
   * @see RequestUtils#toAbsolutePath(String, String)
   * @see URLHelper#removeJSessionId(String)
   */
  public final static String toAbsolutePath(final String requestUrl, final String relativePagePath)
  {
    final String absoluteUrl = RequestUtils.toAbsolutePath(requestUrl, relativePagePath);
    return URLHelper.removeJSessionId(absoluteUrl);
  }

  /**
   * @param id
   * @return new PageParameters containing the given id as page parameter.
   */
  public final static PageParameters getEditPageParameters(final Integer id)
  {
    return new PageParameters().set(AbstractEditPage.PARAMETER_KEY_ID, id);
  }

  /**
   * @return Default page of ProjectForge. Currently {@link WicketApplication#DEFAULT_PAGE} is the default page (e. g. to redirect after
   *         login if no forward url is specified).
   */
  public static String getDefaultPageUrl()
  {
    return getBookmarkablePageUrl(getDefaultPage());
  }

  /**
   * @return Default page of ProjectForge. Currently {@link WicketApplication#DEFAULT_PAGE} is the default page (e. g. to redirect after
   *         cancel if no other return page is specified).
   */
  public static Class< ? extends WebPage> getDefaultPage()
  {
    final WebConfig webConfig = ConfigXml.getInstance().getWebConfig();
    return webConfig != null ? webConfig.getDefaultPage() : WicketApplication.DEFAULT_PAGE;
  }

  /**
   * @return MenuMobilePage.class.
   */
  public static Class< ? extends AbstractSecuredMobilePage> getDefaultMobilePage()
  {
    return MenuMobilePage.class;
  }

  /**
   * If value is null or value is default value then nothing is done. Otherwise the given value is added as page parameter under the given
   * key. Dates and TimePeriods are converted and can be gotten by {@link #getPageParameter(PageParameters, String, Class)}.
   * @param pageParameters
   * @param key
   * @param value
   * @see ClassHelper#isDefaultType(Class, Object)
   */
  public static void putPageParameter(final PageParameters pageParameters, final String key, final Object value)
  {
    if (value == null) {
      // Do not put null values to page parameters.
    } else if (ClassHelper.isDefaultType(value.getClass(), value)) {
      // Do not put default values to page parameters.
    } else if (value instanceof Date) {
      addOrReplaceParameter(pageParameters, key, ((Date) value).getTime());
    } else if (value instanceof TimePeriod) {
      addOrReplaceParameter(pageParameters, key, ((TimePeriod) value).getFromDate().getTime()
          + "-"
          + ((TimePeriod) value).getToDate().getTime());
    } else {
      addOrReplaceParameter(pageParameters, key, value);
    }
  }

  public static void addOrReplaceParameter(final PageParameters pageParameters, final String key, final Object value)
  {
    if (pageParameters.get(key).isNull() == true) {
      pageParameters.add(key, value);
    } else {
      pageParameters.set(key, value);
    }
  }

  public static void putPageParameters(final ISelectCallerPage callerPage, final Object dataObject, final Object filterObject,
      final PageParameters pageParameters, final String[] bookmarkableProperties)
  {
    if (bookmarkableProperties == null) {
      return;
    }
    // final String pre = prefix != null ? prefix + "." : "";
    for (final String propertyString : bookmarkableProperties) {
      final InitialPageParameterHolder paramHolder = new InitialPageParameterHolder(propertyString);
      final Object bean;
      if (paramHolder.isFilterParameter() == true) {
        bean = filterObject;
      } else {
        bean = dataObject;
      }
      try {
        final Object value = BeanHelper.getProperty(bean, paramHolder.property);
        WicketUtils.putPageParameter(pageParameters, paramHolder.prefix + paramHolder.alias, value);
      } catch (final Exception ex) {
        log.warn("Couldn't put page parameter '" + paramHolder.property + "' of bean '" + bean + "'. Ignoring this parameter.");
      }
    }
  }

  /**
   * @param pageParameters
   * @param key
   * @param objectType
   * @see #putPageParameter(PageParameters, String, Object)
   */
  public static Object getPageParameter(final PageParameters pageParameters, final String key, final Class< ? > objectType)
  {
    if (objectType.isAssignableFrom(Date.class) == true) {
      final StringValue sval = pageParameters.get(key);
      if (sval.isNull() == true) {
        return null;
      }
      return new Date(sval.toLongObject());
    } else if (objectType.isAssignableFrom(Boolean.class) == true) {
      return pageParameters.get(key).toBooleanObject();
    } else if (objectType.isPrimitive() == true) {
      if (Boolean.TYPE.equals(objectType)) {
        return pageParameters.get(key).toBooleanObject();
      } else if (Integer.TYPE.equals(objectType) == true) {
        return pageParameters.get(key).toInteger();
      } else if (Long.TYPE.equals(objectType) == true) {
        return pageParameters.get(key).toLong();
      } else if (Float.TYPE.equals(objectType) == true) {
        return new Float(pageParameters.get(key).toDouble());
      } else if (Double.TYPE.equals(objectType) == true) {
        return pageParameters.get(key).toDouble();
      } else if (Character.TYPE.equals(objectType) == true) {
        return pageParameters.get(key).toChar();
      } else {
        log.warn("Primitive objectType '" + objectType + "' not yet implemented. Parameter type '" + key + "' is ignored.");
      }
    } else if (Enum.class.isAssignableFrom(objectType) == true) {
      final StringValue sval = pageParameters.get(key);
      if (sval.isNull() == true) {
        return null;
      }
      final String sValue = sval.toString();
      @SuppressWarnings({ "unchecked", "rawtypes"})
      final Enum< ? > en = Enum.valueOf((Class<Enum>) objectType, sValue);
      return en;
    } else if (objectType.isAssignableFrom(Integer.class) == true) {
      final StringValue sval = pageParameters.get(key);
      if (sval.isNull() == true) {
        return null;
      }
      return sval.toInteger();
    } else if (objectType.isAssignableFrom(String.class) == true) {
      return pageParameters.get(key).toString();
    } else if (objectType.isAssignableFrom(TimePeriod.class) == true) {
      final String sValue = pageParameters.get(key).toString();
      if (sValue == null) {
        return null;
      }
      final int pos = sValue.indexOf('-');
      if (pos < 0) {
        log.warn("PageParameter of type TimePeriod '" + objectType.getName() + "' in wrong format: " + sValue);
        return null;
      }
      final Long fromTime = NumberHelper.parseLong(sValue.substring(0, pos));
      final Long toTime = pos < sValue.length() - 1 ? NumberHelper.parseLong(sValue.substring(pos + 1)) : null;
      return new TimePeriod(fromTime != null ? new Date(fromTime) : null, toTime != null ? new Date(toTime) : null);
    } else {
      log.error("PageParameter of type '" + objectType.getName() + "' not yet supported.");
    }
    return null;
  }

  public static boolean hasParameter(final PageParameters parameters, final String name)
  {
    final StringValue sval = parameters.get(name);
    return sval != null && sval.isNull() == false;
  }

  /**
   * At least one parameter should be given for setting the fill the bean with all book-markable properties (absent properties will be set
   * to zero). If the given bean is an instance of {@link ISelectCallerPage} then the select/unselect methods are used, otherwise the
   * properties will set directly of the given bean.
   * @param bean
   * @param parameters
   * @param prefix
   * @param bookmarkableProperties
   */
  public static void evaluatePageParameters(final ISelectCallerPage callerPage, final Object dataObject, final Object filter,
      final PageParameters parameters, final String[] bookmarkableProperties)
  {
    if (bookmarkableProperties == null) {
      return;
    }
    // First check if any parameter is given:
    boolean useParameters = false;
    for (final String str : bookmarkableProperties) {
      final InitialPageParameterHolder paramHolder = new InitialPageParameterHolder(str);
      if (hasParameter(parameters, paramHolder.prefix + paramHolder.property) == true
          || hasParameter(parameters, paramHolder.prefix + paramHolder.alias) == true) {
        useParameters = true;
        break;
      }
    }
    if (useParameters == false) {
      // No book-markable parameters found.
      return;
    }
    for (final String str : bookmarkableProperties) {
      final InitialPageParameterHolder paramHolder = new InitialPageParameterHolder(str);
      String key = null;
      if (hasParameter(parameters, paramHolder.prefix + paramHolder.property) == true) {
        key = paramHolder.property;
      } else if (hasParameter(parameters, paramHolder.prefix + paramHolder.alias) == true) {
        key = paramHolder.alias;
      }
      if (paramHolder.isCallerPageParameter() == true) {
        if (callerPage == null) {
          log.warn("PageParameter '" + str + "' ignored, ISelectCallerPage isn't given.");
        } else if (key == null) {
          callerPage.unselect(paramHolder.property);
        } else {
          callerPage.select(paramHolder.property, parameters.get(paramHolder.prefix + key).toString());
        }
      } else {
        try {
          final Object bean;
          if (paramHolder.isFilterParameter() == true) {
            // Use filter object
            bean = filter;
            if (bean == null) {
              log.warn("PageParameter '" + str + "' ignored, filter isn't given.");
              continue;
            }
          } else {
            bean = dataObject;
            if (bean == null) {
              log.warn("PageParameter '" + str + "' ignored, dataObject isn't given.");
              continue;
            }
          }
          final Method method = BeanHelper.determineGetter(bean.getClass(), paramHolder.property);
          if (key == null) {
            BeanHelper.setProperty(bean, paramHolder.property, ClassHelper.getDefaultType(method.getReturnType()));
          } else {
            final Object value = WicketUtils.getPageParameter(parameters, paramHolder.prefix + key, method.getReturnType());
            BeanHelper.setProperty(bean, paramHolder.property, value);
          }
        } catch (final Exception ex) {
          log.warn("Property '" + key + "' not found. Ignoring URL parameter.");
        }
      }
    }
  }

  /**
   * Adds onclick attribute with "javascript:rowClick(this);".
   * @param row Html tr element.
   */
  public static void addRowClick(final Component row)
  {
    row.add(AttributeModifier.replace("onclick", "javascript:rowClick(this);"));
    // add marker css class for contextMenu javaScript
    row.add(new AttributeAppender("class", Model.of("withContextMenu"), " "));
  }

  /**
   * 
   * @return
   */
  public static ContextImage getInvisibleDummyImage(final String id, final RequestCycle requestCylce)
  {
    final ContextImage image = new ContextImage(id, WicketUtils.getImageUrl(requestCylce, WebConstants.IMAGE_SPACER));
    image.setVisible(false);
    return image;
  }

  public static Component getInvisibleComponent(final String id)
  {
    return new Label(id).setVisible(false);
  }

  /**
   * @param label
   * @param unit
   * @return label [<unit>] (label with appended unit in brackets).
   */
  public static String getLabelWithUnit(final String label, final String unit)
  {
    return label + " [" + unit + "]";
  }

  /**
   * Uses "jiraSupportTooltipImage" as component id.
   * @param parent only needed for localization
   * @param id
   * @return IconPanel which is invisible if JIRA isn't configured.
   */
  public static IconPanel getJIRASupportTooltipIcon(final Component parent, final String id)
  {
    final IconPanel icon = new IconPanel(id, IconType.JIRA_SUPPORT, Model.of(parent.getString("tooltip.jiraSupport.field.title")),
        Model.of(parent.getString("tooltip.jiraSupport.field.content")));
    if (isJIRAConfigured() == false) {
      icon.setVisible(false);
    }
    return icon;
  }

  /**
   * Uses "jiraSupportTooltipImage" as component id. Please use {@link FieldsetPanel#addJIRASupportHelpIcon()} instead of this method if
   * possible.
   * @param fieldset needed for localization and for getting new child id.
   * @return IconPanel which is invisible if JIRA isn't configured.
   */
  public static IconPanel getJIRASupportTooltipIcon(final FieldsetPanel fieldset)
  {
    return getJIRASupportTooltipIcon(fieldset, fieldset.newIconChildId());
  }

  /**
   */
  public static IconPanel getAlertTooltipIcon(final FieldsetPanel fieldset, final String tooltip)
  {
    return getAlertTooltipIcon(fieldset, null, Model.of(tooltip));
  }

  /**
   */
  public static IconPanel getAlertTooltipIcon(final FieldsetPanel fieldset, final IModel<String> title, final IModel<String> tooltip)
  {
    final IconPanel icon = new IconPanel(fieldset.newIconChildId(), IconType.ALERT, title, tooltip);
    return icon;
  }

  public static final boolean isJIRAConfigured()
  {
    return ConfigXml.getInstance().isJIRAConfigured();
  }

  /**
   * Add JavaScript function showDeleteEntryQuestionDialog(). Depending on BaseDao.isHistorizable() a delete or mark-as-deleted question
   * will be displayed. Usage in markup: &lt;script wicket:id="showDeleteEntryQuestionDialog"&gt;[...]&lt;/script&gt;
   * @param parent
   * @param dao
   */
  public static void addShowDeleteRowQuestionDialog(final MarkupContainer parent, final BaseDao< ? > dao)
  {
    final StringBuffer buf = new StringBuffer();
    buf.append("function showDeleteEntryQuestionDialog() {\n").append("  return window.confirm('");
    if (dao.isHistorizable() == true) {
      buf.append(parent.getString("question.markAsDeletedQuestion"));
    } else {
      buf.append(parent.getString("question.deleteQuestion"));
    }
    buf.append("');\n}\n");
    parent.add(new Label("showDeleteEntryQuestionDialog", buf.toString()).setEscapeModelStrings(false).add(
        AttributeModifier.replace("type", "text/javascript")));
  }

  /**
   * Sets the html attribute placeholder.
   * @param component
   * @param value
   */
  public static void setPlaceHolderAttribute(Component component, final String value)
  {
    if (component instanceof ComponentWrapperPanel) {
      component = ((ComponentWrapperPanel) component).getFormComponent();
    }
    component.add(AttributeModifier.replace("placeholder", value));
  }

  /**
   * @param parent Only for i18n needed.
   * @param startTime Start time or null.
   * @param stopTime Stop time or null.
   * @return The weeks of year range for the given start an stop time.
   */
  public static String getCalendarWeeks(final MarkupContainer parent, final Date startTime, final Date stopTime)
  {
    int fromWeek = -1;
    int toWeek = -1;
    if (startTime != null) {
      fromWeek = DateHelper.getWeekOfYear(startTime);
    }
    if (stopTime != null) {
      toWeek = DateHelper.getWeekOfYear(stopTime);
    }
    if (fromWeek < 0 && toWeek < 0) {
      return "";
    }
    final StringBuffer buf = new StringBuffer();
    buf.append(parent.getString("calendar.weekOfYearShortLabel")).append(" ");
    if (fromWeek >= 0) {
      buf.append(StringHelper.format2DigitNumber(fromWeek));
      if (toWeek == -1) {
        buf.append("-");
      } else if (toWeek != fromWeek) {
        buf.append("-").append(StringHelper.format2DigitNumber(toWeek));
      }
    } else {
      buf.append("-").append(StringHelper.format2DigitNumber(toWeek));
    }
    return buf.toString();
  }

  /**
   * @param date
   */
  public static String getUTCDate(final Date date)
  {
    if (date == null) {
      return "";
    }
    final DateHolder dh = new DateHolder(date);
    return DateHelper.TECHNICAL_ISO_UTC.get().format(dh.getDate());
  }

  /**
   * @param label Label as prefix
   * @param date
   * @return <label>: <date>
   */
  public static String getUTCDate(final String label, final Date date)
  {
    if (date == null) {
      return label + ":";
    }
    final DateHolder dh = new DateHolder(date);
    return label + ": " + DateHelper.TECHNICAL_ISO_UTC.get().format(dh.getDate());
  }

  /**
   * @param startTime Start time or null.
   * @param stopTime Stop time or null.
   */
  public static String getUTCDates(final Date startTime, final Date stopTime)
  {
    final StringBuffer buf = new StringBuffer();
    final DateHolder start = startTime != null ? new DateHolder(startTime) : null;
    final DateHolder stop = stopTime != null ? new DateHolder(stopTime) : null;
    if (start != null) {
      buf.append(DateHelper.TECHNICAL_ISO_UTC.get().format(start.getDate()));
      if (stop != null) {
        buf.append(" - ");
      }
    }
    if (stop != null) {
      buf.append(DateHelper.TECHNICAL_ISO_UTC.get().format(stop.getDate()));
    }
    return buf.toString();
  }

  public static LabelValueChoiceRenderer<Long> getDatumChoiceRenderer(final int lastNDays)
  {
    final LabelValueChoiceRenderer<Long> datumChoiceRenderer = new LabelValueChoiceRenderer<Long>();
    for (int i = 0; i > -lastNDays; i--) {
      final DayHolder day = new DayHolder();
      day.add(Calendar.DAY_OF_YEAR, i);
      datumChoiceRenderer.addValue(day.getSQLDate().getTime(),
          DateTimeFormatter.instance().getFormattedDate(day.getSQLDate(), DateFormats.getFormatString(DateFormatType.DATE)));
    }
    return datumChoiceRenderer;
  }

  public static void append(final Component component, final RowCssClass... rowCssClasses)
  {
    for (final RowCssClass rowCssClass : rowCssClasses) {
      component.add(AttributeModifier.append("class", rowCssClass.getCssClass()));
    }
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component.
   * @param component
   * @param title
   * @param text
   * @see #createTooltip(String, String)
   * @see #setStyleHasTooltip(Component)
   */
  public static Component addTooltip(final Component component, final String title, final String text)
  {
    return addTooltip(component, title, text, true);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component.
   * @param component
   * @param title
   * @param text
   * @see #createTooltip(String, String)
   * @see #setStyleHasTooltip(Component)
   */
  public static Component addTooltip(final Component component, final String title, final String text, final boolean rightAlignment)
  {
    return addTooltip(component, Model.of(title), Model.of(text), rightAlignment);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component.
   * @param component
   * @param text
   * @see #createTooltip(String, String)
   * @see #setStyleHasTooltip(Component)
   */
  public static Component addTooltip(final Component component, final String text)
  {
    return addTooltip(component, text, true);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component.
   * @param component
   * @param text
   * @param rightAlignment If false (default is true) the tooltip will be aligned at the bottom.
   * @see #createTooltip(String, String)
   * @see #setStyleHasTooltip(Component)
   */
  public static Component addTooltip(final Component component, final String text, final boolean rightAlignment)
  {
    return addTooltip(component, null, Model.of(text), rightAlignment);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component. Does not modify the given tool tip text!
   * @param component
   * @param text
   */
  public static Component addTooltip(final Component component, final IModel<String> text)
  {
    return addTooltip(component, text, true);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component. Does not modify the given tool tip text!
   * @param component
   * @param text
   */
  public static Component addTooltip(final Component component, final IModel<String> text, final boolean rightAlignment)
  {
    return addTooltip(component, null, text, rightAlignment);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component. Does not modify the given tool tip text!
   * @param component
   * @param title
   * @param text If the string contains "\n" characters then html=true and &lt;br/&gt; are used.
   */
  public static Component addTooltip(final Component component, final IModel<String> title, final IModel<String> text)
  {
    return addTooltip(component, title, text, true);
  }

  /**
   * Adds a SimpleAttributeModifier("title", ...) to the given component. Does not modify the given tool tip text!
   * @param component
   * @param title
   * @param text If the string contains "\n" characters then html=true and &lt;br/&gt; are used.
   * @param rightAlignment If false (default is true) the tooltip will be aligned at the bottom.
   */
  public static Component addTooltip(final Component component, final IModel<String> title, final IModel<String> text,
      final boolean rightAlignment)
  {
    @SuppressWarnings("serial")
    final IModel<String> myModel = new Model<String>() {
      /**
       * @see org.apache.wicket.model.Model#getObject()
       */
      @Override
      public String getObject()
      {
        if (text != null && text.getObject() != null && text.getObject().indexOf("\n") > 0) {
          final String newText = HtmlHelper.escapeHtml(text.getObject(), true);
          return newText;
        }
        return text.getObject();
      }
    };
    component.add(AttributeModifier.replace("data-html", true));
    if (title != null && title.getObject() != null) {
      component.add(AttributeModifier.replace("rel", rightAlignment ? "mypopup-right" : "mypopup"));
      component.add(AttributeModifier.replace("data-original-title", title));
      component.add(AttributeModifier.replace("data-content", myModel));
    } else {
      component.add(AttributeModifier.replace("rel", rightAlignment ? "mytooltip-right" : "mytooltip"));
      component.add(AttributeModifier.replace("title", myModel));
    }
    return component;
  }

  /**
   * You need to use {@link AjaxEditableLabel#getLabel()}.
   * @param label
   * @return
   */
  public static Component addEditableLabelDefaultTooltip(final Component label)
  {
    return addTooltip(label, label.getString("form.ajaxEditableLabel.tooltip"));
  }

  public static Component setWarningTooltip(final Component component)
  {
    component.add(AttributeModifier.append("class", "warning"));
    return component;
  }

  /**
   * Sets readonly="readonly" and "readOnly" as class.
   * @param component
   * @return This for chaining.
   */
  public static FormComponent< ? > setReadonly(final FormComponent< ? > component)
  {
    component.add(AttributeModifier.append("class", "readonly"));
    component.add(AttributeModifier.replace("readonly", "readonly"));
    return component;
  }

  /**
   * Sets attribute size (only for TextFields) and style="length: width"; The width value is size + 0.5 em and for drop down choices size +
   * 2em;
   * @param component
   * @param size
   * @return This for chaining.
   */
  public static FormComponent< ? > setSize(final FormComponent< ? > component, final int size)
  {
    return setSize(component, size, true);
  }

  /**
   * Sets attribute size (only for TextFields) and style="length: width"; The width value is size + 0.5 em and for drop down choices size +
   * 2em;
   * @param component
   * @param size
   * @param important If true then "!important" is appended to the width style (true is default).
   * @return This for chaining.
   */
  public static FormComponent< ? > setSize(final FormComponent< ? > component, final int size, final boolean important)
  {
    if (component instanceof TextField) {
      component.add(AttributeModifier.replace("size", String.valueOf(size)));
    }
    final StringBuffer buf = new StringBuffer(20);
    buf.append("width: ");
    if (component instanceof DropDownChoice) {
      buf.append(size + 2).append("em");
    } else {
      buf.append(size).append(".5em");
    }
    if (important == true) {
      buf.append(" !important;");
    }
    buf.append(";");
    component.add(AttributeModifier.append("style", buf.toString()));
    return component;
  }

  /**
   * Sets attribute size (only for TextFields) and style="width: x%";
   * @param component
   * @param size
   * @return This for chaining.
   */
  public static FormComponent< ? > setPercentSize(final FormComponent< ? > component, final int size)
  {
    component.add(AttributeModifier.append("style", "width: " + size + "%;"));
    return component;
  }

  /**
   * Sets attribute font-size: style="font-size: 1.1em;";
   * @param component
   * @param size
   * @return This for chaining.
   */
  public static Component setFontSizeLarge(final Component component)
  {
    component.add(AttributeModifier.append("style", "font-size: 1.5em;"));
    return component;
  }

  public static Component setStrong(final Component component)
  {
    component.add(AttributeModifier.append("style", "font-weight: bold;"));
    return component;
  }

  /**
   * Sets attribute style="height: <height>ex;"
   * @param component
   * @param size
   * @return This for chaining.
   */
  public static FormComponent< ? > setHeight(final FormComponent< ? > component, final int height)
  {
    component.add(AttributeModifier.append("style", "height: " + height + "ex;"));
    return component;
  }

  /**
   * Adds class="focus" to the given component. It's evaluated by the adminica_ui.js. FocusOnLoadBehaviour doesn't work because the focus is
   * set to early (before the components are visible).
   * @param component
   * @return This for chaining.
   */
  public static FormComponent< ? > setFocus(final FormComponent< ? > component)
  {
    component.add(setFocus());
    return component;
  }

  /**
   * Same as {@link #setFocus(FormComponent)}
   * @return AttributeAppender
   */
  public static Behavior setFocus()
  {
    return new FocusOnLoadBehavior();
  }

  /**
   * For field-sets with multiple fields this method generates a multi label, such as "label1/label2", e. g. "zip code/city".
   * @param label
   * @return
   */
  public static String createMultipleFieldsetLabel(final String... labels)
  {
    return StringHelper.listToString("/", labels);
  }

  /**
   * If true then a tick-mark icon is returned, otherwise an invisible label.
   * @param requestCycle
   * @param componentId
   * @param value
   * @return
   */
  public static Component createBooleanLabel(final RequestCycle requestCycle, final String componentId, final boolean value)
  {
    if (value == true) {
      return new IconPanel(componentId, IconType.ACCEPT);
    }
    return new Label(componentId, "invisible").setVisible(false);
  }

  /**
   * Searchs the attribute behavior (SimpleAttributeModifier or AttibuteApendModifier) with the given attribute name and returns it if
   * found, otherwise null.
   * @param comp
   * @param name Name of attribute.
   */
  public static AttributeModifier getAttributeModifier(final Component comp, final String name)
  {
    for (final Behavior behavior : comp.getBehaviors()) {
      if (behavior instanceof AttributeAppender && name.equals(((AttributeAppender) behavior).getAttribute()) == true) {
        return (AttributeAppender) behavior;
      } else if (behavior instanceof AttributeModifier && name.equals(((AttributeModifier) behavior).getAttribute()) == true) {
        return (AttributeModifier) behavior;
      }
    }
    return null;
  }

  /**
   * Calls {@link Component#setResponsePage(Page)}. If the responseItem is an instance of a Page then setResponse for this Page is called
   * otherwise setResponse is called via {@link Component#getPage()}.
   * @param component
   * @param responseItem Page or Component.
   */
  public static void setResponsePage(final Component component, final Component responseItem)
  {
    if (responseItem instanceof Page) {
      component.setResponsePage((Page) responseItem);
    } else {
      component.setResponsePage(responseItem.getPage());
    }
  }

  /**
   * Casts callerPage to Component and calls {@link #setResponsePage(Component, Component)}.
   * @param component
   * @param callerPage Must be an instance of Component (otherwise a ClassCastException is thrown).
   */
  public static void setResponsePage(final Component component, final ISelectCallerPage callerPage)
  {
    setResponsePage(component, (Component) callerPage);
  }

  public static AttributeModifier javaScriptConfirmDialogOnClick(final String message)
  {
    final String escapedText = message.replace("'", "\'");
    return AttributeModifier.replace("onclick", "javascript:return showConfirmDialog('" + escapedText + "');");
  }

  @SuppressWarnings("unchecked")
  public static void setLabel(final FormComponent< ? > component, final Label label)
  {
    final IModel<String> labelModel = (IModel<String>) label.getDefaultModel();
    if (component instanceof DatePanel) {
      ((DatePanel) component).getDateField().setLabel(labelModel);
    } else {
      component.setLabel(labelModel);
    }
  }

  public static boolean isParent(final Component parent, final Component descendant)
  {
    final MarkupContainer p = descendant.getParent();
    if (p == null) {
      return false;
    } else if (p == parent) {
      return true;
    } else {
      return isParent(parent, p);
    }
  }
}