TreeNodeConf.java

package swingtree;

import org.jspecify.annotations.Nullable;
import sprouts.Lens;
import sprouts.Tuple;
import swingtree.api.IconDeclaration;

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

/**
 *  Describes how the nodes of one particular type behave inside a
 *  {@link javax.swing.JTree} built through {@link UI#tree(sprouts.Var, swingtree.api.Configurator)}.
 *  You never create one of these yourself, instead you receive it inside the
 *  {@link swingtree.api.Configurator} lambda passed to
 *  {@link TreeConf#nodesOf(Class, swingtree.api.Configurator)}:
 *  <pre>{@code
 *  UI.tree(fileSystem, conf -> conf
 *      .nodesOf(Dir.class, dir -> dir
 *          .children(Dir::entries, Dir::withEntries)
 *          .text(Dir::name, Dir::withName)
 *          .icon(dir -> Icons.FOLDER)
 *      )
 *      .nodesOf(Doc.class, doc -> doc
 *          .text(Doc::name)
 *      )
 *  );
 *  }</pre>
 *  A configuration block like this is the tree's equivalent of one {@code case} in a
 *  {@code switch} over a sealed type, which is why a sum type based tree usually has
 *  exactly one {@code nodesOf(..)} block per permitted subtype.
 *  <p>
 *  <b>A getter alone is read only, a getter together with a wither is a lens and
 *  therefore two way.</b> This is the same rule the {@link sprouts.Var#zoomTo(Function, BiFunction)}
 *  method follows, and it applies to every aspect declared here: {@link #children(Function)}
 *  makes a branch the user cannot restructure, {@link #children(Function, BiFunction)} makes
 *  one they can, {@link #text(Function)} renders a label and {@link #text(Function, BiFunction)}
 *  additionally permits renaming the node in place.
 *  <p>
 *  Instances of this class are immutable values, so every method returns a new
 *  instance instead of modifying the receiver.
 *
 * @param <N> The common node type of the tree this rule belongs to.
 * @param <B> The concrete node type this rule applies to.
 */
public final class TreeNodeConf<N, B extends N>
{
    /*
        The children rule is erased so the tree machinery can apply it without knowing the
        concrete node type. What makes that sound: the tuple handed to a wither is always the
        very tuple its own getter produced, with at most one entry exchanged.
    */
    private final Class<B>                                                    _type;
    private final @Nullable Function<Object, Tuple<Object>>                   _childrenGetter;
    private final @Nullable BiFunction<Object, Tuple<Object>, Object>         _childrenWither;
    private final @Nullable Function<B, String>                               _textGetter;
    private final @Nullable BiFunction<B, String, B>                          _textWither;
    private final @Nullable Function<B, IconDeclaration>                      _iconGetter;
    private final @Nullable Function<B, String>                               _toolTipGetter;
    private final @Nullable Boolean                                           _isLeaf;

    static <N, B extends N> TreeNodeConf<N, B> of( Class<B> type ) {
        return new TreeNodeConf<>(type, null, null, null, null, null, null, null);
    }

    private TreeNodeConf(
        Class<B>                                            type,
        @Nullable Function<Object, Tuple<Object>>           childrenGetter,
        @Nullable BiFunction<Object, Tuple<Object>, Object> childrenWither,
        @Nullable Function<B, String>                       textGetter,
        @Nullable BiFunction<B, String, B>                  textWither,
        @Nullable Function<B, IconDeclaration>              iconGetter,
        @Nullable Function<B, String>                       toolTipGetter,
        @Nullable Boolean                                   isLeaf
    ) {
        _type           = Objects.requireNonNull(type);
        _childrenGetter = childrenGetter;
        _childrenWither = childrenWither;
        _textGetter     = textGetter;
        _textWither     = textWither;
        _iconGetter     = iconGetter;
        _toolTipGetter  = toolTipGetter;
        _isLeaf         = isLeaf;
    }

