TupleTree.java

package sprouts.impl;

import org.jspecify.annotations.Nullable;
import sprouts.SequenceChange;
import sprouts.Tuple;
import sprouts.Val;

import java.util.*;
import java.util.function.Consumer;

import static sprouts.impl.ArrayUtil.*;

/**
 * An immutable, persistent tuple (ordered sequence) implementation based on a multi-branch tree structure.
 * This provides efficient O(log32 n) positional access, slicing, and bulk operations while maximizing
 * structural sharing for memory efficiency.
 *
 * <h2>Design Overview</h2>
 * <p>The tree consists of two node types:</p>
 * <ul>
 *   <li><strong>Leaf nodes</strong>: Store elements in contiguous arrays (up to {@value #IDEAL_LEAF_NODE_SIZE} elements)</li>
 *   <li><strong>Branch nodes</strong>: Hold references to subtrees with a branching factor of {@value #BRANCHING_FACTOR},
 *       using cumulative subtree sizes for index resolution</li>
 * </ul>
 *
 * <h2>Performance Characteristics</h2>
 * <table border="1">
 *   <caption>Operation Complexities</caption>
 *   <tr><th>Operation</th><th>Time</th><th>Space</th></tr>
 *   <tr><td>{@link #get(int)}</td><td>O(log32 n)</td><td>O(1)</td></tr>
 *   <tr><td>{@link #slice(int, int)}</td><td>O(log32 n)</td><td>O(log32 n) shared</td></tr>
 *   <tr><td>{@link #addAllAt(int, Tuple)}</td><td>O(log32 n + m)</td><td>O(log32 n + m)</td></tr>
 *   <tr><td>{@link #setAt(int, Object)}</td><td>O(log32 n)</td><td>O(log32 n)</td></tr>
 * </table>
 *
 * <h2>Structural Sharing</h2>
 * <p>All modification operations return new tuples that maximally share structure with the original:
 * <pre>{@code
 * Tuple<String> original = Tuple.of("A", "B", "C");
 * Tuple<String> modified = original.setAt(1, "X");
 * // Only the path from root to modified leaf is copied
 * }</pre>
 *
 * <h2>Use Cases</h2>
 * <ul>
 *   <li>Immutable sequences requiring frequent slicing/subsequence operations</li>
 *   <li>Edit-heavy workflows where versions share most content (e.g., document history)</li>
 *   <li>As a building block for persistent collections</li>
 * </ul>
 *
 * <h2>Implementation Notes</h2>
 * <ul>
 *   <li>Null elements are permitted only when constructed with {@code allowsNull=true}</li>
 *   <li>Iteration uses stack-based traversal for O(1) per-element overhead</li>
 *   <li>Automatic leaf consolidation/splitting maintains size invariants</li>
 * </ul>
 * <br>
 * Also note that when this tuple tree is constructed from
 * another sequenced collection with a known size, like an array or list,
 * then there will only be a single leaf node with one large array holding all elements.
 * Only after adding or removing from this initial densely packed tuple, will a
 * tree structure be created to accommodate for followup operations...
 *
 * @param <T> the type of elements in this tuple
 *
 * @see sprouts.Association
 * @see sprouts.ValueSet
 * @see sprouts.Tuple
 * @see sprouts.Pair
 */
public final class TupleTree<T extends @Nullable Object> implements Tuple<T> {

    private static final int BRANCHING_FACTOR = 32;
    private static final int IDEAL_LEAF_NODE_SIZE = 512;


    interface Node {
        int size();

        <T> T getAt(int index, Class<T> type);

        @Nullable
        Node slice(int from, int to, Class<?> type, boolean allowsNull);

        @Nullable
        Node removeRange(int from, int to, Class<?> type, boolean allowsNull);

        @Nullable <T> Node addAllAt(int index, Tuple<T> tuple, Class<T> type, boolean allowsNull);

        @Nullable <T> Node setAllAt(int index, int offset, Tuple<T> tuple, Class<T> type, boolean allowsNull);

        <T> void forEach(Class<T> type, Consumer<T> consumer);
    }

    static final class LeafNode implements Node {
        private final Object _data;

        LeafNode(Object data) {
            _data = data;
        }

        @Override
        public int size() {
            return _length(_data);
        }

