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.commons.internal.binding;
  
  import java.lang.ref.WeakReference;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.function.Function;
  import java.util.function.UnaryOperator;
  
  import org.apache.causeway.commons.binding.Bindable;
  import org.apache.causeway.commons.binding.ChangeListener;
  import org.apache.causeway.commons.binding.InvalidationListener;
  import org.apache.causeway.commons.binding.Observable;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  
  import org.jspecify.annotations.NonNull;
  import lombok.Setter;
  
  /**
   * <h1>- internal use only -</h1>
   *
   * <p>
   * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
   * These may be changed or removed without notice!
   * </p>
   *
   * @since 2.0
   */
  public abstract class _BindableAbstract<T> implements Bindable<T> {
  
      private T value;
      private Observable<? extends T> observable = null;
      private InvalidationListener invalidationListener = null;
      private boolean valid = true;
      private InternalUtil<T> util = null;
  
      /**
       * Called within {@link #getValue()} to refine the internally stored value.
       * <p>
       * use-case: re-fetch detached entities
       */
      @Setter private @NonNull UnaryOperator<T> valueRefiner = UnaryOperator.identity();
  
      /**
       * Called within {@link #setValue()} to guard against invalid new values.
       * <p>
       * use-case: guard against incompatible types
       */
      @Setter private @NonNull UnaryOperator<T> valueGuard = UnaryOperator.identity();
  
      public _BindableAbstract() {
      }
  
      public _BindableAbstract(final T initialValue) {
          this.value = initialValue;
      }
  
      @Override
      public void addListener(final InvalidationListener listener) {
          util = InternalUtil.addListener(util, this, listener);
      }
  
      @Override
      public void removeListener(final InvalidationListener listener) {
          util = InternalUtil.removeListener(util, listener);
      }
  
      @Override
      public void addListener(final ChangeListener<? super T> listener) {
          util = InternalUtil.addListener(util, this, listener);
      }
  
      @Override
      public void removeListener(final ChangeListener<? super T> listener) {
          util = InternalUtil.removeListener(util, listener);
      }
  
      @Override
      public void bindBidirectional(final Bindable<T> other) {
          InternalBidirectionalBinding.bind(this, other);
      }
  
      @Override
      public void unbindBidirectional(final Bindable<T> other) {
          InternalBidirectionalBinding.unbind(this, other);
      }
  
      @Override
      public T getValue() {
          valid = true;
          var val = observable == null
                  ? value
                  : observable.getValue();
          return valueRefiner.apply(val);
      }
  
      @Override
      public void setValue(final T proposedNewValue) {
          if (isBound()) {
              throw _Exceptions.unrecoverable("Cannot set value on a bound bindable.");
          }
          var newValue = valueGuard.apply(proposedNewValue);
          if (value != newValue) {
              value = newValue;
              markInvalid();
          }
      }
  
      @Override
      public boolean isBound() {
          return observable != null;
      }
  
      @Override
      public void bind(final @NonNull Observable<? extends T> newObservable) {
          if (!newObservable.equals(this.observable)) {
              unbind();
              observable = newObservable;
              if (invalidationListener == null) {
                  invalidationListener = new WeakInvalidationListener(this);
              }
              observable.addListener(invalidationListener);
              markInvalid();
          }
      }
  
      @Override
      public void unbind() {
          if (observable != null) {
              value = observable.getValue();
              observable.removeListener(invalidationListener);
              observable = null;
          }
      }
  
      protected void fireValueChanged() {
          InternalUtil.fireValueChanged(util);
      }
  
      protected void onInvalidated() {
      }
  
      // -- COMPOSITION
  
      @Override
      public <R> Observable<R> map(
              final Function<T, R> forwardMapper) {
          final var newBindable = _Observables.<R>lazy(()->forwardMapper.apply(getValue()));
          addListener((e,o,n)->{
              newBindable.setValue(forwardMapper.apply(n));
          });
          return newBindable;
      }
  
      @Override
      public <R> Bindable<R> mapToBindable(
              final Function<T, R> forwardMapper,
              final Function<R, T> reverseMapper) {
  
          var isForwardUpdating = new AtomicBoolean();
          var isReverseUpdating = new AtomicBoolean();
  
          var newBindable = _Bindables.<R>forValue(forwardMapper.apply(getValue()));
          addListener((e,o,n)->{
              if(isReverseUpdating.get()) return;
  
              try {
                  isForwardUpdating.set(true);
                  newBindable.setValue(forwardMapper.apply(n));
              } finally {
                  isForwardUpdating.set(false);
              }
          });
  
          newBindable.addListener((e,o,n)->{
              if(isForwardUpdating.get()) return;
  
              try {
                  isReverseUpdating.set(true);
                  setValue(reverseMapper.apply(n));
              } finally {
                  isReverseUpdating.set(false);
              }
          });
  
          return newBindable;
      }
  
      // -- HELPER
  
      private void markInvalid() {
          if (valid) {
              valid = false;
              onInvalidated();
              fireValueChanged();
          }
      }
  
      private static class WeakInvalidationListener
      implements InvalidationListener, InternalUtil.WeakListener {
  
          private final WeakReference<_BindableAbstract<?>> wref;
  
          public WeakInvalidationListener(final _BindableAbstract<?> ref) {
              this.wref = new WeakReference<_BindableAbstract<?>>(ref);
          }
  
          @Override
          public void invalidated(final Observable<?> observable) {
              _BindableAbstract<?> ref = wref.get();
              if (ref == null) {
                  observable.removeListener(this);
              } else {
                  ref.markInvalid();
              }
          }
  
          @Override
          public boolean isNoLongerReferenced() {
              return wref.get() == null;
          }
      }
  
  }
  