    /**
     *  Declares that nodes of this type are branches whose children are read from the
     *  supplied getter, and that the user may not restructure them.
     *  <pre>{@code
     *  .nodesOf(Dir.class, dir -> dir.children(Dir::entries))
     *  }</pre>
     *  Declaring a children rule is also what turns a node type into a branch: a type
     *  without one is a leaf, and a branch stays a branch even while it happens to have
     *  no children (see {@link TreeConf#leafWhenEmpty(boolean)}).
     *
     * @param getter Reads the children of a node of this type.
     * @param <C> The type of the children, which must be assignable to the tree's node type.
     * @return An updated configuration.
     */
    public <C extends N> TreeNodeConf<N, B> children( Function<B, Tuple<C>> getter ) {
        Objects.requireNonNull(getter, "getter");
        return new TreeNodeConf<>(
            _type, _erase(getter), null,
            _textGetter, _textWither, _iconGetter, _toolTipGetter, _isLeaf
        );
    }

    /**
     *  Declares that nodes of this type are branches whose children are read from the
     *  supplied getter and written back through the supplied wither, which makes the
     *  branch structure editable and gives every node below it a writable lens
     *  reaching all the way up into the root property.
     *  <pre>{@code
     *  .nodesOf(Dir.class, dir -> dir.children(Dir::entries, Dir::withEntries))
     *  }</pre>
     *
     * @param getter Reads the children of a node of this type.
     * @param wither Returns a new node of this type with the given children.
     * @param <C> The type of the children, which must be assignable to the tree's node type.
     * @return An updated configuration.
     */
    public <C extends N> TreeNodeConf<N, B> children(
        Function<B, Tuple<C>> getter,
        BiFunction<B, Tuple<C>, B> wither
    ) {
        Objects.requireNonNull(getter, "getter");
        Objects.requireNonNull(wither, "wither");
        return new TreeNodeConf<>(
            _type, _erase(getter), _erase(wither),
            _textGetter, _textWither, _iconGetter, _toolTipGetter, _isLeaf
        );
    }