        @Override
        public <T> T getAt(int index, Class<T> type) {
            return Util.fakeNonNull(_getAt(index, _data, type));
        }

        @Override
        public @Nullable Node slice(int from, int to, Class<?> type, boolean allowsNull) {
            if ( from == 0 && to == _length(_data) )
                return this;
            else {
                int newSize = (to - from);
                Object newItems = _createArray(type, allowsNull, newSize);
                System.arraycopy(_data, from, newItems, 0, newSize);
                return new LeafNode(newItems);
            }
        }

        @Override
        public @Nullable Node removeRange(int from, int to, Class<?> type, boolean allowsNull) {
            if ( from < 0 || to > _length(_data) )
                return this;
            if ( from > to )
                return this;
            int numberOfItemsToRemove = to - from;
            if ( numberOfItemsToRemove == 0 )
                return this;
            if ( numberOfItemsToRemove == this.size() )
                return null;
            else {
                int newSize = _length(_data) - numberOfItemsToRemove;
                if ( newSize > IDEAL_LEAF_NODE_SIZE) {
                    return _createRootFromList(type, allowsNull, new AbstractList() {
                        @Override
                        public int size() {
                            return newSize;
                        }
                        @Override
                        public @Nullable Object get(int fetchIndex) {
                            if ( fetchIndex >= from ) {
                                fetchIndex = fetchIndex + numberOfItemsToRemove;
                            }
                            return _getAt(fetchIndex, _data);
                        }
                    });
                }
                return new LeafNode(_withRemoveRange(from, to, _data, type, allowsNull));
            }
        }

        @Override
        public @Nullable <T> Node addAllAt(int index, Tuple<T> tuple, Class<T> type, boolean allowsNull) {
            int currentSize = _length(_data);
            int newSize = (currentSize + tuple.size());
            if ( newSize > IDEAL_LEAF_NODE_SIZE) {
                return _createRootFromList(type, allowsNull, new AbstractList<T>() {
                    @Override
                    public int size() {
                        return newSize;
                    }
                    @Override
                    @SuppressWarnings("unchecked")
                    public @Nullable T get(int fetchIndex) {
                        if ( fetchIndex < index ) {
                            return (T) _getAt(fetchIndex, _data);
                        }
                        if ( fetchIndex < (index + tuple.size()) ) {
                            return tuple.get(fetchIndex - index);
                        }
                        return (T) _getAt(fetchIndex - tuple.size(), _data);
                    }
                });
            }
            Object newItems = _withAddAllAt(index, tuple, _data, type, allowsNull);
            return new LeafNode(newItems);
        }

        @Override
        public @Nullable <T> Node setAllAt(int index, int offset, Tuple<T> tuple, Class<T> type, boolean allowsNull) {
            int currentSize = _length(_data);
            int offsetInTuple = Math.abs(Math.min(0, index))+offset;
            int startIndex = Math.max(0, index);
            boolean isAlreadyTheSame = true;
            Object newItems = _clone(_data, type, allowsNull);
            int numberToSet = Math.min(tuple.size()-offsetInTuple, currentSize - startIndex);
            for (int i = 0; i < numberToSet; i++) {
                Object itemToSet = tuple.get(offsetInTuple+i);
                _setAt(startIndex + i, itemToSet, newItems);
                if ( !Objects.equals(itemToSet, _getAt(startIndex+i, _data, type)) )
                    isAlreadyTheSame = false;
            }
            if ( isAlreadyTheSame )
                return this;
            return new LeafNode(newItems);
        }

        @Override
        public <T> void forEach(Class<T> type, Consumer<T> consumer) {
            _each(_data, type, consumer);
        }
    }

    static final class BranchNode implements Node {
        private final Node[] _children;
        private final int _size;

        BranchNode(Node[] children) {
            _children = children;
            int sum = 0;
            for (Node child : children) {
                if ( child != null )
                    sum += child.size();
            }
            _size = sum;
        }

        @Override
        public int size() {
            return _size;
        }

        @Override
        public <T> T getAt(int index, Class<T> type) {
            int currentBranchStartIndex = 0;
            Node[] children = _children;
            Node lastNode = children[0];
            for (Node branch : children) {
                if ( branch != null ) {
                    if (index < currentBranchStartIndex + branch.size()) {
                        lastNode = branch;
                        break;
                    }
                    currentBranchStartIndex += branch.size();
                }
            }
            int childIndex = index - currentBranchStartIndex;
            if (childIndex < 0 || childIndex >= lastNode.size()) {
                throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);
            }
            return lastNode.getAt(childIndex, type);
        }

