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.components.widgets.select2;
  
  import java.io.Serializable;
  import java.util.Collection;
  import java.util.stream.Collectors;
  
  import org.apache.wicket.util.string.Strings;
  import org.jspecify.annotations.Nullable;
  import org.wicketstuff.select2.Response;
  
  import org.apache.causeway.applib.services.i18n.TranslationContext;
  import org.apache.causeway.applib.services.placeholder.PlaceholderRenderService.PlaceholderLiteral;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.objectmanager.memento.ObjectMemento;
  import org.apache.causeway.core.metamodel.util.Facets;
  import org.apache.causeway.viewer.commons.model.attrib.UiAttribute;
  import org.apache.causeway.viewer.commons.model.attrib.UiParameter;
  import org.apache.causeway.viewer.wicket.model.models.UiAttributeWkt;
  
  record ChoiceProvider(
      UiAttributeWkt attributeModel,
      UiAttribute.ChoiceProviderSort choiceProviderSort)
  implements HasMetaModelContext, Serializable {
  
      public ChoiceProvider(
              final UiAttributeWkt attributeModel) {
          this(attributeModel, UiAttribute.ChoiceProviderSort.valueOf(attributeModel));
      }
  
      /**
       * Get the value for displaying to an end user.
       */
      public String getDisplayValue(final ObjectMemento choiceMemento) {
          if (choiceMemento == null
                  || choiceMemento.isEmpty()) {
              return getPlaceholderRenderService().asText(PlaceholderLiteral.NULL_REPRESENTATION);
          }
          return translate(choiceMemento.title());
      }
  
      /**
       * This method is called to get the id value of an object (used as the value attribute of a
       * choice element) The id can be extracted from the object like a primary key, or if the list is
       * stable you could just return a toString of the index.
       * <p>
       * Note that the given index can be {@code -1} if the object in question is not contained in the
       * available choices.
       */
      public String getIdValue(final ObjectMemento choiceMemento) {
          if (choiceMemento == null) return ObjectMemento.NULL_ID;
  
          return ObjectMemento.enstringToUrlBase64(choiceMemento);
      }
  
      /**
       * Queries application for choices that match the search {@code term} and adds them to the
       * {@code response}
       */
      public void query(
              final String term,
              final int page,
              final org.wicketstuff.select2.Response<ObjectMemento> response) {
  
          var mementosFiltered = query(term);
  
          if(isRequired()) {
              response.addAll(mementosFiltered.toList());
              return;
          }
  
          // else, if not mandatory, prepend null
          var mementosIncludingNull = mementosFiltered.toArrayList();
          mementosIncludingNull.add(0, null);
  
          response.addAll(mementosIncludingNull);
      }
  
      /**
       * Converts a list of choice ids back into application's choice objects. When the choice
       * provider is attached to a single-select component the {@code ids} collection will contain
       * exactly one id, and a collection containing exactly one choice should be returned.
       */
      public Collection<ObjectMemento> toChoices(final Collection<String> ids) {
          return _NullSafe.stream(ids)
                  .map(this::mementoFromIdWithNullHandling)
                  .collect(Collectors.toList());
      }
  
      // -- UTIL
  
      /** adapter method */
      org.wicketstuff.select2.ChoiceProvider<ObjectMemento> toSelect2ChoiceProvider() {
          var delegate = this;
          return new org.wicketstuff.select2.ChoiceProvider<ObjectMemento>() {
              private static final long serialVersionUID = 1L;
  
              @Override public Collection<ObjectMemento> toChoices(final Collection<String> ids) {
                  return delegate.toChoices(ids);
              }
              @Override public void query(final String term, final int page, final Response<ObjectMemento> response) {
                  delegate.query(term, page, response);
              }
              @Override public String getIdValue(final ObjectMemento object) {
                  return delegate.getIdValue(object);
              }
              @Override public String getDisplayValue(final ObjectMemento object) {
                  return delegate.getDisplayValue(object);
              }
          };
      }
  
      // -- HELPER
  
      private Can<ObjectMemento> queryAll() {
          return attributeModel().getChoices() // must not return detached entities
                  .map(ManagedObject::getMementoElseFail);
      }
  
      private Can<ObjectMemento> queryWithAutoCompleteUsingObjectSpecification(final String term) {
          var autoCompleteAdapters = Facets
                  .autoCompleteExecute(attributeModel().getElementType(), term);
          return autoCompleteAdapters
                  .map(ManagedObject::getMementoElseFail);
      }
  
      private Can<ObjectMemento> queryWithAutoComplete(final String term) {
          var attributeModel = attributeModel();
          var pendingArgs = attributeModel.isParameter()
                  ? ((UiParameter)attributeModel).getParameterNegotiationModel().getParamValues()
                  : Can.<ManagedObject>empty();
          var pendingArgMementos = pendingArgs
                  .map(ManagedObject::getMementoElseFail);
  
          if(attributeModel.isParameter()) {
              // recover any pendingArgs
              var paramModel = (UiParameter)attributeModel;
  
              paramModel
                  .getParameterNegotiationModel()
                  .setParamValues(
                          reconstructPendingArgs(paramModel, pendingArgMementos));
          }
  
          return attributeModel
                  .getAutoComplete(term)
                  .map(ManagedObject::getMementoElseFail);
      }
  
      private Can<ManagedObject> reconstructPendingArgs(
              final UiParameter parameterModel,
              final Can<ObjectMemento> pendingArgMementos) {
          var pendingArgsList = _NullSafe.stream(pendingArgMementos)
              .map(getObjectManager()::demementify)
              .collect(Can.toCan());
  
         return pendingArgsList;
      }
  
      private @Nullable ObjectMemento mementoFromIdWithNullHandling(final String id) {
          if(ObjectMemento.NULL_ID.equals(id)) return null;
  
          return mementoFromId(id);
      }
  
      /**
       * Whether to not prepend <code>null</code> as choice candidate.
       */
      private boolean isRequired() {
          return attributeModel().isRequired();
      }
  
      /**
       * Get choice candidates with filtering (don't include <code>null</code>).
       */
      private Can<ObjectMemento> query(final String term) {
          return switch(choiceProviderSort) {
              case CHOICES->filter(term, queryAll());
              case AUTO_COMPLETE->queryWithAutoComplete(term);
              case OBJECT_AUTO_COMPLETE->queryWithAutoCompleteUsingObjectSpecification(term);
              case NO_CHOICES->Can.empty();
          };
      }
  
      /**
       * Filters all choices against a term by using their
       * {@link ManagedObject#getTitle() title string}
       *
       * @param term The term entered by the user
       * @param choiceMementos The collections of choices to filter
       * @return A list of all matching choices
       */
      private Can<ObjectMemento> filter(
              final String term,
              final Can<ObjectMemento> choiceMementos) {
  
          if (Strings.isEmpty(term)) return choiceMementos;
  
          var translationContext = TranslationContext.empty();
          var translator = getTranslationService();
          var termLower = term.toLowerCase();
  
          return choiceMementos.filter((final ObjectMemento candidateMemento)->{
              var title = translator.translate(translationContext, candidateMemento.title());
              return title.toLowerCase().contains(termLower);
          });
      }
  
      private @Nullable ObjectMemento mementoFromId(final @Nullable String id) {
          return ObjectMemento.destringFromUrlBase64(id);
      }
  
  }
  