content: /*
   *  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.causeway.viewer.wicket.ui.util;
  
  import static de.agilecoders.wicket.jquery.JQuery.$;
  
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.Serializable;
  import java.util.List;
  import java.util.Optional;
  import java.util.OptionalInt;
  import java.util.function.BiConsumer;
  import java.util.function.Function;
  import java.util.function.Supplier;
  import java.util.stream.Stream;
  
  import org.apache.wicket.AttributeModifier;
  import org.apache.wicket.Component;
  import org.apache.wicket.MarkupContainer;
  import org.apache.wicket.Page;
  import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
  import org.apache.wicket.ajax.AjaxEventBehavior;
  import org.apache.wicket.ajax.AjaxRequestTarget;
  import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
  import org.apache.wicket.ajax.markup.html.form.AjaxButton;
  import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
  import org.apache.wicket.behavior.AttributeAppender;
  import org.apache.wicket.behavior.Behavior;
  import org.apache.wicket.event.IEvent;
  import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxButton;
  import org.apache.wicket.extensions.markup.html.image.resource.ThumbnailImageResource;
  import org.apache.wicket.markup.ComponentTag;
  import org.apache.wicket.markup.MarkupStream;
  import org.apache.wicket.markup.head.CssHeaderItem;
  import org.apache.wicket.markup.head.IHeaderResponse;
  import org.apache.wicket.markup.head.JavaScriptContentHeaderItem;
  import org.apache.wicket.markup.head.JavaScriptHeaderItem;
  import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
  import org.apache.wicket.markup.html.WebMarkupContainer;
  import org.apache.wicket.markup.html.basic.Label;
  import org.apache.wicket.markup.html.form.Button;
  import org.apache.wicket.markup.html.form.DropDownChoice;
  import org.apache.wicket.markup.html.form.Form;
  import org.apache.wicket.markup.html.form.FormComponent;
  import org.apache.wicket.markup.html.form.FormComponentUpdatingBehavior;
  import org.apache.wicket.markup.html.form.TextArea;
  import org.apache.wicket.markup.html.form.TextField;
  import org.apache.wicket.markup.html.form.upload.FileUpload;
  import org.apache.wicket.markup.html.form.upload.FileUploadField;
  import org.apache.wicket.markup.html.image.Image;
  import org.apache.wicket.markup.html.image.NonCachingImage;
  import org.apache.wicket.markup.html.image.resource.BufferedDynamicImageResource;
  import org.apache.wicket.markup.html.link.AbstractLink;
  import org.apache.wicket.markup.html.link.BookmarkablePageLink;
  import org.apache.wicket.markup.html.link.ResourceLink;
  import org.apache.wicket.markup.html.list.ListItem;
  import org.apache.wicket.markup.html.list.ListView;
  import org.apache.wicket.markup.html.panel.Fragment;
  import org.apache.wicket.markup.parser.XmlTag.TagType;
  import org.apache.wicket.markup.repeater.Item;
  import org.apache.wicket.markup.repeater.OddEvenItem;
  import org.apache.wicket.markup.repeater.RepeatingView;
  import org.apache.wicket.model.IModel;
  import org.apache.wicket.model.Model;
  import org.apache.wicket.request.IRequestCycle;
  import org.apache.wicket.request.cycle.RequestCycle;
  import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
  import org.apache.wicket.request.mapper.parameter.PageParameters;
  import org.apache.wicket.request.resource.ContentDisposition;
  import org.apache.wicket.request.resource.CssResourceReference;
  import org.apache.wicket.request.resource.IResource;
  import org.apache.wicket.request.resource.ResourceReference;
  import org.apache.wicket.resource.JQueryPluginResourceReference;
  import org.apache.wicket.util.convert.IConverter;
  import org.apache.wicket.util.file.Files;
  import org.apache.wicket.util.resource.FileResourceStream;
  import org.apache.wicket.validation.IValidationError;
  import org.apache.wicket.validation.ValidationError;
  import org.apache.wicket.validation.validator.StringValidator;
  import org.danekja.java.util.function.serializable.SerializableBiConsumer;
  import org.danekja.java.util.function.serializable.SerializableBooleanSupplier;
  import org.danekja.java.util.function.serializable.SerializableConsumer;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.applib.Identifier;
  import org.apache.causeway.applib.fa.FontAwesomeLayers;
  import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
  import org.apache.causeway.commons.internal.base._Casts;
  import org.apache.causeway.commons.internal.base._Strings;
  import org.apache.causeway.commons.internal.debug._Probe;
  import org.apache.causeway.commons.internal.debug._Probe.EntryPoint;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.commons.internal.functions._Functions.SerializableFunction;
  import org.apache.causeway.commons.internal.functions._Functions.SerializableSupplier;
  import org.apache.causeway.core.config.CausewayConfiguration.Viewer.Wicket;
  import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
  import org.apache.causeway.viewer.commons.model.components.UiString;
  import org.apache.causeway.viewer.wicket.model.hints.CausewayActionCompletedEvent;
  import org.apache.causeway.viewer.wicket.model.hints.CausewayEnvelopeEvent;
  import org.apache.causeway.viewer.wicket.model.models.coll.DataRowToggleWkt;
  import org.apache.causeway.viewer.wicket.model.models.coll.DataRowWkt;
  import org.apache.causeway.viewer.wicket.ui.components.attributes.markup.MarkupComponent;
  import org.apache.causeway.viewer.wicket.ui.components.text.TextAreaWithConverter;
  import org.apache.causeway.viewer.wicket.ui.components.text.TextFieldWithConverter;
  import org.apache.causeway.viewer.wicket.ui.components.widgets.fileinput.FileUploadFieldWithNestingFix;
  import org.apache.causeway.viewer.wicket.ui.components.widgets.links.AjaxLinkNoPropagate;
  import org.apache.causeway.viewer.wicket.ui.panels.PanelUtil;
  import org.apache.causeway.viewer.wicket.ui.util.BootstrapConstants.ButtonSemantics;
  
  import org.jspecify.annotations.NonNull;
  import lombok.experimental.UtilityClass;
  
  import de.agilecoders.wicket.core.markup.html.bootstrap.behavior.CssClassNameAppender;
  import de.agilecoders.wicket.core.util.Attributes;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.confirmation.ConfirmationBehavior;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.confirmation.ConfirmationConfig;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.checkboxx.CheckBoxX;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.checkboxx.CheckBoxXConfig;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.checkboxx.CheckBoxXConfig.Sizes;
  import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.fileinput.FileInputConfig;
  import de.agilecoders.wicket.jquery.Key;
  
  /**
   * Wicket common idioms, in alphabetical order.
   */
  @UtilityClass
  public class Wkt {
  
      public <T extends Component> T add(final @NonNull MarkupContainer container, final @Nullable T component) {
          if(component==null) return null;
          container.addOrReplace((Component)component);
          return component;
      }
  
      public <T extends Behavior> T add(final @NonNull Component component, final @Nullable T behavior) {
          if(behavior==null) return null;
          component.add((Behavior)behavior);
          return behavior;
      }
  
      /**
       * If condition is true, adds a new {@link Component} to the {@link MarkupContainer},
       * using given componentFactory. Otherwise permanently hide the corresponding tag
       * that is mapped by given id.
       * <p>
       * Regardless, if given componentFactory is null or returns null, the tag is hidden as well.
       */
      public <T extends Component> Optional<T> addIfElseHide(
              final boolean condition,
              final @NonNull MarkupContainer container,
              final @NonNull String id,
              final @Nullable Function<String, T> componentFactory) {
          final Optional<T> componentOpt = condition
                  ? Optional.ofNullable(componentFactory)
                      .map(factory->factory.apply(id))
                  : Optional.empty();
          componentOpt.ifPresentOrElse(
                  component->add(container, component),
                  ()->WktComponents.permanentlyHide(container, id));
          return componentOpt;
      }
  
      // -- AJAX ENABLER
  
      /**
       * Requirement for AJAX updates to work.
       */
      public <T extends Component> T ajaxEnable(final T component) {
          component.setOutputMarkupId(true);
          return component;
      }
  
      // -- ATTRIBUTES
  
      /**
       * If any of {@code component} or {@code attributeName} is null or empty, does nothing.
       * On empty {@code attributeValue} removes the attribute.
       */
      public <T extends Component> T attributeReplace(
              final @Nullable T component,
              final @Nullable String attributeName,
              final @Nullable String attributeValue) {
          if(component==null
                  || _Strings.isEmpty(attributeName)) {
              return component;
          }
          if(_Strings.isEmpty(attributeValue)) {
              component.add(AttributeModifier.remove(attributeName));
              return component;
          }
          component.add(AttributeModifier.replace(attributeName, attributeValue));
          return component;
      }
  
      /**
       * If any of {@code component} or {@code attributeName} is null or empty, does nothing.
       * On missing {@code attributeValue} removes the attribute.
       */
      public <T extends Component> T attributeReplace(
              final @Nullable T component,
              final @Nullable String attributeName,
              final @Nullable Integer attributeValue) {
          return attributeReplace(component, attributeName, attributeValue!=null
                  ? ""+attributeValue
                  : null);
      }
  
      // -- BEHAVIOR
  
      public Behavior behaviorOnClick(final SerializableConsumer<AjaxRequestTarget> onClick) {
          return new AjaxEventBehavior("click") {
              private static final long serialVersionUID = 1L;
  
              @Override
              protected void onEvent(final AjaxRequestTarget target) {
                  _Probe.entryPoint(EntryPoint.USER_INTERACTION, "Wicket Ajax Request, "
                          + "originating from User clicking on an "
                          + "editable Property (to start inline editing)"
                          + "or an Action (to enter param negotiaton or directly execute the Action).");
  
                  onClick.accept(target);
              }
          };
      }
  
      public Behavior behaviorFireOnEscapeKey(final SerializableConsumer<AjaxRequestTarget> onRespond) {
          return new AbstractDefaultAjaxBehavior() {
              private static final long serialVersionUID = 1L;
              private static final String PRE_JS =
                      "" + "$(document).ready( function() { \n"
                              + "  $(document).bind('keyup', function(evt) { \n"
                              + "    if (evt.keyCode == 27) { \n";
              private static final String POST_JS =
                      "" + "      evt.preventDefault(); \n   "
                              + "    } \n"
                              + "  }); \n"
                              + "});";
              @Override public void renderHead(final Component component, final IHeaderResponse response) {
                  super.renderHead(component, response);
                  final String javascript = PRE_JS + getCallbackScript() + POST_JS;
                  response.render(
                          new JavaScriptContentHeaderItem(javascript, null));
              }
              @Override protected void respond(final AjaxRequestTarget target) {
                  onRespond.accept(target);
              }
          };
      }
  
      private static class ReplaceDisabledTagWithReadonlyTagBehavior extends Behavior {
          private static final long serialVersionUID = 1L;
          @Override public void onComponentTag(final Component component, final ComponentTag tag) {
              super.onComponentTag(component, tag);
              if(component.isEnabled()) {
                  return;
              }
              tag.remove("disabled");
              tag.put("readonly","readonly");
          }
      }
  
      public Behavior behaviorReplaceDisabledTagWithReadonlyTag() {
          return new ReplaceDisabledTagWithReadonlyTagBehavior();
      }
  
      public Behavior behaviorConfirm(final ConfirmationConfig config) {
          return new ConfirmationBehavior(config);
      }
  
      public Behavior behaviorAddOnClick(
              final Component component,
              final SerializableConsumer<AjaxRequestTarget> onClick) {
          return add(component, behaviorOnClick(onClick));
      }
  
      public Behavior behaviorAddFireOnEscapeKey(
              final Component component,
              final SerializableConsumer<AjaxRequestTarget> onRespond) {
          return add(component, behaviorFireOnEscapeKey(onRespond));
      }
  
      public Behavior behaviorAddConfirm(
              final Component component,
              final ConfirmationConfig config) {
          return add(component, behaviorConfirm(config));
      }
  
      public void behaviorAddReplaceDisabledTagWithReadonlyTag(final @Nullable Component component) {
          if(component==null) {
              return;
          }
          if (component.getBehaviors(ReplaceDisabledTagWithReadonlyTagBehavior.class).isEmpty()) {
              component.add(new ReplaceDisabledTagWithReadonlyTagBehavior());
          }
      }
  
      // -- BOKMARKABLE PAGE LINK
  
      public BookmarkablePageLink<Void> bookmarkablePageLinkWithVisibility(
              final String id,
              final Class<? extends Page> pageClass,
              final PageParameters pageParameters,
              final SerializableBooleanSupplier dynamicVisibility) {
  
          return new BookmarkablePageLink<Void>(
                  id, pageClass, pageParameters) {
  
              private static final long serialVersionUID = 1L;
  
              @Override
              public boolean isVisible() {
                  return dynamicVisibility.getAsBoolean();
              }
  
              //XXX CAUSEWAY[3022] adds support for CTRL down behavior, that is, opens URL in new tab if CTRL pressed
              @Override protected CharSequence getOnClickScript(final CharSequence url) {
                  return "var win = this.ownerDocument.defaultView || this.ownerDocument.parentWindow; "
                          + "if (win == window) {"
                          + "  if(event.ctrlKey) {"
                          + "    window.open('" + url + "', '_blank').focus();"
                          + "  } else {"
                          + "    window.location.href='" + url + "';"
                          + "  }"
                          + "}"
                          + "return false";
              }
  
          };
  
      }
  
      // -- BUTTON
  
      public AjaxButton button(
              final String id,
              final IModel<String> labelModel,
              final SerializableBiConsumer<AjaxButton, AjaxRequestTarget> onClick) {
          return new AjaxButton(id, labelModel) {
              private static final long serialVersionUID = 1L;
              @Override public void onSubmit(final AjaxRequestTarget target) {
                  onClick.accept(this, target);
              }
          };
      }
  
      public AjaxButton buttonOk(
              final String id,
              final IModel<String> labelModel,
              final Wicket settings,
              final SerializableBiConsumer<AjaxButton, AjaxRequestTarget> onClick) {
  
          // be aware: settings is not Serializable
          var isPreventDoubleClickForFormSubmit = settings.isPreventDoubleClickForFormSubmit();
  
          return settings.isUseIndicatorForFormSubmit()
          ? new IndicatingAjaxButton(id, labelModel) {
              private static final long serialVersionUID = 1L;
              @Override public void onSubmit(final AjaxRequestTarget target) {
                  onClick.accept(this, target);
              }
              @Override protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) {
                  if (isPreventDoubleClickForFormSubmit) {
                      PanelUtil.disableBeforeReenableOnComplete(attributes, this);
                  }
              }
              @Override protected void onError(final AjaxRequestTarget target) {
                  target.add(getForm());
              }
          }
          : new AjaxButton(id, labelModel) {
              private static final long serialVersionUID = 1L;
              @Override public void onSubmit(final AjaxRequestTarget target) {
                  onClick.accept(this, target);
              }
              @Override protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) {
                  if (isPreventDoubleClickForFormSubmit) {
                      PanelUtil.disableBeforeReenableOnComplete(attributes, this);
                  }
              }
              @Override protected void onError(final AjaxRequestTarget target) {
                  target.add(getForm());
              }
          };
      }
  
      public AjaxButton buttonAdd(
              final MarkupContainer markupContainer,
              final String id,
              final IModel<String> labelModel,
              final SerializableBiConsumer<AjaxButton, AjaxRequestTarget> onClick) {
          return add(markupContainer, button(id, labelModel, onClick));
      }
  
      public AjaxButton buttonAddOk(
              final MarkupContainer markupContainer,
              final String id,
              final IModel<String> labelModel,
              final Wicket settings,
              final SerializableBiConsumer<AjaxButton, AjaxRequestTarget> onClick) {
          return add(markupContainer, buttonOk(id, labelModel, settings, onClick));
      }
  
      // -- CHECKBOX
  
      /**
       * In correspondence with ScalarPanelFormFieldAbstract.html
       * <wicket:fragment wicket:id="fragment-prompt-checkboxYes">
       */
      final String fragment_prompt_checkboxYes = "<label class=\"fs-4\" style=\"color: green;\">"
              + "<i class=\"fa-regular fa-check-square\"></i></label>";
      /**
       * In correspondence with ScalarPanelFormFieldAbstract.html
       * <wicket:fragment wicket:id="fragment-prompt-checkboxNo">
       */
      final String fragment_prompt_checkboxNo = "<label class=\"fs-4\">"
              + "<i class=\"fa-regular fa-square\"></i></label>";
      /**
       * In correspondence with ScalarPanelFormFieldAbstract.html
       * <wicket:fragment wicket:id="fragment-prompt-checkboxIntermediate">
       */
      final String fragment_prompt_checkboxIntermediate = "<label class=\"fs-4\" style=\"color: silver;\">"
              + "<i class=\"fa-regular fa-square-minus\"></i></label>";
  
      public static CheckBoxX checkboxX(
              final String id,
              final IModel<Boolean> checkedModel,
              final boolean required,
              final Sizes size) {
  
           final CheckBoxXConfig config = new CheckBoxXConfig() {
              private static final long serialVersionUID = 1L;
              {
                  // so can tab to the checkbox
                  // not part of the API, so have to use this object initializer
                  put(new Key<String>("tabindex"), "0");
              }
          }
          .withSize(size)
          .withEnclosedLabel(false)
          .withIconChecked(fragment_prompt_checkboxYes)
          .withIconNull(fragment_prompt_checkboxIntermediate)
          .withIconUnchecked(fragment_prompt_checkboxNo)
          .withThreeState(!required);
  
          final CheckBoxX checkBox = new CheckBoxX(id, checkedModel) {
              private static final long serialVersionUID = 1L;
              @Override public CheckBoxXConfig getConfig() {
                  return config;
              }
              //override to don't express FontAwesome twice, we already do that for all pages
              @Override public void renderHead(final IHeaderResponse response) {
                  response.render(CssHeaderItem.forReference(new CssResourceReference(CheckBoxX.class, "css/checkbox-x.css")));
                  response.render(JavaScriptHeaderItem.forReference(new JQueryPluginResourceReference(CheckBoxX.class, "js/checkbox-x.js")));
                  response.render(OnDomReadyHeaderItem.forScript($(this).chain("checkboxX", getConfig()).get()));
              }
              @Override protected void onComponentTag(final ComponentTag tag) {
                  super.onComponentTag(tag);
                  //
                  // this is a horrid hack to allow the space bar to work as a way of toggling the checkbox.
                  // this hack works for 1.5.4 of the JS plugin (https://github.com/kartik-v/bootstrap-checkbox-x)
                  //
                  // the problem is that the "change" event is not fired for a keystroke; instead the callback in the
                  // JS code (https://github.com/kartik-v/bootstrap-checkbox-x/blob/v1.5.4/js/checkbox-x.js#L70)
                  // calls self.change().  This in turn calls validateCheckbox().  In that method it is possible to
                  // cause the "change" event to fire, but only if the input element is NOT type="checkbox".
                  // (https://github.com/kartik-v/bootstrap-checkbox-x/blob/v1.5.4/js/checkbox-x.js#L132)
                  //
                  // It's not possible to simply change the associated markup to input type='xx' because it falls foul
                  // of a check in super.onComponentTag(tag).  So instead we let that through then hack the tag
                  // afterwards:
                  //
                  tag.put("type", "xx");
              }
          };
          return ajaxEnable(checkBox);
      }
  
      public static AjaxCheckBox checkbox(
              final String id,
              final IModel<Boolean> checkedModel,
              final SerializableConsumer<AjaxRequestTarget> onUpdate) {
          return new AjaxCheckBox(id, checkedModel) {
              private static final long serialVersionUID = 1L;
              @Override protected void onUpdate(final AjaxRequestTarget target) {
                  onUpdate.accept(target);
              }
              /**
               * [CAUSEWAY-3005] Any action dialog submission on the same page will
               * result in a new {@link DataTableInteractive}, where any previously rendered check-boxes
               * run out of sync with their DataRowToggle model.
               * Hence we intercept such events and reset check-boxes to un-checked.
               */
              @Override public void onEvent(final IEvent<?> event) {
                  if(event==null) return; // just in case
                  _Casts.castTo(CausewayEnvelopeEvent.class, event.getPayload())
                  .ifPresent(envelopeEvent->{
                      if(envelopeEvent.getLetter() instanceof CausewayActionCompletedEvent) {
                          var model = this.getModel();
                          if(hasMemoizedDataRow(model)
                                  && Boolean.TRUE.equals(model.getObject())) {
                              this.setModelObject(false);
                              envelopeEvent.getTarget().add(this);
                         }
                      }
                  });
                  super.onEvent(event);
              }
              /**
               * Whether it is safe (free of side-effects) to load/access given model's object.
               * <p>
               * As of [CAUSEWAY-3658], don't call
               * {@link org.apache.causeway.viewer.wicket.model.models.coll.DataRowWkt#getObject()},
               * when the model's object is not transiently already loaded, because otherwise it would
               * enforce a page-reload as side-effect.
               */
              private boolean hasMemoizedDataRow(final IModel<Boolean> model) {
                  if(model instanceof DataRowToggleWkt dataRowToggleWkt) {
                      if(dataRowToggleWkt.delegate() instanceof DataRowWkt dataRowWkt) {
                          return dataRowWkt.isTableDataLoaded();
                      }
                  }
                  return false;
              }
          };
      }
  
      public static AjaxCheckBox checkboxAdd(
              final MarkupContainer container,
              final String id,
              final IModel<Boolean> checkedModel,
              final SerializableConsumer<AjaxRequestTarget> onUpdate) {
          return add(container, checkbox(id, checkedModel, onUpdate));
      }
  
      // -- CONTAINER
  
      public WebMarkupContainer container(final String id) {
          final WebMarkupContainer component = new WebMarkupContainer(id);
          component.setOutputMarkupId(true);
          return component;
      }
  
      public WebMarkupContainer containerWithVisibility(
              final String id,
              final SerializableBooleanSupplier isVisible) {
          final WebMarkupContainer component = new WebMarkupContainer(id) {
              private static final long serialVersionUID = 1L;
              @Override public boolean isVisible() {
                  return isVisible.getAsBoolean();
              }
          };
          component.setOutputMarkupId(true);
          return component;
      }
  
      public WebMarkupContainer containerAdd(final MarkupContainer container, final String id) {
          return add(container, container(id));
      }
  
      // -- CSS
  
      /**
       * If {@code cssClass} is empty, does nothing.
       */
      public ComponentTag cssAppend(final ComponentTag tag, final @Nullable String cssClass) {
          if(_Strings.isNotEmpty(cssClass)) {
              tag.append("class", cssClass, " ");
          }
          return tag;
      }
  
      /**
       * If {@code cssClass} is empty, does nothing.
       */
      public <T extends Component> T cssAppend(final T component, final @Nullable String cssClass) {
          if(_Strings.isNotEmpty(cssClass)) {
              if(ButtonSemantics.appliesTo(cssClass)) {
                  component.add(ButtonSemantics.remover());
              }
              component.add(new CssClassNameAppender(cssClass));
          }
          return component;
      }
  
      public <T extends Component> T cssAppend(final T component, final @Nullable IModel<String> cssClassModel) {
          if(cssClassModel!=null) {
              cssAppend(component, cssClassModel.getObject());
          }
          return component;
      }
  
      public <T extends Component> T cssAppend(final T component, final Identifier identifier) {
          return cssAppend(component, cssNormalize(identifier));
      }
  
      public <T extends Component> T cssReplace(final T component, final @Nullable String cssClass) {
          component.add(AttributeModifier.replace("class", _Strings.nullToEmpty(cssClass)));
          return component;
      }
  
      public static String cssNormalize(final Identifier identifier) {
          var sb = new StringBuilder();
          sb.append("causeway-");
          sb.append(identifier.logicalType().logicalName());
          if(_Strings.isNullOrEmpty(identifier.memberLogicalName())) {
              sb.append("-");
              sb.append(identifier.memberLogicalName());
          }
          return cssNormalize(sb.toString());
      }
  
      public static String cssNormalize(final String cssClass) {
          var trimmed = _Strings.blankToNullOrTrim(cssClass);
          return _Strings.isNullOrEmpty(trimmed)
                  ? null
                  : cssClass.replaceAll("\\.", "-").replaceAll("[^A-Za-z0-9- ]", "").replaceAll("\\s+", "-");
      }
  
      // -- DOWNLOAD (RESOURCE LINK)
  
      public ResourceLinkVolatile downloadLinkNoCache(final String id, final IResource resourceModel) {
          return new ResourceLinkVolatile(id, resourceModel);
      }
  
      // -- DROPDOWN CHOICE
  
      public <T extends Serializable> DropDownChoice<T> dropDownChoice(final String id,
              final IModel<T> model,
              final List<? extends T> choices) {
          return new DropDownChoice<T>(id, model, choices);
      }
  
      public <T extends Serializable> DropDownChoice<T> dropDownChoiceWithAjaxUpdate(final String id,
              final IModel<T> model,
              final List<? extends T> choices) {
          var component = dropDownChoice(id, model, choices);
          component.setOutputMarkupId(true);
          component.add(new FormComponentUpdatingBehavior());
          return component;
      }
  
      public <T extends Serializable> DropDownChoice<T> dropDownChoiceWithAjaxUpdateAdd(
              final MarkupContainer container,
              final String id,
              final IModel<T> model,
              final List<? extends T> choices) {
          return add(container, dropDownChoiceWithAjaxUpdate(id, model, choices));
      }
  
      // -- FILE DOWNLOAD
  
      /**
       * Schedules a file download within the context of the current {@link RequestCycle}.
       * @param model - provides a (temporary) file located at the host, which is deleted after consumption
       */
      public void fileDownloadClickHandler(final IModel<File> model, final CommonMimeType mime, final String fileName) {
          final File file = model.getObject();
          if (file == null) {
              throw _Exceptions.illegalState("Failed to retrieve a File object from model %s", model.getClass().getName());
          }
          RequestCycle.get().scheduleRequestHandlerAfterCurrent(
                  Wkt.fileResourceStreamRequestHandler(file, mime)
                  .setFileName(fileName)
                  .setContentDisposition(ContentDisposition.ATTACHMENT));
      }
  
      private ResourceStreamRequestHandler fileResourceStreamRequestHandler(final File file, final CommonMimeType mime) {
          return new ResourceStreamRequestHandler(Wkt.fileResourceStream(file, mime)){
              @Override public void respond(final IRequestCycle requestCycle) {
                  super.respond(requestCycle);
                  Files.remove(file);
              }
          };
      }
  
      private FileResourceStream fileResourceStream(final File file, final CommonMimeType mime) {
          return new FileResourceStream(
                  new org.apache.wicket.util.file.File(file)) {
              private static final long serialVersionUID = 1L;
              @Override public String getContentType() {
                  return mime.getBaseType();
              }
          };
      }
  
      // -- FILE UPLOAD
  
      public FileUploadField fileUploadField(
              final String id,
              final String initialCaption,
              final IModel<List<FileUpload>> model) {
          var fileUploadField = new FileUploadFieldWithNestingFix(
                  id,
                  model,
                  new FileInputConfig()
                      .maxFileCount(1)
                      .mainClass("input-group-sm")
                      .initialCaption(initialCaption)
                      .captionClass("form-control-sm")
                      .showUpload(false));
          return fileUploadField;
      }
  
      // -- FONT AWESOME
  
      public String faIcon(final String faClasses) {
          return String.format("<i class=\"%s\"></i>", faClasses);
      }
  
      public MarkupComponent faIconLayers(
              final String id,
              final FontAwesomeLayers fontAwesomeLayers) {
          return markup(id, fontAwesomeLayers.toHtml());
      }
  
      public MarkupComponent faIconLayersAdd(final MarkupContainer container, final String id,
              final FontAwesomeLayers faLayers) {
          return add(container, faIconLayers(id, faLayers));
      }
  
      // -- FRAGMENT
  
      /**
       * @param id - The component id
       * @param fragmentId - The id of the associated markup fragment
       * @param markupProvider - The component whose markup contains the fragment's markup
       */
      public Fragment fragment(final String id, final String fragmentId, final MarkupContainer markupProvider) {
          return new Fragment(id, fragmentId, markupProvider);
      }
  
      public Fragment fragmentDebug(final String id, final String fragmentId, final MarkupContainer markupProvider) {
          return new Fragment(id, fragmentId, markupProvider) {
              private static final long serialVersionUID = 1L;
              @Override public MarkupContainer add(final Component... children) {
                  for(var child:children) {
                      System.err.printf("add %s -> %s %n", this.getId(), child.getId());
                  }
                  return super.add(children); }
              @Override public MarkupContainer addOrReplace(final Component... children) {
                  for(var child:children) {
                      System.err.printf("addOrReplace %s -> %s %n", this.getId(), child.getId());
                  }
                  return super.addOrReplace(children); }
          };
      }
  
      /**
       * @param id - The component id
       * @param fragmentId - The id of the associated markup fragment
       * @param markupProvider - The component whose markup contains the fragment's markup
       */
      public Fragment fragmentNoTab(final String id, final String fragmentId, final MarkupContainer markupProvider) {
          return new Fragment(id, fragmentId, markupProvider) {
              private static final long serialVersionUID = 1L;
              @Override protected void onComponentTag(final ComponentTag tag) {
                  super.onComponentTag(tag);
                  tag.put("tabindex", "-1");}};
      }
  
      /**
       * @param container - The component to add the fragment to
       * @param id - The component id
       * @param fragmentId - The id of the associated markup fragment
       * @param markupProvider - The component whose markup contains the fragment's markup
       */
      public Fragment fragmentAdd(final MarkupContainer container,
              final String id, final String fragmentId, final MarkupContainer markupProvider) {
          return add(container, fragment(id, fragmentId, markupProvider));
      }
  
      /**
       * @param container - The component to add the fragment to
       * @param id - The component id
       * @param fragmentId - The id of the associated markup fragment
       * @param markupProvider - The component whose markup contains the fragment's markup
       */
      public Fragment fragmentAddNoTab(final MarkupContainer container,
              final String id, final String fragmentId, final MarkupContainer markupProvider) {
          return add(container, fragmentNoTab(id, fragmentId, markupProvider));
      }
  
      // -- FORM
  
      public static Form<Object> form(final String id) {
          return new Form<Object>(id);
      }
  
      public static Form<Object> formAdd(final MarkupContainer container, final String id) {
          return add(container, form(id));
      }
  
      // -- IMAGE
  
      public Image imageNonCaching(final String id, final BufferedImage buffImg) {
          var imageResource = new BufferedDynamicImageResource();
          imageResource.setImage(buffImg);
  
          var thumbnailImageResource = new ThumbnailImageResource(imageResource, 300);
  
          var wicketImage = new NonCachingImage(id, thumbnailImageResource);
          wicketImage.setOutputMarkupId(true);
          return wicketImage;
      }
  
      public Image imageCachable(final String id, final ResourceReference imageResource) {
          return new Image(id, imageResource) {
              private static final long serialVersionUID = 1L;
              @Override protected boolean shouldAddAntiCacheParameter() { return false; }
          };
      }
  
      public Image imageAddCachable(final MarkupContainer container, final String id, final ResourceReference imageResource) {
          return add(container, imageCachable(id, imageResource));
      }
  
      // -- LABEL
  
      public Label label(final String id, final String label) {
          return new Label(id, label);
      }
  
      public Label label(final String id, final IModel<String> labelModel) {
          return new Label(id, labelModel);
      }
  
      /**
       *  Whether to escape the underlying String for rendering
       *  is dynamically based on the provided {@link UiString}.
       */
      public Label labelWithDynamicEscaping(final String id, final SerializableSupplier<UiString> labelModel) {
          var label = new Label(id, labelModel.get()::string) {
              private static final long serialVersionUID = 1L;
              // we are using this method as a hook to update the ESCAPE flag before rendering
              @Override public <C> IConverter<C> getConverter(final Class<C> type) {
                  setEscapeModelStrings(!labelModel.get().isMarkup());
                  return super.getConverter(type);
              }
          };
          return label;
      }
  
      public Label labelNoTab(final String id, final IModel<String> labelModel) {
          return new Label(id, labelModel) {
              private static final long serialVersionUID = 1L;
              @Override protected void onComponentTag(final ComponentTag tag) {
                  super.onComponentTag(tag);
                  tag.put("tabindex", "-1");
              }
          };
      }
  
      public <T> Label labelWithConverter(
              final String id, final IModel<T> model, final Class<T> type, final IConverter<T> converter) {
          return new Label(id, model) {
              private static final long serialVersionUID = 1L;
              @SuppressWarnings("unchecked")
              @Override public <C> IConverter<C> getConverter(final Class<C> cType) {
                  return cType == type
                          ? (IConverter<C>) converter
                          : super.getConverter(cType);}
          };
      }
  
      public Label labelAdd(final MarkupContainer container, final String id, final String label) {
          return add(container, label(id, label));
      }
  
      public Label labelAdd(final MarkupContainer container, final String id, final IModel<String> labelModel) {
          return add(container, new Label(id, labelModel));
      }
  
      public Label labelAddNoTab(final MarkupContainer container, final String id, final IModel<String> labelModel) {
          return add(container, labelNoTab(id, labelModel));
      }
  
      public <T> Label labelAddWithConverter(
              final MarkupContainer container,
              final String id, final IModel<T> model, final Class<T> type, final IConverter<T> converter) {
          return add(container, labelWithConverter(id, model, type, converter));
      }
  
      // -- LINK
  
      public ResourceLink<Void> link(final String id, final BufferedImage buffImg) {
          var imageResource = new BufferedDynamicImageResource();
          imageResource.setImage(buffImg);
          return new ResourceLink<Void>(id, imageResource);
      }
  
      public AjaxLinkNoPropagate link(final String id, final SerializableConsumer<AjaxRequestTarget> onClick) {
          return new AjaxLinkNoPropagate(id, onClick);
      }
  
      public AjaxLinkNoPropagate linkAdd(
              final MarkupContainer container,
              final String id,
              final SerializableConsumer<AjaxRequestTarget> onClick) {
          return add(container, link(id, onClick));
      }
  
      /** renders plain HTML; useful in combination with font-awesome icons */
      public AjaxLinkNoPropagate linkWithBody(final String id, final String bodyHtml,
              final SerializableConsumer<AjaxRequestTarget> onClick) {
          return new AjaxLinkNoPropagate(id, onClick) {
              private static final long serialVersionUID = 1L;
              @Override public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag){
                  replaceComponentTagBody(markupStream, openTag, bodyHtml); }
              @Override protected void onComponentTag(final ComponentTag tag)   {
                  super.onComponentTag(tag);
                  tag.setType(TagType.OPEN); }
          };
      }
  
      /** renders plain HTML; useful in combination with font-awesome icons */
      public AjaxLinkNoPropagate linkAddWithBody(
              final MarkupContainer container,
              final String id, final String bodyHtml,
              final SerializableConsumer<AjaxRequestTarget> onClick) {
          return add(container, linkWithBody(id, bodyHtml, onClick));
      }
  
      /** renders plain HTML; useful in combination with font-awesome icons */
      public AjaxLinkNoPropagate linkAddWithBody(final RepeatingView rv, final String bodyHtml,
              final SerializableConsumer<AjaxRequestTarget> onClick) {
          return linkAddWithBody(rv, rv.newChildId(), bodyHtml, onClick);
      }
  
      // -- LIST VIEW
  
      public <T> ListView<T> listView(
              final String id,
              final List<T> list,
              final SerializableConsumer<ListItem<T>> itemPopulator) {
          return new ListView<T>(id, list) {
              private static final long serialVersionUID = 1L;
              @Override protected void populateItem(final ListItem<T> item) {
                  itemPopulator.accept(item);
              }
          };
      }
  
      public <T> ListView<T> listView(
              final String id,
              final IModel<? extends List<T>> listModel,
              final SerializableConsumer<ListItem<T>> itemPopulator) {
          return new ListView<T>(id, listModel) {
              private static final long serialVersionUID = 1L;
              @Override protected void populateItem(final ListItem<T> item) {
                  itemPopulator.accept(item);
              }
          };
      }
  
      public <T> ListView<T> listViewAdd(
              final MarkupContainer container,
              final String id,
              final List<T> list,
              final SerializableConsumer<ListItem<T>> itemPopulator) {
          return add(container, listView(id, list, itemPopulator));
      }
  
      public <T> ListView<T> listViewAdd(
              final MarkupContainer container,
              final String id,
              final IModel<? extends List<T>> listModel,
                      final SerializableConsumer<ListItem<T>> itemPopulator) {
          return add(container, listView(id, listModel, itemPopulator));
      }
  
      // -- MARKUP
  
      public MarkupComponent markup(final String id, final IModel<String> htmlModel) {
          return new MarkupComponent(id, htmlModel);
      }
  
      public MarkupComponent markup(final String id, final String html) {
          return markup(id, Model.of(html));
      }
  
      public MarkupComponent markupAdd(final MarkupContainer container, final String id, final IModel<String> htmlModel) {
          return add(container, markup(id, htmlModel));
      }
  
      public MarkupComponent markupAdd(final MarkupContainer container, final String id, final String html) {
          return add(container, markup(id, html));
      }
  
      // -- REPEATING VIEW
  
      public RepeatingView repeatingView(final String id) {
          return new RepeatingView(id);
      }
  
      public RepeatingView repeatingViewAdd(final MarkupContainer container, final String id) {
          return add(container, repeatingView(id));
      }
  
      public <T> RepeatingView repeatingViewAdd(
              final MarkupContainer container,
              final String id,
              final Stream<T> elementStream,
              final BiConsumer<WebMarkupContainer, T> itemPopulator) {
          var repeatingView = add(container, repeatingView(id));
          elementStream
          .forEach(t->{
              var innerContainer = Wkt.container(repeatingView.newChildId());
              repeatingView.add(innerContainer);
              itemPopulator.accept(innerContainer, t);
          });
          return repeatingView;
      }
  
      // -- TABLES
  
      public <T> Item<T> oddEvenItem(
              final String id, final int index, final IModel<T> model,
              final SerializableFunction<T, String> cssClassProvider) {
  
          return new OddEvenItem<T>(id, index, model) {
              private static final long serialVersionUID = 1L;
  
              @Override
              protected void onComponentTag(final ComponentTag tag) {
                  super.onComponentTag(tag);
                  Wkt.cssAppend(tag, cssClassProvider.apply(model.getObject()));
              }
          };
      }
  
      // -- TEXT AREA
  
      public TextArea<String> textArea(final String id, final IModel<String> textModel) {
          return new TextArea<String>(id, textModel);
      }
  
      public TextArea<String> textAreaNoTab(final String id, final IModel<String> textModel) {
          return new TextArea<String>(id, textModel) {
              private static final long serialVersionUID = 1L;
              @Override protected void onComponentTag(final ComponentTag tag) {
                  super.onComponentTag(tag);
                  tag.put("tabindex", "-1");
              }
          };
      }
  
  //    public TextArea<String> textAreaAddNoTab(
  //            final MarkupContainer container, final String id, final IModel<String> textModel) {
  //        return add(container, textAreaNoTab(id, textModel));
  //    }
  
      /**
       * @param converter - if {@code Optional.empty()} returns {@link TextArea} using Wicket's default converters.
       */
      public <T> TextArea<T> textAreaWithConverter(
              final @NonNull String id,
              final @NonNull IModel<T> model,
              final @NonNull Class<T> type,
              final @NonNull Optional<IConverter<T>> converter) {
          return new TextAreaWithConverter<T>(id, model, type, converter);
      }
  
      // -- TEXT FIELD
  
      /**
       * @param converter - if {@code Optional.empty()} returns {@link TextField} using Wicket's default converters.
       */
      public <T> TextField<T> textFieldWithConverter(
              final @NonNull String id,
              final @NonNull IModel<T> model,
              final @NonNull Class<T> type,
              final @NonNull Optional<IConverter<T>> converter) {
          return new TextFieldWithConverter<T>(id, model, type, converter);
      }
  
      public <T> TextField<T> passwordFieldWithConverter(
              final @NonNull String id,
              final @NonNull IModel<T> model,
              final @NonNull Class<T> type,
              final @NonNull Optional<IConverter<T>> converter) {
          return new TextFieldWithConverter<T>(id, model, type, converter) {
              private static final long serialVersionUID = 2L;
              @Override protected void onComponentTag(final ComponentTag tag) {
                  Attributes.set(tag, "type", "password");
                  super.onComponentTag(tag);
              }
              @Override protected String[] getInputTypes() {
                  return new String[] {"password"};
              }
          };
  
      }
  
      // -- FOCUS UTILITY
  
      /**
       * If the container has any child with the marker attribute {@code data-causeway-focus},
       * then the first one found will receive focus (in the browser).
       * @implNote HTML allows for custom attributes with naming convention {@code data-}.
       */
      public void focusOnMarkerAttribute(
              final MarkupContainer container,
              final AjaxRequestTarget target) {
  
          container.streamChildren()
          .filter(child->child.getMarkupAttributes().containsKey("data-causeway-focus"))
          .findFirst()
          .ifPresent(child->{
              target.focusComponent(child);
          });
  
      }
  
      // -- JAVA SCRIPT UTILITY
  
      public enum EventTopic {
          FOCUS_FIRST_PROPERTY,
          FOCUS_FIRST_PARAMETER,
          OPEN_SELECT2,
          //CLOSE_SELECT2,
      }
  
      public void javaScriptAdd(final AjaxRequestTarget target, final EventTopic topic, final String containerId) {
          target.appendJavaScript(javaScriptFor(topic, containerId));
      }
  
      public void javaScriptAdd(final IHeaderResponse response, final EventTopic topic, final String containerId) {
          response.render(OnDomReadyHeaderItem.forScript(javaScriptFor(topic, containerId)));
      }
  
      private String javaScriptFor(final EventTopic topic, final String containerId) {
          return _Strings.isNotEmpty(containerId)
                  ? String.format("Wicket.Event.publish(Causeway.Topic.%s, '%s')", topic.name(), containerId)
                  : String.format("Wicket.Event.publish(Causeway.Topic.%s)", topic.name());
      }
  
      public OnDomReadyHeaderItem javaScriptAsOnDomReadyHeaderItem(final String javaScript) {
          return OnDomReadyHeaderItem.forScript(javaScript);
      }
  
      // -- TABBING UTILITY
  
      public Component noTabbing(final @Nullable Component component) {
          if(component != null) {
              component.add(new AttributeAppender("tabindex", "-1"));
          }
          return component;
      }
  
      // -- ERROR MESSAGE UTILITY
  
      /**
       * Reports a validation error against given form component.
       * Uses plain error message from ConversionException, circumventing resource bundles.
       * @return whether handled
       */
      public boolean errorMessageIgnoringResourceBundles(
              final @Nullable FormComponent<?> formComponent,
              final @Nullable IValidationError error) {
          if(formComponent==null
                  || error==null) {
              return true;
          }
          if(error instanceof ValidationError) {
              var message = ((ValidationError)error).getMessage();
              // use plain error message from ConversionException, circumventing resource bundles.
              if(_Strings.isNotEmpty(message)) {
                  formComponent.error(message);
                  return true; // handled
              }
          }
          return false; // not-handled
      }
  
      // -- FORM COMPONENT ATTRIBUTE UTILITY
  
      public void setFormComponentAttributes(
              final FormComponent<?> formComponent,
              final Supplier<OptionalInt> multilineNumberOfLines,
              final Supplier<OptionalInt> maxLength,
              final Supplier<OptionalInt> typicalLength) {
  
          if(formComponent instanceof TextArea) {
              multilineNumberOfLines.get()
              .ifPresent(numberOfLines->
                  Wkt.attributeReplace(formComponent, "rows", numberOfLines));
          }
  
          maxLength.get()
          .ifPresent(maxLen->{
              // for TextArea in conjunction with javascript in CausewayWicketViewerJsResourceReference
              // see http://stackoverflow.com/questions/4459610/set-maxlength-in-html-textarea
  
              Wkt.attributeReplace(formComponent, "maxlength", maxLen);
              if(formComponent.getType().equals(String.class)) {
                  formComponent.add(StringValidator.maximumLength(maxLen));
              }
          });
  
          typicalLength.get()
          .ifPresent(typicalLen->
              Wkt.attributeReplace(formComponent, "size", typicalLen));
  
      }
  
      // -- DISABLE WORKAROUND
  
      /**
       * MOVED over from Wicket 8 - potentially no longer required
       * <p>
       * HACK issue #79: wicket changes tag name if component wasn't enabled
       *
       * @param component the component to fix
       * @param tag       the component tag
       * @deprecated since Wicket 7.0: doesn't mangle the link/button's markup anymore
       */
      @Deprecated
      public void fixDisabledState(final Component component, final ComponentTag tag) {
          if (!component.isEnabledInHierarchy()) {
              if (component instanceof AbstractLink) {
                  tag.setName("a");
              } else if (component instanceof Button) {
                  tag.setName("button");
              } else {
                  if (tag.getAttribute("value") != null) {
                      tag.setName("input");
                  } else {
                      tag.setName("button");
                  }
              }
              tag.put("disabled", "disabled");
          }
      }
  
  }
  