Property.java

package sprouts.impl;

import org.jspecify.annotations.Nullable;
import sprouts.*;

import java.util.Objects;

/**
 * 	The base implementation for both {@link Var} and {@link Val} interfaces.
 * 	This also serves as a reference implementation for the concept of the
 *  {@link Var}/{@link Val} properties in general.
 * 
 * @param <T> The type of the value wrapped by a given property...
 */
final class Property<T extends @Nullable Object> implements Var<T>, Viewable<T> {

	public static <T> Var<@Nullable T> ofNullable( boolean immutable, Class<T> type, @Nullable T value ) {
		return new Property<T>( immutable, type, value, Sprouts.factory().defaultId(), new ChangeListeners<>(), true );
	}

	public static <T> Var<T> of( boolean immutable, Class<T> type, T value ) {
		return new Property<T>( immutable, type, value, Sprouts.factory().defaultId(), new ChangeListeners<>(), false );
	}

	public static <T> Var<T> of( boolean immutable, T iniValue ) {
		Objects.requireNonNull(iniValue);
		Class<T> itemType = Util.expectedClassFromItem(iniValue);
		return new Property<T>( immutable, itemType, iniValue, Sprouts.factory().defaultId(), new ChangeListeners<>(), false );
	}


	private final ChangeListeners<T> _changeListeners;
	private final String   _id;
	private final Class<T> _type;

	private final boolean  _nullable;
	private final boolean  _isImmutable;

	private @Nullable T _value;


	Property(
		boolean            immutable,
		Class<T>           type,
		@Nullable T        iniValue,
		String             id,
		ChangeListeners<T> changeListeners,
		boolean            allowsNull
	) {
		Objects.requireNonNull(id);
		Objects.requireNonNull(type);
		Objects.requireNonNull(changeListeners);
		_type            = type;
		_id              = id;
		_nullable        = allowsNull;
		_isImmutable     = immutable;
		_value           = iniValue;
		_changeListeners = new ChangeListeners<>(changeListeners);

		if ( _value != null ) {
			// We check if the type is correct
			if ( !_type.isAssignableFrom(_value.getClass()) )
				throw new IllegalArgumentException(
						"The provided type of the initial value 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!");
		if ( !allowsNull && iniValue == null )
			throw new IllegalArgumentException("The provided initial value is null, but the property does not allow null values!");

	}

	/** {@inheritDoc} */
	@Override public Var<T> withId( String id ) {
        return new Property<T>( _isImmutable, _type, _value, id, _changeListeners, _nullable);
	}

	/** {@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 _value; }

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

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

	/** {@inheritDoc} */
	@Override
	public Var<T> set( Channel channel, T newItem ) {
		Objects.requireNonNull(channel);
		if ( _isImmutable )
			throw new UnsupportedOperationException("This variable is immutable!");
		if ( _setInternal(newItem) )
			this.fireChange(channel);
		return this;
	}

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

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

			_value = newValue;
			return true;
		}
		return false;
	}

	/** {@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 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();
	}

	@Override
	public final String toString() {
		String value = 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() ) value = "\"" + value + "\"";
		if (_nullable) type = type + "?";
		String name = _isImmutable ? "Val" : "Var";
		String content = ( id.equals("?") ? value : id + "=" + value );
		return name + "<" + type + ">" + "[" + content + "]";
	}

	@Override
	public final boolean equals( Object obj ) {
		if ( obj == null ) return false;
		if ( obj == this ) return true;
		if ( !_isImmutable ) {
			return false;
		}
		if ( obj instanceof Val ) {
			Val<?> other = (Val<?>) obj;
			if ( other.type() != _type) return false;
			if ( other.orElseNull() == null ) return _value == null;
			return Val.equals( other.orElseThrowUnchecked(), _value); // Arrays are compared with Arrays.equals
		}
		return false;
	}

	@Override
	public final int hashCode() {
		if ( !_isImmutable ) {
			return System.identityHashCode(this);
		}
		int hash = 7;
		hash = 31 * hash + ( _value == null ? 0 : Val.hashCode(_value) );
		hash = 31 * hash + ( _type  == null ? 0 : _type.hashCode()     );
		hash = 31 * hash + ( _id    == null ? 0 : _id.hashCode()       );
		return hash;
	}
}