        @Override
        public @Nullable Node slice(
            final int from,
            final int to,
            final Class<?> type,
            final boolean allowsNull
        ) {
            int currentBranchStartIndex = 0;
            Node @Nullable[] children = _children;
            @Nullable Node[] newChildren = null;
            for ( int i = 0; i < children.length; i++ ) {
                @Nullable Node branch = children[i];
                if ( branch != null ) {
                    if (from < currentBranchStartIndex + branch.size()) {
                        int childFrom = Math.max(0, from - currentBranchStartIndex);
                        int childTo = Math.min(to - currentBranchStartIndex, branch.size());
                        if (childFrom < childTo) {
                            if (newChildren == null)
                                newChildren = new Node[children.length];
                            newChildren[i] = branch.slice(childFrom, childTo, type, allowsNull);
                        }
                    }
                    currentBranchStartIndex += branch.size();
                }
            }
            if ( newChildren == null )
                return null;
            else if ( _isAllNull(newChildren) )
                return null;
            else
                return new BranchNode(newChildren);
        }

        @Override
        public @Nullable Node removeRange(
            final int from,
            final int to,
            final Class<?> type,
            final boolean allowsNull
        ) {
            if ( from == to )
                return this;
            int currentBranchStartIndex = 0;
            Node @Nullable[] children = _children;
            Node[] newChildren = children;
            for ( int i = 0; i < children.length; i++ ) {
                @Nullable Node branch = children[i];
                if ( branch != null ) {
                    int nextPosition = currentBranchStartIndex + branch.size();
                    if (from < nextPosition) {
                        int childFrom = Math.max(0, from - currentBranchStartIndex);
                        int childTo = Math.min(to - currentBranchStartIndex, branch.size());
                        if (childFrom < childTo) {
                            if (newChildren == children)
                                newChildren = children.clone();
                            newChildren[i] = branch.removeRange(childFrom, childTo, type, allowsNull);
                        }
                    }
                    currentBranchStartIndex = nextPosition;
                }
            }
            if ( newChildren == children )
                return this; // this was not affected
            else if ( _isAllNull(newChildren) )
                return null;
            else
                return new BranchNode(newChildren);
        }

        @Override
        public @Nullable <T> Node addAllAt(int index, Tuple<T> tuple, Class<T> type, boolean allowsNull) {
            int currentBranchStartIndex = 0;
            Node @Nullable[] children = _children;
            Node[] newChildren = children;
            int bestIndex = -1;
            int bestIndexStart = -1;
            for ( int i = 0; i < children.length; i++ ) {
                @Nullable Node branch = children[i];
                if ( branch != null ) {
                    int nextPosition = currentBranchStartIndex + branch.size();
                    if ( currentBranchStartIndex <= index && index <= nextPosition ) {
                        bestIndex = i;
                        bestIndexStart = currentBranchStartIndex;
                    }
                    currentBranchStartIndex = nextPosition;
                }
            }
            if (newChildren == children)
                newChildren = children.clone();
            int childIndex = index - bestIndexStart;
            Node branch = newChildren[bestIndex];
            if ( branch == null )
                newChildren[bestIndex] = _createRootFromList(type, allowsNull, tuple.toList());
            else
                newChildren[bestIndex] = branch.addAllAt(childIndex, tuple, type, allowsNull);

            if (newChildren[bestIndex] == branch)
                throw new IllegalStateException("TupleNode was not modified");
            if ( _isAllNull(newChildren) )
                throw new IllegalStateException("TupleNode is all null");
            else
                return new BranchNode(newChildren);
        }