    /**
     *  Declares the children of this node type through a {@link Lens}, which is
     *  equivalent to {@link #children(Function, BiFunction)} but lets you reuse a lens
     *  you already have, or write one whose focus needs logic of its own.
     *
     * @param lens Focuses the children collection of a node of this type.
     * @param <C> The type of the children, which must be assignable to the tree's node type.
     * @return An updated configuration.
     */
    public <C extends N> TreeNodeConf<N, B> children( Lens<B, Tuple<C>> lens ) {
        Objects.requireNonNull(lens, "lens");
        return children(
            node -> {
                try {
                    return lens.getter(node);
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
            },
            (node, children) -> {
                try {
                    return lens.wither(node, children);
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
            }
        );
    }

    /**
     *  Declares the label shown for nodes of this type. Without a text rule the tree falls
     *  back to {@link Object#toString()}, which is rarely what a record should show.
     *
     * @param getter Produces the label of a node of this type.
     * @return An updated configuration.
     */
    public TreeNodeConf<N, B> text( Function<B, String> getter ) {
        Objects.requireNonNull(getter, "getter");
        return new TreeNodeConf<>(
            _type, _childrenGetter, _childrenWither,
            getter, null, _iconGetter, _toolTipGetter, _isLeaf
        );
    }

    /**
     *  Declares the label shown for nodes of this type together with a wither, which
     *  additionally permits the user to rename the node in place. The edited text is
     *  handed to the wither and the resulting node is written back into the root property.
     *  <pre>{@code
     *  .nodesOf(Dir.class, dir -> dir.text(Dir::name, Dir::withName))
     *  }</pre>
     *  Renaming additionally requires the tree to be bound to a mutable {@link sprouts.Var}
     *  and every branch above the node to declare a children wither, because that is the
     *  chain the new value has to travel back up.
     *
     * @param getter Produces the label of a node of this type.
     * @param wither Returns a new node of this type carrying the edited label.
     * @return An updated configuration.
     */
    public TreeNodeConf<N, B> text( Function<B, String> getter, BiFunction<B, String, B> wither ) {
        Objects.requireNonNull(getter, "getter");
        Objects.requireNonNull(wither, "wither");
        return new TreeNodeConf<>(
            _type, _childrenGetter, _childrenWither,
            getter, wither, _iconGetter, _toolTipGetter, _isLeaf
        );
    }

    /**
     *  Declares the icon shown next to nodes of this type. The returned
     *  {@link IconDeclaration} is resolved through the regular SwingTree icon cache, so
     *  SVG sources and HiDPI scaling work exactly as they do everywhere else.
     *
     * @param getter Produces the icon declaration of a node of this type.
     * @return An updated configuration.
     */
    public TreeNodeConf<N, B> icon( Function<B, IconDeclaration> getter ) {
        Objects.requireNonNull(getter, "getter");
        return new TreeNodeConf<>(
            _type, _childrenGetter, _childrenWither,
            _textGetter, _textWither, getter, _toolTipGetter, _isLeaf
        );
    }

    /**
     *  Declares the tool tip shown when the pointer rests on a node of this type.
     *
     * @param getter Produces the tool tip text of a node of this type.
     * @return An updated configuration.
     */
    public TreeNodeConf<N, B> toolTip( Function<B, String> getter ) {
        Objects.requireNonNull(getter, "getter");
        return new TreeNodeConf<>(
            _type, _childrenGetter, _childrenWither,
            _textGetter, _textWither, _iconGetter, getter, _isLeaf
        );
    }

    /**
     *  Overrides whether nodes of this type are leaves. By default a node type is a leaf
     *  exactly when it declares no {@code children(..)} rule, which is the answer a sum
     *  type based model wants. Override it when a node has no children <i>yet</i> but must
     *  still show a handle, because expanding it is what triggers the load:
     *  <pre>{@code
     *  .nodesOf(Pending.class, p -> p.text(Pending::label).isLeaf(false))
     *  }</pre>
     *
     * @param isLeaf True to force nodes of this type to be leaves, false to force them to be branches.
     * @return An updated configuration.
     */
    public TreeNodeConf<N, B> isLeaf( boolean isLeaf ) {
        return new TreeNodeConf<>(
            _type, _childrenGetter, _childrenWither,
            _textGetter, _textWither, _iconGetter, _toolTipGetter, isLeaf
        );
    }

    @SuppressWarnings("unchecked")
    private static <B, C> Function<Object, Tuple<Object>> _erase( Function<B, Tuple<C>> getter ) {
        return node -> (Tuple<Object>) (Tuple<?>) getter.apply((B) node);
    }

    @SuppressWarnings("unchecked")
    private static <B, C> BiFunction<Object, Tuple<Object>, Object> _erase( BiFunction<B, Tuple<C>, B> wither ) {
        return (node, children) -> wither.apply((B) node, (Tuple<C>) (Tuple<?>) children);
    }

    Class<B> type() {
        return _type;
    }

    boolean hasChildrenRule() {
        return _childrenGetter != null;
    }

    boolean isStructurallyWritable() {
        return _childrenWither != null;
    }

    Tuple<Object> childrenOf( Object node ) {
        Function<Object, Tuple<Object>> getter = _childrenGetter;
        if ( getter == null )
            return Tuple.of(Object.class);
        return getter.apply(node);
    }

    Object withChildren( Object node, Tuple<Object> children ) {
        BiFunction<Object, Tuple<Object>, Object> wither = _childrenWither;
        if ( wither == null )
            return node;
        return wither.apply(node, children);
    }

    @SuppressWarnings("unchecked")
    @Nullable String textOf( Object node ) {
        Function<B, String> getter = _textGetter;
        if ( getter == null )
            return null;
        return getter.apply((B) node);
    }

    boolean isRenamable() {
        return _textWither != null;
    }

    @SuppressWarnings("unchecked")
    Object withText( Object node, String text ) {
        BiFunction<B, String, B> wither = _textWither;
        if ( wither == null )
            return node;
        return wither.apply((B) node, text);
    }

    @SuppressWarnings("unchecked")
    @Nullable IconDeclaration iconOf( Object node ) {
        Function<B, IconDeclaration> getter = _iconGetter;
        if ( getter == null )
            return null;
        return getter.apply((B) node);
    }

    @SuppressWarnings("unchecked")
    @Nullable String toolTipOf( Object node ) {
        Function<B, String> getter = _toolTipGetter;
        if ( getter == null )
            return null;
        return getter.apply((B) node);
    }

    @Nullable Boolean declaredLeafState() {
        return _isLeaf;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" +
                    "type="       + _type.getSimpleName() + ", " +
                    "children="   + ( _childrenGetter == null ? "none" : ( _childrenWither == null ? "readOnly" : "writable" ) ) + ", " +
                    "text="       + ( _textGetter == null ? "none" : ( _textWither == null ? "readOnly" : "writable" ) ) + ", " +
                    "icon="       + ( _iconGetter == null ? "none" : "declared" ) + ", " +
                    "isLeaf="     + ( _isLeaf == null ? "derived" : String.valueOf(_isLeaf) ) +
                "]";
    }
}