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.interactions.managed;
  
  import org.jspecify.annotations.NonNull;
  
  import org.apache.causeway.applib.value.semantics.Parser;
  import org.apache.causeway.applib.value.semantics.Renderer;
  import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
  import org.apache.causeway.commons.binding.Bindable;
  import org.apache.causeway.commons.binding.Observable;
  import org.apache.causeway.commons.functional.Either;
  import org.apache.causeway.commons.internal.binding._BindableAbstract;
  import org.apache.causeway.commons.internal.binding._Bindables;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  import org.apache.causeway.core.metamodel.object.ManagedObject;
  import org.apache.causeway.core.metamodel.object.MmUnwrapUtils;
  import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
  import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
  import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
  
  import lombok.experimental.UtilityClass;
  
  @UtilityClass
  class _BindingUtil {
  
      enum TargetFormat {
          TITLE,
          HTML,
          PARSABLE_TEXT;
          boolean requiresParser() {
              return this==PARSABLE_TEXT;
          }
          boolean requiresRenderer() {
              return this==TITLE
                      || this==HTML;
          }
      }
  
      /**
       * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
       * otherwise just an {@link Observable}.
       */
      @SuppressWarnings({ "rawtypes" })
      Observable<String> bindAsFormated(
              final @NonNull TargetFormat format,
              final @NonNull OneToOneAssociation prop,
              final @NonNull _BindableAbstract<ManagedObject> bindablePropertyValue) {
  
          var spec = prop.getElementType();
  
          // value types should have associated parsers/formatters via value semantics
          return spec.valueFacet()
              .map(valueFacet->{
                  var eitherRendererOrParser = format.requiresRenderer()
                      ? Either.<Renderer, Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(prop))
                      : Either.<Renderer, Parser>right(valueFacet.selectParserForAttributeOrElseFallback(prop));
                  var ctx = valueFacet.createValueSemanticsContext(prop);
  
                  return bindAsFormated(format, spec, bindablePropertyValue, eitherRendererOrParser, ctx);
              })
              .orElseGet(()->
                  // fallback Bindable that is floating free (unbound)
                  // writing to it has no effect on the domain
                  _Bindables.forValue(String.format("Could not find a ValueFacet for type %s",
                          spec.logicalType()))
              );
      }
  
      /**
       * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
       * otherwise just an {@link Observable}.
       */
      @SuppressWarnings({ "rawtypes" })
      Observable<String> bindAsFormated(
              final @NonNull TargetFormat format,
              final @NonNull ObjectActionParameter param,
              final @NonNull _BindableAbstract<ManagedObject> bindableParamValue) {
  
          // non-scalar action parameters are neither parseable nor renderable
          if(param.isPlural()) return _Bindables.forValue("multiple parameters");
  
          var spec = param.getElementType();
  
          // value types should have associated parsers/formatters via value semantics
          return spec.valueFacet()
              .map(valueFacet->{
                  var eitherRendererOrParser = format.requiresRenderer()
                      ? Either.<Renderer, Parser>left(valueFacet.selectRendererForParamOrPropOrCollOrElseFallback(param))
                      : Either.<Renderer, Parser>right(valueFacet.selectParserForAttributeOrElseFallback(param));
                  var ctx = valueFacet.createValueSemanticsContext(param);
  
                  return bindAsFormated(format, spec, bindableParamValue, eitherRendererOrParser, ctx);
              })
              .orElseGet(()->
                  // fallback Bindable that is floating free (unbound)
                  // writing to it has no effect on the domain
                  _Bindables.forValue(String.format("Could not find a ValueFacet for type %s",
                          spec.logicalType()))
              );
      }
  
      // -- PREDICATES
  
      boolean hasParser(final @NonNull OneToOneAssociation prop) {
          return prop.getElementType()
                  .valueFacet()
                  .map(valueFacet->valueFacet.selectRendererForParamOrPropOrColl(prop).isPresent())
                  .orElse(false);
      }
  
      boolean hasParser(final @NonNull ObjectActionParameter param) {
          return param.isPlural()
                  ? false
                  : param.getElementType()
                      .valueFacet()
                      .map(valueFacet->valueFacet.selectRendererForParamOrPropOrColl(param).isPresent())
                      .orElse(false);
      }
  
      // -- HELPER
  
      /**
       * For {@link TargetFormat#PARSABLE_TEXT} returns a {@link Bindable},
       * otherwise just an {@link Observable}.
       */
      @SuppressWarnings({ "unchecked", "rawtypes" })
      private Observable<String> bindAsFormated(
              final @NonNull TargetFormat format,
              final @NonNull ObjectSpecification spec,
              final @NonNull _BindableAbstract<ManagedObject> bindableValue,
              final @NonNull Either<Renderer, Parser> eitherRendererOrParser,
              final ValueSemanticsProvider.@NonNull Context context) {
  
          switch (format) {
          case TITLE: {
              var renderer = eitherRendererOrParser.left().orElseThrow();
              return bindableValue.map(value->{
                          var pojo = MmUnwrapUtils.single(value);
                          var title = renderer.titlePresentation(context, pojo);
                          return title;
                      });
          }
          case HTML: {
              var renderer = eitherRendererOrParser.left().orElseThrow();
              return bindableValue.map(value->{
                          var pojo = MmUnwrapUtils.single(value);
                          var html = renderer.htmlPresentation(context, pojo);
                          return html;
                      });
          }
          case PARSABLE_TEXT:
              var parser = eitherRendererOrParser.right().orElseThrow();
              return bindableValue.mapToBindable(
                      value->{
                          var pojo = MmUnwrapUtils.single(value);
                          var text = parser.parseableTextRepresentation(context, pojo);
                          //System.err.printf("toText: %s -> '%s'%n", ""+value, text);
                          return text;
                      },
                      text->{
                          var value = ManagedObject.value(spec, parser.parseTextRepresentation(context, text));
                          //System.err.printf("fromText: '%s' -> %s%n", text, ""+value);
                          return value;
                      });
          default:
              throw _Exceptions.unmatchedCase(format);
          }
      }
  
  }
  