        @Override
        public @Nullable <T> Node setAllAt(int index, int offset, Tuple<T> tuple, Class<T> type, boolean allowsNull) {
            int currentBranchStartIndex = 0;
            Node @Nullable[] children = _children;
            Node[] newChildren = children;
            int endIndex = index + tuple.size();
            for ( int i = 0; i < children.length; i++ ) {
                @Nullable Node branch = children[i];
                if ( branch != null ) {
                    int nextPosition = currentBranchStartIndex + branch.size();
                    if ( endIndex <= currentBranchStartIndex )
                        break;
                    if (currentBranchStartIndex <= index && index < nextPosition) {
                        int childIndex = index - currentBranchStartIndex;
                        if (newChildren == children)
                            newChildren = children.clone();
                        newChildren[i] = branch.setAllAt(childIndex, offset, tuple, type, allowsNull);
                    } else if (currentBranchStartIndex <= endIndex && endIndex < nextPosition) {
                        int childIndex = 0;
                        int additionalOffset = offset + (currentBranchStartIndex - index);
                        if (newChildren == children)
                            newChildren = children.clone();
                        newChildren[i] = branch.setAllAt(childIndex, additionalOffset, tuple, type, allowsNull);
                    } else if (index <= currentBranchStartIndex && nextPosition <= endIndex ) {
                        int childIndex = 0;
                        int additionalOffset = offset + (currentBranchStartIndex - index);
                        if (newChildren == children)
                            newChildren = children.clone();
                        newChildren[i] = branch.setAllAt(childIndex, additionalOffset, tuple, type, allowsNull);
                    }
                    currentBranchStartIndex += branch.size();
                }
            }
            if ( newChildren == children )
                return this; // this was not affected
            else if ( _isAllNull(newChildren) )
                return null;
            else
                return new BranchNode(newChildren);
        }

