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.core.metamodel.postprocessors.param;
  
  import jakarta.inject.Inject;
  
  import org.apache.causeway.applib.annotation.Where;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.core.config.progmodel.ProgrammingModelConstants;
  import org.apache.causeway.core.metamodel.commons.MetaModelVisitor;
  import org.apache.causeway.core.metamodel.context.MetaModelContext;
  import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
  import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet;
  import org.apache.causeway.core.metamodel.facets.object.defaults.DefaultedFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.choices.ChoicesFacet;
  import org.apache.causeway.core.metamodel.facets.param.autocomplete.ActionParameterAutoCompleteFacet;
  import org.apache.causeway.core.metamodel.facets.param.autocomplete.ActionParameterAutoCompleteFacetFromElementType;
  import org.apache.causeway.core.metamodel.facets.param.choices.ActionParameterChoicesFacet;
  import org.apache.causeway.core.metamodel.facets.param.choices.ActionParameterChoicesFacetFromAction;
  import org.apache.causeway.core.metamodel.facets.param.choices.ActionParameterChoicesFacetFromElementType;
  import org.apache.causeway.core.metamodel.facets.param.defaults.ActionParameterDefaultsFacet;
  import org.apache.causeway.core.metamodel.facets.properties.autocomplete.PropertyAutoCompleteFacet;
  import org.apache.causeway.core.metamodel.facets.properties.choices.PropertyChoicesFacet;
  import org.apache.causeway.core.metamodel.facets.properties.choices.enums.PropertyChoicesFacetFromChoicesFacet;
  import org.apache.causeway.core.metamodel.facets.properties.defaults.PropertyDefaultFacet;
  import org.apache.causeway.core.metamodel.facets.properties.defaults.fromtype.PropertyDefaultFacetFromDefaultedFacet;
  import org.apache.causeway.core.metamodel.postprocessors.MetaModelPostProcessorAbstract;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
  import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
  import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
  import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
  import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
  import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;
  import org.apache.causeway.core.metamodel.util.Facets;
  
  /**
   * Does post-processing of
   * {@link ActionParameterDefaultsFacet} and {@link ActionParameterChoicesFacet},
   * as well as
   * {@link PropertyDefaultFacet} and {@link PropertyChoicesFacet}.
   */
  public class ChoicesAndDefaultsPostProcessor
  extends MetaModelPostProcessorAbstract {
  
      @Inject
      public ChoicesAndDefaultsPostProcessor(final MetaModelContext metaModelContext) {
          super(metaModelContext, SKIP_MIXINS);
      }
  
      @Override
      public void postProcessParameter(
              final ObjectSpecification objectSpecification,
              final ObjectAction objectAction,
              final ObjectActionParameter param) {
  
          // no need to perform this check if the action is always hidden in the UI
          // (one use case being if the action exists solely to be called via the WrapperFactory service,
          //  eg to emit an outbox event for integration)
          if (Facets.hiddenWhere(objectAction)
                    .filter(where -> where == Where.EVERYWHERE)
                    .isPresent()) {
              return;
          }
  
          if(!hasChoicesOrAutoComplete(param)) {
  
              if(FacetUtil
                      .addFacetIfPresent(
                          ActionParameterChoicesFacetFromAction
                              .create(objectAction, objectSpecification, param))
                      .isPresent()) {
  
                  /* ActionParameterChoicesFacetFromAction has precedence over
                   * ActionParameterChoicesFacetFromElementType, so stop processing here.
                   * (also skips validation below) */
                  return;
              }
  
              if(FacetUtil
                      .addFacetIfPresent(
                          ActionParameterChoicesFacetFromElementType
                              .create(param))
                      .isPresent()) {
  
                  /* ActionParameterChoicesFacetFromElementType has precedence over
                   * ActionParameterAutoCompleteFacetFromElementType, so stop processing here.
                   * (also skips validation below) */
                  return;
              }
  
              if(FacetUtil
                      .addFacetIfPresent(
                              ActionParameterAutoCompleteFacetFromElementType
                                  .create(param))
                      .isPresent()) {
  
                  /* skips validation below */
                  return;
              }
  
          }
  
          if(MetaModelVisitor.SKIP_ABSTRACT.test(objectSpecification)) {
              checkParamHasChoicesOrAutoCompleteWhenRequired(param);
              checkActionParameterHasSupporedCollectionTypeWhenPlural(objectSpecification, objectAction, param);
          }
  
      }
  
      @Override
      public void postProcessProperty(
              final ObjectSpecification objectSpecification,
              final OneToOneAssociation prop) {
  
          if(!hasMemberLevelDefaults(prop)) {
              prop.getElementType()
              .lookupNonFallbackFacet(DefaultedFacet.class)
              .ifPresent(specFacet -> FacetUtil.addFacet(new PropertyDefaultFacetFromDefaultedFacet(
                                          specFacet, facetedMethodFor(prop))));
          }
          if(!hasChoicesOrAutoComplete(prop)) {
  
              var choicesFacetIfAny = prop.getElementType()
                      .lookupNonFallbackFacet(ChoicesFacet.class);
  
              FacetUtil.addFacetIfPresent(
                      PropertyChoicesFacetFromChoicesFacet
                      .create(choicesFacetIfAny, facetedMethodFor(prop)));
          }
      }
  
      @Override
      public void postProcessCollection(
              final ObjectSpecification objectSpecification,
              final OneToManyAssociation coll) {
  
          // ... see if any of its actions has a collection parameter of the same type
          //
          // eg Order#getItems() and Order#removeItems(List<OrderItem>)
          //
  
          // setting up filters ...
          var elementType = coll.getElementType();
          var whetherCollectionParamOfType =
                  new ObjectActionParameter.Predicates.CollectionParameter(elementType);
          var whetherScalarParamOfType =
                  new ObjectActionParameter.Predicates.ScalarParameter(elementType);
  
          // processing actions ...
          objectSpecification.streamRuntimeActions(MixedIn.INCLUDED)
          .filter(ObjectAction.Predicates.choicesFromAndHavingCollectionParameterFor(coll))
          .forEach(action->{
  
              var parameters = action.getParameters();
  
              var compatibleCollectionParams = parameters.filter(whetherCollectionParamOfType);
              var compatibleScalarParams = parameters.filter(whetherScalarParamOfType);
  
              // for collection parameters, install a defaults facet (if there isn't one already)
              // this will cause the UI to render the collection with toggleboxes
              for (final ObjectActionParameter collectionParam : compatibleCollectionParams) {
                  addCollectionParamDefaultsFacetIfNoneAlready(collectionParam);
              }
  
              // for compatible collection parameters, install a choices facet (if there isn't one already)
              // using the associated collection for its values
              for (final ObjectActionParameter collectionParam : compatibleCollectionParams) {
                  addCollectionParamChoicesFacetIfNoneAlready(coll, collectionParam);
              }
  
              // similarly for compatible scalar parameters, install a choices facet (if there isn't one already)
              // using the associated collection for its values.
              for (final ObjectActionParameter scalarParam : compatibleScalarParams) {
                  addCollectionParamChoicesFacetIfNoneAlready(coll, scalarParam);
              }
          });
      }
  
      // -- HELPER
  
      private static boolean hasMemberLevelDefaults(final ObjectActionParameter param) {
          return param.containsNonFallbackFacet(ActionParameterDefaultsFacet.class);
      }
  
      private static boolean hasMemberLevelDefaults(final OneToOneAssociation prop) {
          return prop.containsNonFallbackFacet(PropertyDefaultFacet.class);
      }
  
      private static boolean hasChoicesOrAutoComplete(final ObjectActionParameter param) {
          return param.containsNonFallbackFacet(ActionParameterChoicesFacet.class)
                  || param.containsNonFallbackFacet(ActionParameterAutoCompleteFacet.class);
      }
  
      private static boolean hasChoicesOrAutoComplete(final OneToOneAssociation prop) {
          return prop.containsNonFallbackFacet(PropertyChoicesFacet.class)
                  || prop.containsNonFallbackFacet(PropertyAutoCompleteFacet.class);
      }
  
      private static void addCollectionParamDefaultsFacetIfNoneAlready(
              final ObjectActionParameter collectionParam) {
          if(!hasMemberLevelDefaults(collectionParam)) {
              FacetUtil.addFacet(
                      ActionParameterDefaultsFacetFromAssociatedCollection
                      .create(collectionParam));
          }
      }
  
      private static void addCollectionParamChoicesFacetIfNoneAlready(
              final OneToManyAssociation coll,
              final ObjectActionParameter param) {
          if(!hasChoicesOrAutoComplete(param)) {
              FacetUtil.addFacet(
                      new ActionParameterChoicesFacetFromParentedCollection(param, coll));
          }
      }
  
      private void checkParamHasChoicesOrAutoCompleteWhenRequired(final ObjectActionParameter param) {
          var elementType = param.getElementType();
          if(elementType == null
                  || !elementType.getBeanSort().policy().isAllowedAsMemberElementType()) {
              // ignore, as these cases are covered later by meta-model validation
              return;
          }
          if(elementType.isEntityOrViewModel()
                  || param.isPlural()) {
              if(!hasChoicesOrAutoComplete(param)) {
  
                  ValidationFailure.raiseFormatted(param,
                          ProgrammingModelConstants.MessageTemplate.PARAMETER_HAS_NO_CHOICES_NOR_AUTOCOMPLETE.builder()
                              .addVariable("paramId", param.getFeatureIdentifier().toString())
                              .buildMessage());
              }
          }
      }
  
      private static void checkActionParameterHasSupporedCollectionTypeWhenPlural(
              final ObjectSpecification objectSpec,
              final ObjectAction objectAction,
              final ObjectActionParameter param) {
  
          if(param.isSingular()) return;
  
          param.lookupFacet(TypeOfFacet.class)
          .ifPresentOrElse(typeOfFacet->{
  
              // Violation if there are action parameter types that are assignable
              // from java.util.Collection but are not of
              // exact type List, Set, SortedSet or Collection.
              if(!typeOfFacet.value().isSupportedForActionParameter()) {
  
                  ValidationFailure.raiseFormatted(param,
                          ProgrammingModelConstants.MessageTemplate.PARAMETER_HAS_UNSUPPORTED_COLLECTION_TYPE.builder()
                              .addVariable("type", objectSpec.getFullIdentifier())
                              .addVariable("action", objectAction.getId())
                              .addVariable("paramIndex", param.getParameterIndex())
                              .buildMessage());
              }
  
          },()->{
  
              var messageFormat = "framework bug: plural action parameter found,"
                      + " that has no TypeOfFacet"
                      + "Class: %s action: %s parameter %d";
  
              throw _Exceptions.unrecoverable(
                              messageFormat,
                              objectSpec.getFullIdentifier(),
                              objectAction.getId(),
                              param.getParameterIndex());
  
          });
      }
  
  }
  