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.util;
  
  import java.util.Optional;
  import java.util.OptionalInt;
  import java.util.function.Predicate;
  import java.util.stream.Stream;
  
  import org.jspecify.annotations.Nullable;
  import org.springframework.util.ClassUtils;
  
  import org.apache.causeway.applib.annotation.BookmarkPolicy;
  import org.apache.causeway.applib.annotation.DomainServiceLayout.MenuBar;
  import org.apache.causeway.applib.annotation.LabelPosition;
  import org.apache.causeway.applib.annotation.PromptStyle;
  import org.apache.causeway.applib.annotation.SemanticsOf;
  import org.apache.causeway.applib.annotation.TableDecorator;
  import org.apache.causeway.applib.annotation.Where;
  import org.apache.causeway.applib.id.LogicalType;
  import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._Casts;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.commons.internal.factory._InstanceUtil;
  import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedType;
  import org.apache.causeway.core.config.metamodel.facets.ParameterConfigOptions;
  import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
  import org.apache.causeway.core.metamodel.facetapi.Facet;
  import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
  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.actions.semantics.ActionSemanticsFacet;
  import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet;
  import org.apache.causeway.core.metamodel.facets.collections.CollectionFacet;
  import org.apache.causeway.core.metamodel.facets.collections.collection.defaultview.DefaultViewFacet;
  import org.apache.causeway.core.metamodel.facets.members.cssclass.CssClassFacet;
  import org.apache.causeway.core.metamodel.facets.object.autocomplete.AutoCompleteFacet;
  import org.apache.causeway.core.metamodel.facets.object.bookmarkpolicy.BookmarkPolicyFacet;
  import org.apache.causeway.core.metamodel.facets.object.domainservicelayout.DomainServiceLayoutFacet;
  import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
  import org.apache.causeway.core.metamodel.facets.object.icon.IconFacet;
  import org.apache.causeway.core.metamodel.facets.object.mixin.MixinFacet;
  import org.apache.causeway.core.metamodel.facets.object.projection.ProjectionFacet;
  import org.apache.causeway.core.metamodel.facets.object.promptStyle.PromptStyleFacet;
  import org.apache.causeway.core.metamodel.facets.object.tabledec.TableDecoratorFacet;
  import org.apache.causeway.core.metamodel.facets.object.value.ValueFacet;
  import org.apache.causeway.core.metamodel.facets.object.value.ValueSerializer;
  import org.apache.causeway.core.metamodel.facets.objectvalue.daterenderedadjust.DateRenderAdjustFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MaxFractionalDigitsFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MaxTotalDigitsFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.digits.MinFractionalDigitsFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.fileaccept.FileAcceptFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.labelat.LabelAtFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.multiline.MultiLineFacet;
  import org.apache.causeway.core.metamodel.facets.objectvalue.typicallen.TypicalLengthFacet;
  import org.apache.causeway.core.metamodel.facets.param.parameter.precpol.PrecedingParametersPolicyFacet;
  import org.apache.causeway.core.metamodel.interactions.managed.ManagedProperty;
  import org.apache.causeway.core.metamodel.interactions.managed.ParameterNegotiationModel;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  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.ObjectFeature;
  
  import lombok.experimental.UtilityClass;
  
  /**
   * Facet utility.
   * <p>
   * Motivation: Viewers should not use {@link Facet}s directly.
   */
  @UtilityClass
  public final class Facets {
  
      public Can<ManagedObject> autoCompleteExecute(
              final ObjectSpecification objectSpec, final String term) {
          return objectSpec.lookupFacet(AutoCompleteFacet.class)
              .map(autoCompleteFacet->autoCompleteFacet.execute(term, InteractionInitiatedBy.USER))
              .orElseGet(Can::empty);
      }
  
      public boolean autoCompleteIsPresent(final ObjectSpecification objectSpec) {
          return objectSpec.containsFacet(AutoCompleteFacet.class);
      }
  
      public OptionalInt autoCompleteMinLength(final ObjectSpecification objectSpec) {
          return objectSpec.lookupFacet(AutoCompleteFacet.class)
              .map(AutoCompleteFacet::getMinLength)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      public Optional<BookmarkPolicy> bookmarkPolicy(final @Nullable FacetHolder facetHolder) {
          return Optional.ofNullable(facetHolder)
              .flatMap(spec->spec.lookupFacet(BookmarkPolicyFacet.class))
              .map(BookmarkPolicyFacet::value);
      }
  
      public BookmarkPolicy bookmarkPolicyOrElseNotSpecified(final @Nullable FacetHolder facetHolder) {
          return bookmarkPolicy(facetHolder).orElse(BookmarkPolicy.NOT_SPECIFIED);
      }
  
      public Optional<BSGrid> bootstrapGrid(
              final ObjectSpecification objectSpec, final @Nullable ManagedObject objectAdapter) {
          return objectSpec.lookupFacet(GridFacet.class)
              .map(gridFacet->gridFacet.getGrid(objectAdapter))
              .flatMap(grid->_Casts.castTo(BSGrid.class, grid));
      }
  
      public Optional<BSGrid> bootstrapGrid(final ObjectSpecification objectSpec) {
          return bootstrapGrid(objectSpec, null);
      }
  
      public boolean collectionIsPresent(final ObjectSpecification objectSpec) {
          return objectSpec.containsFacet(CollectionFacet.class);
      }
  
      //XXX could be moved to ManagedObject directly, there be an utility already under a different name
      public Stream<ManagedObject> collectionStream(final @Nullable ManagedObject collection) {
          return CollectionFacet.streamAdapters(collection);
      }
  
      // used for non-scalar action return
      public Stream<ManagedObject> collectionStream(
              final ObjectSpecification objectSpec, final @Nullable ManagedObject collection) {
          return objectSpec.lookupFacet(CollectionFacet.class)
              .map(collectionFacet->collectionFacet.stream(collection))
              .orElseGet(Stream::empty);
      }
  
      public Optional<String> cssClass(
              final FacetHolder facetHolder, final ManagedObject objectAdapter) {
          return facetHolder.lookupFacet(CssClassFacet.class)
              .map(cssClassFacet->cssClassFacet.cssClass(objectAdapter));
      }
  
      public int dateRenderAdjustDays(final ObjectFeature feature) {
          return feature.lookupFacet(DateRenderAdjustFacet.class)
              .map(DateRenderAdjustFacet::getDateRenderAdjustDays)
              .orElse(0);
      }
  
      public boolean defaultViewIsPresent(final ObjectFeature feature) {
          return feature.containsFacet(DefaultViewFacet.class);
      }
  
      public boolean defaultViewIsTable(final ObjectFeature feature) {
          return defaultViewName(feature)
              .map(viewName->"table".equals(viewName))
              .orElse(false);
      }
  
      public Optional<String> defaultViewName(final ObjectFeature feature) {
          return feature.lookupFacet(DefaultViewFacet.class)
              .map(DefaultViewFacet::value);
      }
  
      public static ParameterConfigOptions.PrecedingParametersPolicy precedingParametersPolicy(
              final ObjectActionParameter parameter) {
          return parameter.lookupFacet(PrecedingParametersPolicyFacet.class)
              .map(PrecedingParametersPolicyFacet::value)
              .orElseGet(ParameterConfigOptions.PrecedingParametersPolicy::defaultsIfNotSpecifiedOtherwise);
      }
  
      public Optional<MenuBar> domainServiceLayoutMenuBar(final ObjectSpecification objectSpec) {
          return objectSpec.lookupFacet(DomainServiceLayoutFacet.class)
              .map(DomainServiceLayoutFacet::getMenuBar);
      }
  
      public Optional<String> fileAccept(final ObjectFeature feature) {
          return feature.lookupFacet(FileAcceptFacet.class)
              .map(FileAcceptFacet::value);
      }
  
      public void gridPreload(
              final ObjectSpecification objectSpec, final ManagedObject objectAdapter) {
          objectSpec.lookupFacet(GridFacet.class)
              .ifPresent(gridFacet->
                  // the facet should always exist, in fact
                  // just enough to ask for the metadata.
                  // This will cause the current ObjectSpec to be updated as a side effect.
                  gridFacet.getGrid(objectAdapter));
      }
  
      public boolean hasSafeSemantics(final FacetHolder holder) {
          return holder.lookupFacet(ActionSemanticsFacet.class)
              .map(ActionSemanticsFacet::value)
              .map(SemanticsOf::isSafeInNature)
              .orElse(false);
      }
  
      public Optional<Where> hiddenWhere(final ObjectFeature feature) {
          return feature.lookupFacet(HiddenFacet.class)
              .map(HiddenFacet::where);
      }
  
      public Predicate<ObjectFeature> hiddenWhereMatches(final Predicate<Where> matcher) {
          return feature->Facets.hiddenWhere(feature)
              .map(matcher::test)
              .orElse(false);
      }
  
      public boolean iconIsPresent(final ObjectSpecification objectSpec) {
          return objectSpec.containsFacet(IconFacet.class);
      }
  
      /**
       * Non-null, one of {LEFT, RIGHT, TOP or NONE}.
       */
      public LabelPosition labelAt(final ObjectFeature feature) {
          return feature.lookupFacet(LabelAtFacet.class)
              .map(LabelAtFacet::label)
              .map(labelPos->{
                  switch (labelPos) {
                  case LEFT:
                  case RIGHT:
                  case NONE:
                  case TOP:
                      return labelPos;
                  case DEFAULT:
                  case NOT_SPECIFIED:
                  default:
                      return LabelPosition.LEFT;
                  }
              })
              .orElse(LabelPosition.LEFT);
      }
  
      public String labelAtCss(final ObjectFeature feature) {
          return "label-" + labelAt(feature).name().toLowerCase();
      }
  
      public OptionalInt minFractionalDigits(final FacetHolder facetHolder) {
          return facetHolder.lookupFacet(MinFractionalDigitsFacet.class)
              .map(MinFractionalDigitsFacet::getMinFractionalDigits)
              .filter(digits->digits>-1)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      public OptionalInt maxFractionalDigits(final FacetHolder facetHolder) {
          return facetHolder.lookupFacet(MaxFractionalDigitsFacet.class)
              .map(MaxFractionalDigitsFacet::getMaxFractionalDigits)
              .filter(digits->digits>-1)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      public OptionalInt maxFractionalDigits(final @Nullable Iterable<FacetHolder> facetHolders) {
          return _NullSafe.stream(facetHolders)
              .map(Facets::maxFractionalDigits)
              .findFirst()
              .orElseGet(OptionalInt::empty);
      }
  
      public OptionalInt maxLength(final FacetHolder facetHolder) {
          return facetHolder
              .lookupFacet(MaxLengthFacet.class)
              .map(MaxLengthFacet::value)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      public OptionalInt maxTotalDigits(final FacetHolder facetHolder) {
          return facetHolder.lookupFacet(MaxTotalDigitsFacet.class)
              .map(MaxTotalDigitsFacet::getMaxTotalDigits)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      public OptionalInt maxTotalDigits(final @Nullable Iterable<FacetHolder> facetHolders) {
          return _NullSafe.stream(facetHolders)
              .map(Facets::maxTotalDigits)
              .findFirst()
              .orElseGet(OptionalInt::empty);
      }
  
      public boolean mixinIsPresent(final ObjectSpecification objectSpec) {
          return objectSpec.containsFacet(MixinFacet.class);
      }
  
      public boolean multilineIsPresent(final ObjectFeature feature) {
          return feature.lookupNonFallbackFacet(MultiLineFacet.class)
              .isPresent();
      }
  
      public OptionalInt multilineNumberOfLines(final ObjectFeature feature) {
          return feature
              .lookupFacet(MultiLineFacet.class)
              .map(MultiLineFacet::numberOfLines)
              .map(OptionalInt::of)
              .orElseGet(OptionalInt::empty);
      }
  
      //XXX could be moved to ManagedObject directly
      public ManagedObject projected(final ManagedObject objectAdapter) {
          return objectAdapter.objSpec().lookupFacet(ProjectionFacet.class)
              .map(projectionFacet->projectionFacet.projected(objectAdapter))
              .orElse(objectAdapter);
      }
  
      public Optional<PromptStyle> promptStyle(final ObjectFeature feature) {
          return feature.lookupFacet(PromptStyleFacet.class)
              .map(PromptStyleFacet::value);
      }
  
      public PromptStyle promptStyleOrElse(final ObjectFeature feature, final PromptStyle fallback) {
          return Facets.promptStyle(feature)
              .map(promptStyle->
                  promptStyle == PromptStyle.AS_CONFIGURED
                  ? fallback
                  : promptStyle)
              .orElse(fallback);
      }
  
      public Optional<ObjectSpecification> elementSpec(final FacetHolder facetHolder) {
          return facetHolder.lookupFacet(TypeOfFacet.class)
              .map(TypeOfFacet::elementSpec);
      }
  
      public Optional<ResolvedType> typeOfAnyCardinality(final FacetHolder facetHolder) {
          return facetHolder.lookupFacet(TypeOfFacet.class)
              .map(TypeOfFacet::value);
      }
  
      public OptionalInt typicalLength(
              final ObjectSpecification objectSpec, final OptionalInt maxLength) {
          var typicalLength = objectSpec
                  .lookupFacet(TypicalLengthFacet.class)
                  .map(TypicalLengthFacet::value)
                  .orElse(null);
          // doesn't make sense for typical length to be > maxLength
          final Integer result = (typicalLength != null
                  && maxLength.isPresent()
                  && typicalLength > maxLength.getAsInt())
                  ? (Integer)maxLength.getAsInt()
                  : typicalLength;
          return Optional.ofNullable(result)
                  .map(OptionalInt::of)
                  .orElseGet(OptionalInt::empty);
      }
  
      // -- TABLES
  
      public Optional<TableDecorator> tableDecorator(final FacetHolder ... facetHolders) {
          return FacetUtil.lookupFacetInButExcluding(TableDecoratorFacet.class, o -> o == TableDecorator.Default.class, facetHolders)
                  .stream()
                  .map(TableDecoratorFacet::value)
                  .map(decoratorClass -> {
                      var decorator = _InstanceUtil.createInstance(decoratorClass, decoratorClass);
                      return facetHolders[0].injectServicesInto(decorator);
                  })
                  .findFirst();
      }
  
      // -- VALUE FACET
  
      public static Predicate<ObjectSpecification> valueTypeMatches(final Predicate<Class<?>> typeMatcher) {
          return spec->
              spec.valueFacet()
              .map(ValueFacet::getLogicalType)
              .map(LogicalType::correspondingClass)
              .map(typeMatcher::test)
              .orElse(false);
      }
  
      public Optional<ObjectAction> valueCompositeMixinForParameter(
              final ObjectFeature param,
              final ParameterNegotiationModel parameterNegotiationModel,
              final int paramIndex) {
          var objectSpec = param.getElementType();
          return objectSpec.valueFacet()
              .<ObjectAction>flatMap(valueFacet->
                  valueFacet.selectCompositeValueMixinForParameter(
                          parameterNegotiationModel,paramIndex));
      }
  
      public Optional<ObjectAction> valueCompositeMixinForProperty(
              final ObjectFeature prop,
              final ManagedProperty managedProperty) {
          var objectSpec = prop.getElementType();
          return objectSpec.valueFacet()
              .<ObjectAction>flatMap(valueFacet->
                  valueFacet.selectCompositeValueMixinForProperty(managedProperty));
      }
  
      public <X> Stream<X> valueStreamSemantics(
              final ObjectSpecification objectSpec, final Class<X> requiredType) {
          return objectSpec.valueFacet()
              .map(valueFacet->valueFacet.streamValueSemantics(requiredType))
              .orElseGet(Stream::empty);
      }
  
      public <X> boolean valueHasSemantics(
              final ObjectSpecification objectSpec, final Class<X> requiredType) {
          return objectSpec.valueFacet()
              .map(valueFacet->valueFacet.streamValueSemantics(requiredType)
                      .findFirst()
                      .isPresent())
              .orElse(false);
      }
  
      public <X> Optional<ValueSemanticsProvider<X>> valueDefaultSemantics(
              final ObjectSpecification objectSpec,
              final Class<X> requiredType) {
          return objectSpec.valueFacet()
              .filter(typeGuard(requiredType))
              .flatMap(ValueFacet::selectDefaultSemantics)
              .map(_Casts::uncheckedCast);
      }
  
      @SuppressWarnings("unchecked")
      public <X> Optional<ValueSerializer<X>> valueSerializer(
              final ObjectSpecification objectSpec,
              final Class<X> requiredType) {
          return objectSpec.valueFacet()
              .filter(typeGuard(requiredType))
              .map(valueFacet->(ValueSerializer<X>)valueFacet);
      }
  
      public <X> ValueSerializer<X> valueSerializerElseFail(
              final ObjectSpecification objectSpec,
              final Class<X> requiredType) {
          return valueSerializer(objectSpec, requiredType)
              .orElseThrow(()->_Exceptions.illegalArgument(
                      "ObjectSpec is expected to have a ValueFacet<%s>",
                      objectSpec.getCorrespondingClass().getName()));
      }
  
      // -- HELPER
  
      @SuppressWarnings("rawtypes")
      private Predicate<? super ValueFacet> typeGuard(
              final Class<?> requiredType) {
          return valueFacet->
              ClassUtils.resolvePrimitiveIfNecessary(requiredType)
              .isAssignableFrom(
                      ClassUtils.resolvePrimitiveIfNecessary(valueFacet.getValueClass()));
      }
  
  }
  