PropertyView.java

package sprouts.impl;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.*;

import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 *  A property view is a property that is derived from one or more other properties.
 *  It observes the changes of the source properties and updates its value accordingly.
 *  The value of a property view is calculated by a combiner function or a simple
 *  mapping function depending on the number of source properties.
 *
 * @param <T> The type of the item wrapped by a given property...
 */
final class PropertyView<T extends @Nullable Object> implements Var<T>, Viewable<T> {

	private static final Logger log = org.slf4j.LoggerFactory.getLogger(PropertyView.class);

	private static ParentRef<@Nullable Val<?>>[] _filterStrongParentRefs( Val<?>[] parentRefs ) {
		ParentRef<@Nullable Val<?>>[] strongParentRefs = new ParentRef[parentRefs.length];
		for ( int i = 0; i < parentRefs.length; i++ ) {
			Val<?> property = parentRefs[i];
			Objects.requireNonNull(property);
			strongParentRefs[i] = ParentRef.of(property);
		}
		return strongParentRefs;
	}

	private static <T> PropertyView<@Nullable T> _ofNullable( Class<T> type, @Nullable T item, Val<?>... strongParentRefs ) {
		return new PropertyView<>(type, item, Sprouts.factory().defaultId(), new ChangeListeners<>(), true, _filterStrongParentRefs(strongParentRefs));
	}

	private static <T> PropertyView<T> _of( Class<T> type, T item, Val<?>... strongParentRefs ) {
		return new PropertyView<>(type, item, Sprouts.factory().defaultId(), new ChangeListeners<>(), false, _filterStrongParentRefs(strongParentRefs));
	}

	public static <T, U> Viewable<@Nullable U> ofNullable(Class<U> type, Val<T> source, Function<T, @Nullable U> mapper) {
		final U initialItem = mapper.apply(source.orElseNull());
		if ( source.isImmutable() ) {
			return Viewable.cast(initialItem == null ? Val.ofNull(type) : Val.of(initialItem));
		}
		final PropertyView<@Nullable U> viewProperty = PropertyView._ofNullable(type, initialItem, source);
		Viewable.cast(source).onChange(Util.VIEW_CHANNEL, Action.ofWeak( viewProperty, (innerViewProperty, v) -> {
			innerViewProperty._setInternal(mapper.apply(v.orElseNull()));
			innerViewProperty.fireChange(v.channel());
		}));
		return viewProperty;
	}

	public static <T, U> Viewable<U> of( U nullObject, U errorObject, Val<T> source, Function<T, @Nullable U> mapper) {
		Objects.requireNonNull(nullObject);
		Objects.requireNonNull(errorObject);

		Function<T, U> nonNullMapper = Util.nonNullMapper(nullObject, errorObject, mapper);

		final U initial = nonNullMapper.apply(source.orElseNull());
		final Class<U> targetType = Util.expectedClassFromItem(initial);
		if ( source.isImmutable() ) {
			return Viewable.cast(Val.of(initial)); // A nice little optimization: a view of an immutable property is also immutable.
		}

		final PropertyView<U> viewProperty = PropertyView._of( targetType, initial, source );
		Viewable.cast(source).onChange(Util.VIEW_CHANNEL, Action.ofWeak( viewProperty, (innerViewProperty, v) -> {
			@Nullable Val<T> innerSource = innerViewProperty._getSource(0);
			if ( innerSource == null )
				return;
			final U value = nonNullMapper.apply(innerSource.orElseNull());
			innerViewProperty._setInternal(value);
			innerViewProperty.fireChange(v.channel());
		}));
		return viewProperty;
	}

	public static <T> Viewable<T> of( Val<T> source ) {
		final T initial = source.orElseNull();
		if ( source.isImmutable() ) {
			if ( initial == null )
				return Viewable.cast(Val.ofNull(source.type()));
			else // A nice little optimization: a view of an immutable property is also immutable.
				return Viewable.cast(Val.of(initial));
		}

		final PropertyView<T> viewProperty;
		if ( source.allowsNull() )
			viewProperty = PropertyView._ofNullable( source.type(), initial, source );
		else
			viewProperty = PropertyView._of( source.type(), Objects.requireNonNull(initial), source );
		Viewable.cast(source).onChange(Util.VIEW_CHANNEL, Action.ofWeak( viewProperty, (innerViewProperty, v) -> {
			@Nullable Val<T> innerSource = innerViewProperty._getSource(0);
			if ( innerSource == null )
				return;

			innerViewProperty._setInternal(v.orElseNull());
			innerViewProperty.fireChange(v.channel());
		}));
		return viewProperty;
	}

