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.applib.value.semantics;
  
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.text.DecimalFormat;
  import java.text.DecimalFormatSymbols;
  import java.text.NumberFormat;
  import java.text.ParseException;
  import java.text.ParsePosition;
  import java.time.format.DateTimeFormatter;
  import java.time.format.DateTimeFormatterBuilder;
  import java.time.format.FormatStyle;
  import java.util.Locale;
  import java.util.Optional;
  import java.util.function.Function;
  import java.util.function.Supplier;
  
  import org.springframework.beans.factory.annotation.Autowired;
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.applib.annotation.TimePrecision;
  import org.apache.causeway.applib.exceptions.recoverable.TextEntryParseException;
  import org.apache.causeway.applib.locale.UserLocale;
  import org.apache.causeway.applib.services.bookmark.IdStringifier;
  import org.apache.causeway.applib.services.i18n.TranslationContext;
  import org.apache.causeway.applib.services.i18n.TranslationService;
  import org.apache.causeway.applib.services.iactnlayer.InteractionContext;
  import org.apache.causeway.applib.services.placeholder.PlaceholderRenderService;
  import org.apache.causeway.applib.services.placeholder.PlaceholderRenderService.PlaceholderLiteral;
  import org.apache.causeway.applib.util.schema.CommonDtoUtils;
  import org.apache.causeway.applib.value.semantics.TemporalValueSemantics.EditingFormatDirection;
  import org.apache.causeway.applib.value.semantics.TemporalValueSemantics.TemporalEditingPattern;
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.assertions._Assert;
  import org.apache.causeway.commons.internal.base._Strings;
  import org.apache.causeway.commons.internal.base._Temporals;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.schema.common.v2.ValueType;
  import org.apache.causeway.schema.common.v2.ValueWithTypeDto;
  
  import org.jspecify.annotations.NonNull;
  
  /**
   * @since 2.x {@index}
   */
  public abstract class ValueSemanticsAbstract<T>
  implements
  ValueSemanticsProvider<T> {
  
      @SuppressWarnings("unchecked")
      @Override
      public OrderRelation<T, ?> getOrderRelation() {
          return this instanceof OrderRelation ? (OrderRelation<T, ?>)this : null;
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public Converter<T, ?> getConverter() {
          return this instanceof Converter ? (Converter<T, ?>)this : null;
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public Renderer<T> getRenderer() {
          return this instanceof Renderer ? (Renderer<T>)this : null;
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public Parser<T> getParser() {
          return this instanceof Parser ? (Parser<T>)this : null;
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public DefaultsProvider<T> getDefaultsProvider() {
          return this instanceof DefaultsProvider ? (DefaultsProvider<T>)this : null;
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public IdStringifier<T> getIdStringifier() {
          return this instanceof IdStringifier ? (IdStringifier<T>)this : null;
      }
  
      /**
       * JUnit support.
       */
      public Can<T> getExamples() { return Can.empty(); }
  
      /**
       * @param context - nullable in support of JUnit testing
       * @return {@link Locale} from given context or else system's default
       */
      protected UserLocale getUserLocale(final ValueSemanticsProvider.@Nullable Context context) {
          return Optional.ofNullable(context)
                  .map(ValueSemanticsProvider.Context::interactionContext)
                  .map(InteractionContext::getLocale)
                  .orElseGet(UserLocale::getDefault);
      }
  
      protected String renderTitle(final T value, final Function<T, String> toString) {
          return Optional.ofNullable(value)
                  .map(toString)
                  .orElseGet(()->getPlaceholderRenderService().asText(PlaceholderLiteral.NULL_REPRESENTATION));
      }
  
      protected String renderHtml(final T value, final Function<T, String> toString) {
          return Optional.ofNullable(value)
                  .map(toString)
                  .orElseGet(()->getPlaceholderRenderService().asHtml(PlaceholderLiteral.NULL_REPRESENTATION));
      }
  
      // -- COMPOSITION UTILS
  
      protected ValueDecomposition decomposeAsString(
              final @Nullable T value,
              final @NonNull Function<T, String> toString,
              final @NonNull Supplier<String> onNull) {
          _Assert.assertEquals(getSchemaValueType(), ValueType.STRING);
          return CommonDtoUtils.fundamentalTypeAsDecomposition(ValueType.STRING,
                  value!=null
                  ? toString.apply(value)
                  : onNull.get());
      }
  
      protected T composeFromString(
              final @Nullable ValueDecomposition decomposition,
              final @NonNull Function<String, T> fromString,
              final @NonNull Supplier<T> onNullOrEmpty) {
          var string = decomposition!=null
                  ? decomposition.fundamentalAsOptional().map(ValueWithTypeDto::getString).orElse(null)
                  : null;
          return _Strings.isNotEmpty(string)
                  ? fromString.apply(string)
                  : onNullOrEmpty.get();
      }
  
      /**
       * @param <F> - the underlying fundamental value-type
       */
      protected <F> ValueDecomposition decomposeAsNullable(
              final @Nullable T value,
              final @NonNull Function<T, F> onNonNull,
              final @NonNull Supplier<F> onNull) {
          return CommonDtoUtils.fundamentalTypeAsDecomposition(getSchemaValueType(),
                  value!=null
                  ? onNonNull.apply(value)
                  : onNull.get());
      }
  
      /**
       * @param <F> - the underlying fundamental value-type
       */
      protected <F> T composeFromNullable(
              final @Nullable ValueDecomposition decomposition,
              final @NonNull Function<ValueWithTypeDto, F> fundamentalValueExtractor,
              final @NonNull Function<F, T> onNonNull,
              final @NonNull Supplier<T> onNull) {
  
          var valuePojo = decomposition!=null
                  ? decomposition.fundamentalAsOptional().map(fundamentalValueExtractor).orElse(null)
                  : null;
  
          return valuePojo!=null
                  ? onNonNull.apply(valuePojo)
                  : onNull.get();
      }
  
      // -- NUMBER FORMATTING/PARSING
  
      /**
       * @param context - nullable in support of JUnit testing
       * @return {@link NumberFormat} the default from given context's locale
       * or else system's default locale
       *
       * @implNote the format's MaximumFractionDigits are initialized to 16, as
       * 64 bit IEEE 754 double has 15 decimal digits of precision;
       * this is typically overruled later by implementations of
       * {@link #configureDecimalFormat(org.apache.causeway.applib.adapters.ValueSemanticsProvider.Context, DecimalFormat) configureDecimalFormat}
       */
      @SuppressWarnings("javadoc")
      protected DecimalFormat getNumberFormat(
              final ValueSemanticsProvider.@Nullable Context context) {
          return getNumberFormat(context, FormatUsageFor.RENDERING);
      }
  
      protected DecimalFormat getNumberFormat(
              final ValueSemanticsProvider.@Nullable Context context,
              final @NonNull FormatUsageFor usedFor) {
          var format = (DecimalFormat)NumberFormat.getNumberInstance(getUserLocale(context).numberFormatLocale());
          // prime w/ 16 (64 bit IEEE 754 double has 15 decimal digits of precision)
          format.setMaximumFractionDigits(16);
          configureDecimalFormat(context, format, usedFor);
          return format;
      }
  
      protected Optional<BigInteger> parseInteger(
              final ValueSemanticsProvider.@Nullable Context context,
              final @Nullable String text) {
          var input = _Strings.blankToNullOrTrim(text);
          if(input==null) {
              return Optional.empty();
          }
          try {
              return parseDecimal(context, input, GroupingSeparatorPolicy.ALLOW)
                      .map(BigDecimal::toBigIntegerExact);
          } catch (final NumberFormatException | ArithmeticException e) {
              throw new TextEntryParseException("Not an integer value " + text, e);
          }
      }
  
      protected enum GroupingSeparatorPolicy {
          ALLOW,
          DISALLOW,
          ;
      }
  
      protected Optional<BigDecimal> parseDecimal(
              final @Nullable Context context,
              final @Nullable String text,
              final GroupingSeparatorPolicy groupingSeparatorPolicy) {
          var input = _Strings.blankToNullOrTrim(text);
          if(input==null) {
              return Optional.empty();
          }
  
          if (groupingSeparatorPolicy == GroupingSeparatorPolicy.DISALLOW) {
              var userLocale = getUserLocale(context);
              var decimalFormatSymbols = new DecimalFormatSymbols(userLocale.numberFormatLocale());
              var groupingSeparatorChar = decimalFormatSymbols.getGroupingSeparator();
              if (input.contains(""+groupingSeparatorChar)) {
                  throw new TextEntryParseException("Invalid value '" + input + "'; do not use the '" + groupingSeparatorChar + "' grouping separator");
              }
          }
  
          var format = getNumberFormat(context, FormatUsageFor.PARSING);
          format.setParseBigDecimal(true);
  
          var position = new ParsePosition(0);
          try {
              var number = (BigDecimal)format.parse(input, position);
              if (position.getErrorIndex() != -1) {
                  throw new ParseException("could not parse input='" + input + "'", position.getErrorIndex());
              } else if (position.getIndex() < input.length()) {
                  throw new ParseException("input='" + input + "' was not processed completely", position.getIndex());
              }
              // check for maxFractionDigits if required ...
              final int maxFractionDigits = format.getMaximumFractionDigits();
              if(maxFractionDigits>-1
                      && number.scale()>format.getMaximumFractionDigits()) {
                  throw new TextEntryParseException(String.format(
                          "No more than %d digits can be entered after the decimal separator, "
                                  + "got %d in '%s'.", maxFractionDigits, number.scale(), input));
              }
              return Optional.of(number);
          } catch (final NumberFormatException | ParseException e) {
              throw new TextEntryParseException(String.format(
                      "Not a decimal value '%s': %s", input, e.getMessage()),
                      e);
          }
      }
  
      public static enum FormatUsageFor {
          PARSING,
          RENDERING;
          public boolean isParsing() { return this==PARSING; }
          public boolean isRendering() { return this==RENDERING; }
      }
  
      /**
       * Typically overridden by BigDecimalValueSemantics to set min/max fractional digits.
       */
      protected void configureDecimalFormat(
              final Context context, final DecimalFormat format, final FormatUsageFor usedFor) {}
  
      // -- TEMPORAL RENDERING
  
      protected DateTimeFormatter getTemporalNoZoneRenderingFormat(
              final @Nullable Context context,
              final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
              final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic,
              final @NonNull FormatStyle dateFormatStyle,
              final @NonNull FormatStyle timeFormatStyle,
              final @Nullable String datePattern,
              final @Nullable String dateTimePattern) {
  
          final DateTimeFormatter noZoneOutputFormat;
  
          switch (temporalCharacteristic) {
          case DATE_TIME:
              noZoneOutputFormat =
                      dateTimePattern != null
                      ? DateTimeFormatter.ofPattern(dateTimePattern)
                      : DateTimeFormatter.ofLocalizedDateTime(dateFormatStyle, timeFormatStyle);
              break;
          case DATE_ONLY:
              noZoneOutputFormat =
                      datePattern != null
                      ? DateTimeFormatter.ofPattern(datePattern)
                      : DateTimeFormatter.ofLocalizedDate(dateFormatStyle);
              break;
          case TIME_ONLY:
              noZoneOutputFormat = DateTimeFormatter.ofLocalizedTime(timeFormatStyle);
              break;
          default:
              throw _Exceptions.unmatchedCase(temporalCharacteristic);
          }
          return noZoneOutputFormat
                  .withLocale(getUserLocale(context).timeFormatLocale());
      }
  
      protected Optional<DateTimeFormatter> getTemporalZoneOnlyRenderingFormat(
              final ValueSemanticsProvider.@Nullable Context context,
              final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
              final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic) {
  
          switch (offsetCharacteristic) {
          case LOCAL:
              return Optional.empty();
          case OFFSET:
              return Optional.of(_Temporals.ISO_OFFSET_ONLY_FORMAT);
          case ZONED:
              return Optional.of(_Temporals.DEFAULT_ZONEID_ONLY_FORMAT);
          default:
              throw _Exceptions.unmatchedCase(offsetCharacteristic);
          }
      }
  
      // -- TEMPORAL FORMATTING/PARSING
  
      protected DateTimeFormatter getTemporalEditingFormat(
              final ValueSemanticsProvider.@Nullable Context context,
              final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
              final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic,
              final @NonNull TimePrecision timePrecision,
              final @NonNull EditingFormatDirection direction,
              final @NonNull TemporalEditingPattern editingPattern) {
  
          return new DateTimeFormatterBuilder()
                  .appendPattern(editingPattern
                          .getEditingFormatAsPattern(
                                  temporalCharacteristic, offsetCharacteristic, timePrecision, direction))
                  .toFormatter(getUserLocale(context).timeFormatLocale());
      }
  
      protected DateTimeFormatter getTemporalIsoFormat(
              final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
              final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic) {
  
          switch (temporalCharacteristic) {
          case DATE_TIME:
              return offsetCharacteristic.isLocal()
                      ? DateTimeFormatter.ISO_LOCAL_DATE_TIME
                      : DateTimeFormatter.ISO_DATE_TIME;
          case DATE_ONLY:
              return offsetCharacteristic.isLocal()
                      ? DateTimeFormatter.ISO_LOCAL_DATE
                      : DateTimeFormatter.ISO_DATE;
          case TIME_ONLY:
              return offsetCharacteristic.isLocal()
                      ? DateTimeFormatter.ISO_LOCAL_TIME
                      : DateTimeFormatter.ISO_TIME;
          default:
              throw _Exceptions.unmatchedCase(temporalCharacteristic);
          }
      }
  
      // -- TRANSLATION SUPPORT
  
      @Autowired(required = false) // nullable (JUnit support)
      protected TranslationService translationService;
      protected String translate(final String text) {
          return translationService!=null
                  ? translationService.translate(TranslationContext.empty(), text)
                  : text;
      }
  
      // -- PLACEHOLDER RENDERING
  
      @Autowired(required = false) // nullable (JUnit support)
      private Optional<PlaceholderRenderService> placeholderRenderService = Optional.empty();
      protected PlaceholderRenderService getPlaceholderRenderService() {
          return placeholderRenderService.orElseGet(PlaceholderRenderService::fallback);
      }
  
  }
  