package org.fxmisc.easybind.monadic;

import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableValue;

import org.fxmisc.easybind.PreboundBinding;
import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.select.SelectBuilder;

/**
 * Adds monadic operations to {@link ObservableValue}.
 */
public interface MonadicObservableValue<T> extends ObservableObjectValue<T> {

    /**
     * Checks whether this ObservableValue holds a (non-null) value.
     * @return {@code true} if this ObservableValue holds a (non-null) value,
     * {@code false} otherwise.
     */
    default boolean isPresent() {
        return getValue() != null;
    }

    /**
     * Inverse of {@link #isPresent()}.
     */
    default boolean isEmpty() {
        return getValue() == null;
    }

    /**
     * Invokes the given function if this ObservableValue holds a (non-null)
     * value.
     * @param f function to invoke on the value currently held by this
     * ObservableValue.
     */
    default void ifPresent(Consumer<? super T> f) {
        T val = getValue();
        if(val != null) {
            f.accept(val);
        }
    }

    /**
     * Returns the value currently held by this ObservableValue.
     * @throws NoSuchElementException if there is no value present.
     */
    default T getOrThrow() {
        T res = getValue();
        if(res != null) {
            return res;
        } else {
            throw new NoSuchElementException();
        }
    }

    /**
     * Returns the value currently held by this ObservableValue.
     * If this ObservableValue is empty, {@code other} is returned instead.
     * @param other value to return if there is no value present in this
     * ObservableValue.
     */
    default T getOrElse(T other) {
        T res = getValue();
        if(res != null) {
            return res;
        } else {
            return other;
        }
    }

    /**
     * Returns an {@code Optional} describing the value currently held by this
     * ObservableValue, or and empty {@code Optional} if this ObservableValue
     * is empty.
     */
    default Optional<T> getOpt() {
        return Optional.ofNullable(getValue());
    }

    /**
     * Returns a new ObservableValue that holds the value held by this
     * ObservableValue, or {@code other} when this ObservableValue is empty.
     */
    default MonadicBinding<T> orElse(T other) {
        return new PreboundBinding<T>(this) {
            @Override
            protected T computeValue() {
                T val = MonadicObservableValue.this.getValue();
                return val != null ? val : other;
            }
        };
    }

    /**
     * Returns a new ObservableValue that holds the value held by this
     * ObservableValue, or the value held by {@code other} when this
     * ObservableValue is empty.
     */
    default MonadicBinding<T> orElse(ObservableValue<T> other) {
        return new FirstNonNullBinding<>(this, other);
    }

    /**
     * Returns a new ObservableValue that holds the same value
     * as this ObservableValue when the value satisfies the predicate
     * and is empty when this ObservableValue is empty or its value
     * does not satisfy the given predicate.
     */
    default MonadicBinding<T> filter(Predicate<? super T> p) {
        return new PreboundBinding<T>(this) {
            @Override
            protected T computeValue() {
                T val = MonadicObservableValue.this.getValue();
                return (val != null && p.test(val)) ? val : null;
            }
        };
    }

    /**
     * Returns a new ObservableValue that holds a mapping of the value held
     * by this ObservableValue, and is empty when this ObservableValue is empty.
     * @param f function to map the value held by this ObservableValue.
     */
    default <U> MonadicBinding<U> map(Function<? super T, ? extends U> f) {
        return new PreboundBinding<U>(this) {
            @Override
            protected U computeValue() {
                T baseVal = MonadicObservableValue.this.getValue();
                return baseVal != null ? f.apply(baseVal) : null;
            }
        };
    }

    /**
     * Returns a new ObservableValue that, when this ObservableValue holds
     * value {@code x}, holds the value held by {@code f(x)}, and is empty
     * when this ObservableValue is empty.
     */
    default <U> MonadicBinding<U> flatMap(Function<? super T, ObservableValue<U>> f) {
        return new FlatMapBinding<>(this, f);
    }

    /**
     * Similar to {@link #flatMap(Function)}, except the returned Binding is
     * also a Property. This means you can call {@code setValue()} and
     * {@code bind()} methods on the returned value, which delegate to the
     * currently selected Property.
     *
     * <p>As the value of this ObservableValue changes, so does the selected
     * Property. When the Property returned from this method is bound, as the
     * selected Property changes, the previously selected property is unbound
     * and the newly selected property is bound.
     *
     * <p>Note that if the currently selected property is {@code null}, then
     * calling {@code getValue()} on the returned value will return {@code null}
     * regardless of any prior call to {@code setValue()} or {@code bind()}.
     *
     * <p>Note that you need to retain a reference to the returned value to
     * prevent it from being garbage collected.
     */
    default <U> PropertyBinding<U> selectProperty(Function<? super T, Property<U>> f) {
        return new FlatMapProperty<>(this, f);
    }

    /**
     * Starts a selection chain. A selection chain is just a more efficient
     * equivalent to a chain of flatMaps.
     */
    default <U> SelectBuilder<U> select(Function<? super T, ObservableValue<U>> selector) {
        return SelectBuilder.startAt(this).select(selector);
    }

    /**
     * Adds an invalidation listener and returns a Subscription that can be
     * used to remove that listener.
     *
     * <pre>
     * {@code
     * Subscription s = observable.subscribe(obs -> doSomething());
     *
     * // later
     * s.unsubscribe();
     * }</pre>
     *
     * is equivalent to
     *
     * <pre>
     * {@code
     * InvalidationListener l = obs -> doSomething();
     * observable.addListener(l);
     *
     * // later
     * observable.removeListener();
     * }</pre>
     */
    default Subscription subscribe(InvalidationListener listener) {
        addListener(listener);
        return () -> removeListener(listener);
    }

    /**
     * Adds a change listener and returns a Subscription that can be
     * used to remove that listener. See the example at
     * {@link #subscribe(InvalidationListener)}.
     */
    default Subscription subscribe(ChangeListener<? super T> listener) {
        addListener(listener);
        return () -> removeListener(listener);
    }
}