	public static <U, T> Viewable<T> of( Class<T> type, Val<U> parent, Function<U, T> mapper ) {
		@Nullable T initialItem = mapper.apply(parent.orElseNull());
		if ( parent.isMutable() && parent instanceof Var ) {
			Var<U> source = (Var<U>) parent;
			PropertyView<T> view = PropertyView._of( type, initialItem, parent );
			Viewable.cast(source).onChange(From.ALL, Action.ofWeak(view, (innerViewProperty, v) -> {
				T newItem = mapper.apply(v.orElseNull());
				innerViewProperty._setInternal(newItem);
				innerViewProperty.fireChange(v.channel());
			}));
			return view;
		}
		else // A nice little optimization: a view of an immutable property is also immutable!
			return ( initialItem == null ? (Viewable<T>) Val.ofNull(type) : (Viewable<T>) Val.of(initialItem));
	}

	public static <T extends @Nullable Object, U extends @Nullable Object> Viewable<@NonNull T> viewOf( Val<T> first, Val<U> second, BiFunction<T, U, @NonNull T> combiner ) {
		return of( first, second, combiner );
	}

	public static <T extends @Nullable Object, U extends @Nullable Object> Viewable<@Nullable T> viewOfNullable( Val<T> first, Val<U> second, BiFunction<T, U, @Nullable T> combiner ) {
		return ofNullable( first, second, combiner );
	}

	public static <T extends @Nullable Object, U extends @Nullable Object, R> Viewable<R> viewOf(Class<R> type, Val<T> first, Val<U> second, BiFunction<T, U, R> combiner) {
		return of( type, first, second, combiner );
	}

	public static <T extends @Nullable Object, U extends @Nullable Object, R> Viewable<@Nullable R> viewOfNullable(Class<R> type, Val<T> first, Val<U> second, BiFunction<T, U, @Nullable R> combiner) {
		return ofNullable( type, first, second, combiner );
	}

	private static <T extends @Nullable Object, U extends @Nullable Object> Viewable<@NonNull T> of(
		Val<T> first,
		Val<U> second,
		BiFunction<T, U, @NonNull T> combiner
	) {
		String id = _compositeIdFrom(first, second);

		BiFunction<Maybe<T>, Maybe<U>, @Nullable T> fullCombiner = (p1, p2) -> {
			try {
				return combiner.apply(p1.orElseNull(), p2.orElseNull());
			} catch ( Exception e ) {
				return null;
			}
		};

		T initial = fullCombiner.apply(first, second);
		Objects.requireNonNull(initial,"The result of the combiner function is null, but the property does not allow null items!");
		BiConsumer<PropertyView<T>,ValDelegate<T>> firstListener = (innerResult,v) -> {
			Val<U> innerSecond = innerResult._getSource(1);
			if (innerSecond == null)
				return;
			T newItem = fullCombiner.apply(v, innerSecond);
			if (newItem == null)
				log.error(
					"Invalid combiner result! The combination of the first item '{}' (changed) and the second " +
					"item '{}' was null and null is not allowed! The old item '{}' is retained!",
					v.orElseNull(), innerSecond.orElseNull(), innerResult.orElseNull()
				);
			else {
				innerResult._setInternal(newItem);
				innerResult.fireChange(From.ALL);
			}
		};
		BiConsumer<PropertyView<T>,ValDelegate<U>> secondListener = (innerResult,v) -> {
			Val<T> innerFirst = innerResult._getSource(0);
			T newItem = fullCombiner.apply(innerFirst, v);
			if (newItem == null)
				log.error(
					"Invalid combiner result! The combination of the first item '{}' and the second " +
					"item '{}' (changed) was null and null is not allowed! The old item '{}' is retained!",
					innerFirst.orElseNull(), v.orElseNull(), innerResult.orElseNull()
				);
			else {
				innerResult._setInternal(newItem);
				innerResult.fireChange(From.ALL);
			}
		};

		boolean firstIsImmutable = first.isImmutable();
		boolean secondIsImmutable = second.isImmutable();
		if ( firstIsImmutable && secondIsImmutable ) {
			return Viewable.cast(initial == null ? Val.ofNull(first.type()) : Val.of(initial));
		}

		PropertyView<T> result = PropertyView._of( first.type(), initial, first, second ).withId(id);
		if ( !firstIsImmutable )
			Viewable.cast(first).onChange(From.ALL, Action.ofWeak( result, firstListener ));
		if ( !secondIsImmutable )
			Viewable.cast(second).onChange(From.ALL, Action.ofWeak( result, secondListener ));
		return result;
	}