        @Override
        public <T> void forEach(Class<T> type, Consumer<T> consumer) {
            for (Node branch : _children) {
                if (branch != null)
                    branch.forEach(type, consumer);
            }
        }
    }

    private final int _size;
    private final boolean _allowsNull;
    private final Class<T> _type;
    private final Node _root;

    @SuppressWarnings("NullAway")
    public static <T> TupleTree<T> of(
        boolean allowsNull,
        Class<T> type,
        List<T> items
    ) {
        return new TupleTree(
                items.size(),
                allowsNull,
                type,
                _createInitialRootFromList(type, allowsNull, items)
            );
    }

    public static <T> TupleTree<T> of(
        boolean allowsNull,
        Class<T> type,
        @Nullable T... items
    ) {
        return ofAnyArray(allowsNull, type, items);
    }

    static <T> TupleTree<T> ofAnyArray(
            boolean allowsNull,
            Class<T> type,
            @Nullable Object array
    ) {
        Node node = _createInitialRootFromArray(type, allowsNull, array);
        return new TupleTree(node.size(), allowsNull, type, node);
    }

    static <T> TupleTree<T> ofRaw(
            boolean allowsNull,
            Class<T> type,
            Object data
    ) {
        LeafNode leaf = new LeafNode(data);
        return new TupleTree(
                leaf.size(),
                allowsNull,
                type,
                leaf
        );
    }

    @SuppressWarnings("NullAway")
    private TupleTree(
            int size,
            boolean allowsNull,
            Class<T> type,
            @Nullable Node root
    ) {
        Objects.requireNonNull(type);
        _size = size;
        _allowsNull = allowsNull;
        _type = type;
        _root = root == null ? new LeafNode(_createArray(type, allowsNull, size)) : root;
    }

    private static Node _createInitialRootFromList(Class<?> type, boolean allowsNull, List<?> items) {
        return new LeafNode(_createArrayFromList(type, allowsNull, items));
    }

    private static Node _createInitialRootFromArray(Class<?> type, boolean allowsNull, @Nullable Object arrayFromOutside) {
        if ( arrayFromOutside == null ) {
            if ( allowsNull )
                return new LeafNode(_createArray(type, true, 1));
            else
                throw new NullPointerException("Cannot create a TupleHamt with null items when allowsNull is false");
        }
        return new LeafNode(_createArrayFromArray(type, allowsNull, arrayFromOutside));
    }

    private static Node _createRootFromList(Class<?> type, boolean allowsNull, List<?> items) {
        if ( items.isEmpty() || items.size() < IDEAL_LEAF_NODE_SIZE)
            return new LeafNode(_createArrayFromList(type, allowsNull, items));
        Node[] branches = new Node[BRANCHING_FACTOR];
        int stepSize = items.size() / branches.length;
        for (int i = 0; i < branches.length; i++) {
            int start = i * stepSize;
            int end = (i + 1) * stepSize;
            if (i == branches.length - 1) {
                end = items.size();
            }
            List<?> subList = items.subList(start, end);
            branches[i] = _createRootFromList(type, allowsNull, subList);
        }
        return new BranchNode(branches);
    }


    @Override
    public Class<T> type() {
        return _type;
    }

    @Override
    public int size() {
        return _size;
    }

    @Override
    @SuppressWarnings("NullAway")
    public T get(int index) {
        if (index < 0 || index >= _size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);
        }
        return _root.getAt(index, _type);
    }

    @Override
    public boolean allowsNull() {
        return _allowsNull;
    }

    @Override
    public TupleTree<T> slice(int from, int to) {
        if (from < 0 || to > _size || from > to) {
            throw new IndexOutOfBoundsException("Index: " + from + ", Size: " + _size);
        }
        if ( from > to )
            throw new IllegalArgumentException();

        int newSize = (to - from);
        if ( newSize == this.size() )
            return this;

        if ( newSize == 0 ) {
            Node newRoot = _createRootFromList(_type, _allowsNull, Collections.emptyList());
            return new TupleTree<>(0, _allowsNull, _type, newRoot);
        }
        Node newRoot = _root.slice(from, to, _type, _allowsNull);
        if ( newRoot == _root )
            return this;
        return new TupleTree<>(newSize, _allowsNull, _type, newRoot);
    }

    @Override
    public TupleTree<T> removeRange(int from, int to) {
        if ( from < 0 || to > _size )
            throw new IndexOutOfBoundsException("Index: " + from + ", Size: " + _size);
        if ( from > to )
            throw new IllegalArgumentException();
        int numberOfItemsToRemove = to - from;
        if ( numberOfItemsToRemove == 0 )
            return this;
        if ( numberOfItemsToRemove == this.size() ) {
            return new TupleTree<>(0, _allowsNull, _type, null);
        }
        Node newRoot = _root.removeRange(from, to, _type, _allowsNull);
        if ( newRoot == _root )
            return this;
        return new TupleTree<>(_size - numberOfItemsToRemove, _allowsNull, _type, newRoot);
    }

    @Override
    public TupleTree<T> removeAll(Tuple<T> properties) {
        if (properties.size() == 0) {
            return this;
        }
        List<T> newItems = new ArrayList<>(_size);
        Set<T> toRemove = properties.toSet();
        for (int i = 0; i < _size; i++) {
            T item = get(i);
            if (!toRemove.contains(item)) {
                newItems.add(item);
            }
        }
        if (newItems.size() == _size) {
            return this;
        }
        return new TupleTree<>(newItems.size(), _allowsNull, _type, _createRootFromList(_type, _allowsNull, newItems));
    }

    @Override
    public TupleTree<T> addAt(int index, T item) {
        if ( !this.allowsNull() && item == null )
            throw new NullPointerException();
        if ( index < 0 || index > _size )
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);

        Tuple<T> singleton = _allowsNull ? Tuple.ofNullable(type(), item) : Tuple.of(item);
        Node newRoot = _root.addAllAt(index, singleton, _type, _allowsNull);
        if ( newRoot == _root )
            return this;
        return new TupleTree<>(_size+1, _allowsNull, _type, newRoot);
    }

    @Override
    public TupleTree<T> setAt(int index, T item) {
        if ( index < 0 || index >= _size )
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);
        if ( !this.allowsNull() && item == null )
            throw new NullPointerException();
        return setAllAt(index, _allowsNull ? Tuple.ofNullable(type(), item) : Tuple.of(item));
    }

    @Override
    public TupleTree<T> addAllAt(int index, Tuple<T> tuple) {
        Objects.requireNonNull(tuple);
        if ( tuple.isEmpty() )
            return this; // nothing to do
        if ( !this.allowsNull() && tuple.allowsNull() )
            throw new NullPointerException();
        if ( index < 0 || index > _size )
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);
        Node newRoot = _root.addAllAt(index, tuple, _type, _allowsNull);
        if ( newRoot == _root )
            return this;
        return new TupleTree<>(_size+tuple.size(), _allowsNull, _type, newRoot);
    }

    @Override
    public TupleTree<T> setAllAt(int index, Tuple<T> tuple) {
        Objects.requireNonNull(tuple);
        if ( !this.allowsNull() && tuple.allowsNull() )
            throw new NullPointerException();
        if ( index < 0 || index + tuple.size() > size() )
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + _size);
        Node newRoot = _root.setAllAt(index, 0, tuple, _type, _allowsNull);
        if ( newRoot == _root )
            return this;
        return new TupleTree<>(_size, _allowsNull, _type, newRoot);
    }

    @Override
    public Tuple<T> retainAll(Tuple<T> tuple) {
        Objects.requireNonNull(tuple);
        if ( tuple.isEmpty() ) {
            Node newRoot = _createRootFromList(_type, _allowsNull, Collections.emptyList());
            return new TupleTree<>(0, _allowsNull, _type, newRoot);
        }
        int[] indicesOfThingsToKeep = new int[this.size()];
        int newSize = 0;
        int singleSequenceIndex = size() > 0 ? -2 : -1;
        int retainSequenceSize = 0;
        for ( int i = 0; i < this.size(); i++ ) {
            int index = tuple.firstIndexOf( get(i) );
            if ( index != -1 ) {
                indicesOfThingsToKeep[newSize] = i;
                newSize++;
                if ( singleSequenceIndex != -1 ) {
                    if ( singleSequenceIndex == -2 )
                        singleSequenceIndex = i;
                    else if ( i > singleSequenceIndex + retainSequenceSize )
                        singleSequenceIndex = -1;
                }
                if ( singleSequenceIndex >= 0 )
                    retainSequenceSize++;
            } else {
                indicesOfThingsToKeep[newSize] = -1;
            }
        }
        return _retainAll(singleSequenceIndex, newSize, indicesOfThingsToKeep);
    }

    TupleTree<T> _retainAll(int singleSequenceIndex, int newSize, int[] indicesOfThingsToKeep) {
        if ( newSize == this.size() )
            return this;
        List<T> newItems = new ArrayList<>(newSize);
        for ( int i = 0; i < newSize; i++ ) {
            int index = indicesOfThingsToKeep[i];
            if ( index != -1 )
                newItems.add(get(index));
        }
        Node newRoot = _createRootFromList(_type, _allowsNull, newItems);
        return new TupleTree<>(newItems.size(), _allowsNull, _type, newRoot);
    }

    @Override
    public TupleTree<T> clear() {
        return new TupleTree<>(0, _allowsNull, _type, null);
    }

    @Override
    public TupleTree<T> sort(Comparator<T> comparator) {
        List<T> sortedList = new ArrayList<>(_size);
        for (int i = 0; i < _size; i++) {
            sortedList.add(get(i));
        }
        sortedList.sort(comparator);
        return new TupleTree<>(sortedList.size(), _allowsNull, _type, _createRootFromList(_type, _allowsNull, sortedList));
    }

    @Override
    public TupleTree<T> makeDistinct() {
        Set<T> distinctSet = new LinkedHashSet<>();
        for (int i = 0; i < _size; i++) {
            distinctSet.add(get(i));
        }
        List<T> distinctList = new ArrayList<>(distinctSet);
        return new TupleTree<>(distinctList.size(), _allowsNull, _type, _createRootFromList(_type, _allowsNull, distinctList));
    }

    @Override
    public TupleTree<T> reversed() {
        List<T> reversedList = new ArrayList<>(_size);
        for (int i = _size - 1; i >= 0; i--) {
            reversedList.add(get(i));
        }
        return new TupleTree<>(reversedList.size(), _allowsNull, _type, _createRootFromList(_type, _allowsNull, reversedList));
    }

    private static final class IteratorFrame {
        final Node node;
        final int end;
        int index = 0;
        @Nullable IteratorFrame parent;

        IteratorFrame(Node n, @Nullable IteratorFrame parent) {
            this.node = n;
            this.parent = parent;
            if ( n instanceof LeafNode ) {
                end = n.size();
            } else if ( n instanceof BranchNode ) {
                end = ((BranchNode) n)._children.length;
            }
            else throw new IllegalArgumentException();
        }
    }

    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            private @Nullable IteratorFrame currentFrame = null;
            {
                if (_size > 0)
                    currentFrame = new IteratorFrame(_root, null);
            }
            @Override
            public boolean hasNext() {
                while ( currentFrame != null ) {
                    if ( currentFrame.index >= currentFrame.end ) {
                        currentFrame = currentFrame.parent;
                    } else {
                        if (currentFrame.node instanceof LeafNode) {
                            return true;
                        } else {
                            BranchNode bn = (BranchNode) currentFrame.node;
                            Node[] children = bn._children;
                            Node child = children[currentFrame.index++];
                            if (child != null)
                                currentFrame = new IteratorFrame(child, currentFrame);
                        }
                    }
                }
                return false;
            }

            @Override
            @SuppressWarnings("unchecked")
            public T next() {
                if ( !hasNext() || currentFrame == null )
                    throw new NoSuchElementException();

                if ( currentFrame.node instanceof LeafNode ) {
                    T item = currentFrame.node.getAt(currentFrame.index, _type);
                    currentFrame.index++;
                    return item;
                } else {
                    return next();
                }
            }
        };
    }

    @Override
    public Spliterator<T> spliterator() {
        return new TupleSpliterator<>(0, _size, _type, _allowsNull, _root);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Tuple<");
        sb.append(_type.getSimpleName());
        if (allowsNull())
            sb.append("?");
        sb.append(">[");
        final int howMany = Math.min(_size, 10);
        int numberOfElementsLeft = _size - howMany;
        for (int i = 0; i < howMany; i++) {
            sb.append(get(i));
            if (i < _size - 1)
                sb.append(", ");
        }
        if (numberOfElementsLeft > 0) {
            sb.append("... ").append(numberOfElementsLeft).append(" items left");
        }
        sb.append("]");
        return sb.toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof Tuple))
            return false;
        Tuple<?> other = (Tuple<?>) obj;
        if (other.allowsNull() != this.allowsNull())
            return false;
        if (other.size() != this.size())
            return false;
        if (!other.type().equals(_type))
            return false;
        if ( this._root instanceof LeafNode ) {
            TupleTree<?> otherHamt = null;
             if ( other instanceof TupleTree) {
                 otherHamt = (TupleTree<?>) other;
             } else if ( other instanceof TupleWithDiff<?>) {
                 TupleWithDiff<?> otherDiff = (TupleWithDiff<?>) other;
                 otherHamt = otherDiff.getData();
             }
            if ( otherHamt != null && otherHamt._root instanceof LeafNode ) {
                LeafNode thisLeaf = (LeafNode) this._root;
                LeafNode otherLeaf = (LeafNode) otherHamt._root;
                return Val.equals(thisLeaf._data, otherLeaf._data);
            }
        }
        for (int i = 0; i < this.size(); i++) {
            if (!Objects.equals(this.get(i), other.get(i)))
                return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = _type.hashCode() ^ _size;
        for (int i = 0; i < _size; i++) {
            T item = get(i);
            hash = 31 * hash + (item == null ? 0 : item.hashCode());
        }
        return hash ^ (_allowsNull ? 1 : 0);
    }

    private static final class TupleSpliterator<T> implements Spliterator<T> {
        private final TupleTree.Node root;
        private final Class<T> type;
        private final boolean allowsNull;
        private final int fence;
        private int index; // position in the current node

        TupleSpliterator(int start, int end, Class<T> type, boolean allowsNull, TupleTree.Node root) {
            this.index = start;
            this.fence = end;
            this.type = type;
            this.allowsNull = allowsNull;
            this.root = root;
        }

        @Override
        public @Nullable Spliterator<T> trySplit() {
            int lo = index;
            int mid = (lo + fence) >>> 1;
            if (mid <= lo)
                return null;
            // left half will handle [lo, mid), this spliterator becomes [mid, fence)
            TupleSpliterator<T> prefix = new TupleSpliterator<>(lo, mid, type, allowsNull, root);
            this.index = mid;
            return prefix;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            if (index < fence) {
                T item = root.getAt(index, type);
                index++;
                action.accept(item);
                return true;
            }
            return false;
        }

        @Override
        public void forEachRemaining(Consumer<? super T> action) {
            while (index < fence) {
                T item = root.getAt(index, type);
                index++;
                action.accept(item);
            }
        }

        @Override
        public long estimateSize() {
            return (long) fence - index;
        }

        @Override
        public int characteristics() {
            int c = Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE;
            if (!allowsNull) c |= Spliterator.NONNULL;
            return c;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Comparator<? super T> getComparator() {
            // we don't have a comparator; streams will only call this if SORTED characteristic is set
            throw new IllegalStateException("TupleHamt spliterator is not SORTED");
        }
    }
}