	private static <T extends @Nullable Object, U extends @Nullable Object> Viewable<T> ofNullable( Val<T> first, Val<U> second, BiFunction<T, U, T> combiner ) {

		String id = _compositeIdFrom(first, second);

		BiFunction<Maybe<T>, Maybe<U>, @Nullable T> fullCombiner = (p1, p2) -> {
			try {
				return combiner.apply(p1.orElseNull(), p2.orElseNull());
			} catch ( Exception e ) {
				return null;
			}
		};

		T initial = fullCombiner.apply(first, second);

		boolean firstIsImmutable = first.isImmutable();
		boolean secondIsImmutable = second.isImmutable();
		if ( firstIsImmutable && secondIsImmutable ) {
			return Viewable.cast(initial == null ? Val.ofNull(first.type()) : Val.of(initial));
		}

		PropertyView<@Nullable T> result = PropertyView._ofNullable( first.type(), initial, first, second ).withId(id);
		if ( !firstIsImmutable )
			Viewable.cast(first).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
				Val<U> innerSecond = innerResult._getSource(1);
				innerResult._setInternal(fullCombiner.apply(v, innerSecond));
				innerResult.fireChange(v.channel());
			}));
		if ( !secondIsImmutable )
			Viewable.cast(second).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
				Val<T> innerFirst = innerResult._getSource(0);
				innerResult._setInternal(fullCombiner.apply(innerFirst, v));
				innerResult.fireChange(v.channel());
			}));
		return result;
	}

	private static <T extends @Nullable Object, U extends @Nullable Object, R> Viewable<R> of(
		Class<R> type,
		Val<T> first,
		Val<U> second,
		BiFunction<T,U,R> combiner
	) {
		String id = _compositeIdFrom(first, second);

		BiFunction<Maybe<T>, Maybe<U>, @Nullable R> fullCombiner = (p1, p2) -> {
			try {
				return combiner.apply(p1.orElseNull(), p2.orElseNull());
			} catch ( Exception e ) {
				return null;
			}
		};

		@Nullable R initial = fullCombiner.apply(first, second);

		if (initial == null)
			throw new NullPointerException("The result of the combiner function is null, but the property does not allow null items!");

		PropertyView<R> result = PropertyView._of(type, initial, first, second ).withId(id);

		Viewable.cast(first).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
			Val<U> innerSecond = innerResult._getSource(1);
			@Nullable R newItem = fullCombiner.apply(v, innerSecond);
			if (newItem == null)
				log.error(
					"Invalid combiner result! The combination of the first item '{}' (changed) " +
					"and the second item '{}' was null and null is not allowed! " +
					"The old item '{}' is retained!",
					v.orElseNull(), innerSecond.orElseNull(), innerResult.orElseNull()
				);
			else {
				innerResult._setInternal(newItem);
				innerResult.fireChange(v.channel());
			}
		}));
		Viewable.cast(second).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
			Val<T> innerFirst = innerResult._getSource(0);
			@Nullable R newItem = fullCombiner.apply(innerFirst, v);
			if (newItem == null)
				log.error(
					"Invalid combiner result! The combination of the first item '{}' and the second " +
					"item '{}' (changed) was null and null is not allowed! " +
					"The old item '{}' is retained!",
					innerFirst.orElseNull(), v.orElseNull(), innerResult.orElseNull()
				);
			else {
				innerResult._setInternal(newItem);
				innerResult.fireChange(v.channel());
			}
		}));
		return result;
	}

	private static <T extends @Nullable Object, U extends @Nullable Object, R> Viewable<@Nullable R> ofNullable(
	    Class<R>                      type,
	    Val<T>                        first,
	    Val<U>                        second,
	    BiFunction<T, U, @Nullable R> combiner
	) {
		String id = _compositeIdFrom(first, second);

		BiFunction<Maybe<T>, Maybe<U>, @Nullable R> fullCombiner = (p1, p2) -> {
			try {
				return combiner.apply(p1.orElseNull(), p2.orElseNull());
			} catch ( Exception e ) {
				return null;
			}
		};

		PropertyView<@Nullable R> result =  PropertyView._ofNullable( type, fullCombiner.apply(first, second), first, second ).withId(id);
		Viewable.cast(first).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
			Val<U> innerSecond = innerResult._getSource(1);
			innerResult._setInternal(fullCombiner.apply(v, innerSecond));
			innerResult.fireChange(v.channel());
		}));
		Viewable.cast(second).onChange(From.ALL, Action.ofWeak(result, (innerResult,v) -> {
			Val<T> innerFirst = innerResult._getSource(0);
			innerResult._setInternal(fullCombiner.apply(innerFirst, v));
			innerResult.fireChange(v.channel());
		}));
		return result;
	}

	private static String _compositeIdFrom(Val<?> first, Val<?> second) {
		String id = "";
		if ( !first.id().isEmpty() && !second.id().isEmpty() )
			id = first.id() + "_and_" + second.id();
		else if ( !first.id().isEmpty() )
			id = first.id();
		else if ( !second.id().isEmpty() )
			id = second.id();
		return id;
	}

	private final ChangeListeners<T> _changeListeners;

    private final String _id;
	private final boolean _nullable;
	private final Class<T> _type;

	@Nullable private T _currentItem;

	private final ParentRef<Val<?>>[] _strongParentRefs;


	private PropertyView(
        Class<T> type,
        @Nullable T iniValue,
        String id,
        ChangeListeners<T> changeListeners,
        boolean allowsNull,
		ParentRef<Val<?>>[] strongParentRefs
    ) {
		Objects.requireNonNull(id);
		Objects.requireNonNull(type);
		Objects.requireNonNull(changeListeners);
		Objects.requireNonNull(strongParentRefs);
		_type             = type;
		_id               = id;
		_nullable         = allowsNull;
		_currentItem      = iniValue;
		_changeListeners  = new ChangeListeners<>();
		_strongParentRefs = strongParentRefs;

		if ( _currentItem != null ) {
			// We check if the type is correct
			if ( !_type.isAssignableFrom(_currentItem.getClass()) )
				throw new IllegalArgumentException(
						"The provided type of the initial item is not compatible " +
								"with the actual type of the variable"
				);
		}
		if ( !Sprouts.factory().idPattern().matcher(_id).matches() )
			throw new IllegalArgumentException(
					"The provided id '"+_id+"' is not valid! It must match " +
							"the pattern '"+Sprouts.factory().idPattern().pattern()+"'."
			);
		if ( !allowsNull && iniValue == null )
			throw new IllegalArgumentException(
					"The provided initial item is null, " +
							"but this property view does not allow null items!"
			);
	}

	private <P> Val<P> _getSource( int index ) {
		if ( index < 0 || index >= _strongParentRefs.length )
			throw new IndexOutOfBoundsException("The index "+index+" is out of bounds!");
		return (Val) _strongParentRefs[index].get();
	}

	/** {@inheritDoc} */
	@Override public PropertyView<T> withId( String id ) {
        return new PropertyView<>(_type, _currentItem, id, _changeListeners, _nullable, _strongParentRefs);
	}

	/** {@inheritDoc} */
	@Override
	public Viewable<T> onChange( Channel channel, Action<ValDelegate<T>> action ) {
		_changeListeners.onChange(channel, action);
		return this;
	}

	/** {@inheritDoc} */
	@Override public Var<T> fireChange( Channel channel ) {
		_changeListeners.fireChange(this, channel);
		return this;
	}

	@Override
	public final boolean isMutable() {
		return true;
	}

	@Override
	public boolean isView() {
		return true;
	}

	/** {@inheritDoc} */
	@Override
	public Var<T> set( Channel channel, T newItem ) {
		Objects.requireNonNull(channel);
		if ( _setInternal(newItem) )
			this.fireChange(channel);
		return this;
	}

	private boolean _setInternal( @Nullable T newValue ) {
		if ( !_nullable && newValue == null )
			throw new NullPointerException(
					"This property is configured to not allow null items! " +
					"If you want your property to allow null items, use the 'ofNullable(Class, T)' factory method."
				);

		if ( !Objects.equals(_currentItem, newValue ) ) {
			// First we check if the item is compatible with the type
			if ( newValue != null && !_type.isAssignableFrom(newValue.getClass()) )
				throw new IllegalArgumentException(
						"The provided type '"+newValue.getClass()+"' of the new item is not compatible " +
						"with the type '"+_type+"' of this property"
					);

			_currentItem = newValue;
			return true;
		}
		return false;
	}

	@Override
	public Observable subscribe( Observer observer ) {
		_changeListeners.onChange( observer );
		return this;
	}

	@Override
	public Observable unsubscribe( Subscriber subscriber ) {
		_changeListeners.unsubscribe(subscriber);
		return this;
	}

	public final long numberOfChangeListeners() {
		return _changeListeners.numberOfChangeListeners();
	}

	/** {@inheritDoc} */
	@Override public final Class<T> type() { return _type; }

	/** {@inheritDoc} */
	@Override public final String id() { return _id; }

	/** {@inheritDoc} */
	@Override
	public final @Nullable T orElseNull() { return _currentItem; }

	/** {@inheritDoc} */
	@Override public final boolean allowsNull() { return _nullable; }

	@Override
	public final String toString() {
		String item = this.mapTo(String.class, Object::toString).orElse("null");
		String id = this.id() == null ? "?" : this.id();
		if ( id.equals(Sprouts.factory().defaultId()) ) id = "?";
		String type = ( type() == null ? "?" : type().getSimpleName() );
		if ( type.equals("Object") ) type = "?";
		if ( type.equals("String") && this.isPresent() ) item = "\"" + item + "\"";
		if (_nullable) type = type + "?";
		String name = "View";
		String content = ( id.equals("?") ? item : id + "=" + item );
		return name + "<" + type + ">" + "[" + content + "]";
	}

}