LinkedAssociation.java

package sprouts.impl;

import org.jspecify.annotations.Nullable;
import sprouts.Association;
import sprouts.Pair;
import sprouts.Tuple;
import sprouts.ValueSet;

import java.util.*;
import java.util.stream.StreamSupport;

final class LinkedAssociation<K,V> implements Association<K, V>
{
    private static final class LinkedEntry<K, V> {
        private final V value;
        private final @Nullable K previousKey;
        private final @Nullable K nextKey;

        LinkedEntry(V value, @Nullable K previousKey, @Nullable K nextKey) {
            this.value = value;
            this.previousKey = previousKey;
            this.nextKey = nextKey;
        }
        V value() {
            return this.value;
        }
        @Nullable
        K previousKey() {
            return this.previousKey;
        }
        @Nullable
        K nextKey() {
            return this.nextKey;
        }
        LinkedEntry<K,V> withValue(V value) {
            return new LinkedEntry<>(value, this.previousKey, this.nextKey);
        }
        LinkedEntry<K,V> withPreviousKey(@Nullable K previousKey) {
            return new LinkedEntry<>(this.value, previousKey, this.nextKey);
        }
        LinkedEntry<K,V> withNextKey(@Nullable K nextKey) {
            return new LinkedEntry<>(this.value, this.previousKey, nextKey);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof LinkedEntry)) return false;
            LinkedEntry<?, ?> entry = (LinkedEntry<?, ?>) obj;
            return Objects.equals(value, entry.value) &&
                   Objects.equals(previousKey, entry.previousKey) &&
                   Objects.equals(nextKey, entry.nextKey);
        }
        @Override
        public int hashCode() {
            return Objects.hash(value, previousKey, nextKey);
        }
        @Override
        public String toString() {
            return "LinkedEntry[value=" + value +
                   ", previousKey=" + previousKey +
                   ", nextKey=" + nextKey + "]";
        }
    }

    private final Class<V> _valueType;
    private final AssociationImpl<K, LinkedEntry<K, V>> _entries;
    private final @Nullable K _firstInsertedKey;
    private final @Nullable K _lastInsertedKey;

    LinkedAssociation(
        final Class<K> keyType,
        final Class<V> valueType
    ) {
        this(valueType, new AssociationImpl(keyType, LinkedEntry.class), null, null);
    }

    private LinkedAssociation(
            final Class<V> valueType,
            final AssociationImpl<K, LinkedEntry<K, V>> entries,
            final @Nullable K firstInsertedKey,
            final @Nullable K lastInsertedKey
    ) {
        _valueType = valueType;
        _entries = entries;
        _firstInsertedKey = firstInsertedKey != null ? firstInsertedKey : lastInsertedKey;
        _lastInsertedKey = lastInsertedKey;
    }



    @Override
    public int size() {
        return _entries.size();
    }

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

    @Override
    public boolean isSorted() {
        return false;
    }

    @Override
    public Class<K> keyType() {
        return _entries.keyType();
    }

    @Override
    public Class<V> valueType() {
        return _valueType;
    }

    @Override
    public ValueSet<K> keySet() {
        return StreamSupport.stream(spliterator(), false)
                .reduce(
                        new LinkedValueSet<>(keyType()),
                        (set, pair) -> (LinkedValueSet<K>) set.add(pair.first()),
                        (a, b) -> a
                );
    }

    @Override
    public Tuple<V> values() {
        return StreamSupport.stream(spliterator(), false)
                .map(Pair::second)
                .collect(Tuple.collectorOf(valueType()));
    }

    @Override
    public boolean containsKey(K key) {
        return _entries.containsKey(key);
    }

    @Override
    public Optional<V> get(K key) {
        return _entries.get(key).map(LinkedEntry::value);
    }

    @Override
    public Association<K, V> put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("Key and value must not be null");
        }
        if ( !_entries.keyType().isAssignableFrom(key.getClass()) ) {
            throw new IllegalArgumentException(
                    "The given key '" + key + "' is of type '" + key.getClass().getSimpleName() + "', " +
                            "instead of the expected type '" + _entries.keyType() + "'."
            );
        }
        if ( !_valueType.isAssignableFrom(value.getClass()) ) {
            throw new IllegalArgumentException(
                    "The given value '" + value + "' is of type '" + value.getClass().getSimpleName() + "', " +
                            "instead of the expected type '" + _valueType + "'."
            );
        }
        Optional<LinkedEntry<K, V>> entry = _entries.get(key);
        if (entry.isPresent()) {
            if (entry.get().value().equals(value)) {
                // If the value is the same, we do not need to change anything
                return this;
            }
            LinkedEntry<K, V> existingEntry = entry.get();
            AssociationImpl<K, LinkedEntry<K, V>> newEntries = (AssociationImpl)_entries.put(
                    key,
                    existingEntry.withValue(value)
            );
            return new LinkedAssociation<>(valueType(), newEntries, _firstInsertedKey, _lastInsertedKey);
        } else {
            // If the key does not exist, we create a new entry
            LinkedEntry<K, V> newEntry = new LinkedEntry<>(value, _lastInsertedKey, null);
            AssociationImpl<K, LinkedEntry<K, V>> newEntries = (AssociationImpl)_entries.put(
                    key,
                    newEntry
            );
            if (_lastInsertedKey != null) {
                // Update the previous entry's nextKey to point to the new key
                Optional<LinkedEntry<K, V>> lastEntry = _entries.get(_lastInsertedKey);
                if (lastEntry.isPresent()) {
                    LinkedEntry<K, V> updatedLastEntry = lastEntry.get().withNextKey(key);
                    newEntries = (AssociationImpl)newEntries.put(
                            _lastInsertedKey,
                            updatedLastEntry
                    );
                }
            }
            return new LinkedAssociation<>(valueType(), newEntries, _firstInsertedKey, key);
        }
    }

    @Override
    public Association<K, V> putIfAbsent(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("Key and value must not be null");
        }
        if ( !_entries.keyType().isAssignableFrom(key.getClass()) ) {
            throw new IllegalArgumentException(
                    "The given key '" + key + "' is of type '" + key.getClass().getSimpleName() + "', " +
                    "instead of the expected type '" + _entries.keyType() + "'."
                );
        }
        if ( !_valueType.isAssignableFrom(value.getClass()) ) {
            throw new IllegalArgumentException(
                    "The given value '" + value + "' is of type '" + value.getClass().getSimpleName() + "', " +
                    "instead of the expected type '" + _valueType + "'."
                );
        }
        Optional<LinkedEntry<K, V>> entry = _entries.get(key);
        if (entry.isPresent()) {
            // If the key already exists, we do nothing
            return this;
        } else {
            // If the key does not exist, we create a new entry
            LinkedEntry<K, V> newEntry = new LinkedEntry<>(value, _lastInsertedKey, null);
            AssociationImpl<K, LinkedEntry<K, V>> newEntries = (AssociationImpl)_entries.put(
                    key,
                    newEntry
            );
            if (_lastInsertedKey != null) {
                // Update the previous entry's nextKey to point to the new key
                Optional<LinkedEntry<K, V>> lastEntry = _entries.get(_lastInsertedKey);
                if (lastEntry.isPresent()) {
                    LinkedEntry<K, V> updatedLastEntry = lastEntry.get().withNextKey(key);
                    newEntries = (AssociationImpl)newEntries.put(
                            _lastInsertedKey,
                            updatedLastEntry
                    );
                }
            }
            return new LinkedAssociation<>(valueType(), newEntries, _firstInsertedKey, key);
        }
    }

    @Override
    public Association<K, V> remove(K key) {
        if (Util.refEquals(key, null)) {
            throw new NullPointerException("Key must not be null");
        }
        Optional<LinkedEntry<K, V>> entry = _entries.get(key);
        if (entry.isPresent()) {
            K firstInsertedKey = _firstInsertedKey;
            K lastInsertedKey = _lastInsertedKey;
            if (firstInsertedKey != null && firstInsertedKey.equals(key)) {
                // If we are removing the first inserted key, we need to update it
                firstInsertedKey = entry.get().nextKey();
            }
            if (lastInsertedKey != null && lastInsertedKey.equals(key)) {
                // If we are removing the last inserted key, we need to update it
                lastInsertedKey = entry.get().previousKey();
            }
            LinkedEntry<K, V> existingEntry = entry.get();
            AssociationImpl<K, LinkedEntry<K, V>> newEntries = (AssociationImpl)_entries.remove(key);
            if (existingEntry.previousKey() != null) {
                // Update the previous entry's nextKey to point to the next key
                Optional<LinkedEntry<K, V>> previousEntry = _entries.get(existingEntry.previousKey());
                if (previousEntry.isPresent()) {
                    LinkedEntry<K, V> updatedPreviousEntry = previousEntry.get().withNextKey(existingEntry.nextKey());
                    newEntries = (AssociationImpl)newEntries.put(
                            existingEntry.previousKey(),
                            updatedPreviousEntry
                    );
                }
            }
            if (existingEntry.nextKey() != null) {
                // Update the next entry's previousKey to point to the previous key
                Optional<LinkedEntry<K, V>> nextEntry = _entries.get(existingEntry.nextKey());
                if (nextEntry.isPresent()) {
                    LinkedEntry<K, V> updatedNextEntry = nextEntry.get().withPreviousKey(existingEntry.previousKey());
                    newEntries = (AssociationImpl)newEntries.put(
                            existingEntry.nextKey(),
                            updatedNextEntry
                    );
                }
            }
            return new LinkedAssociation<>(valueType(), newEntries, firstInsertedKey, lastInsertedKey);
        }
        return this; // If the key does not exist, we do nothing
    }

    @Override
    public Association<K, V> clear() {
        AssociationImpl<K, LinkedEntry<K, V>> clearedEntries = (AssociationImpl<K, LinkedEntry<K, V>>) _entries.clear();
        return new LinkedAssociation<>(valueType(), clearedEntries, null, null);
    }

    @Override
    public Map<K, V> toMap() {
        return new AbstractMap<K, V>() {
            @Override
            public @Nullable V get(Object key) {
                return LinkedAssociation.this.get((K) key).orElse(null);
            }

            @Override
            public Set<Entry<K, V>> entrySet() {
                return new AbstractSet<Entry<K, V>>() {
                    @Override
                    public Iterator<Entry<K, V>> iterator() {
                        return StreamSupport.stream(LinkedAssociation.this.spliterator(), false)
                                .map(pair -> (Map.Entry<K,V>)new AbstractMap.SimpleEntry<>(pair.first(), pair.second()))
                                .iterator();
                    }

                    @Override
                    public int size() {
                        return LinkedAssociation.this.size();
                    }
                };
            }
        };
    }

    @Override
    public Spliterator<Pair<K,V>> spliterator() {
        return Spliterators.spliterator(iterator(), _entries.size(),
                        Spliterator.ORDERED |
                        Spliterator.DISTINCT |
                        Spliterator.SIZED |
                        Spliterator.SUBSIZED |
                        Spliterator.NONNULL |
                        Spliterator.IMMUTABLE
                    );
    }

    @Override
    public Iterator<Pair<K, V>> iterator() {
        return new Iterator<Pair<K, V>>() {
            private @Nullable K currentKey = null;
            private @Nullable K nextKey = _firstInsertedKey;

            @Override
            public boolean hasNext() {
                return nextKey != null;
            }

            @Override
            public Pair<K, V> next() {
                if (!hasNext() || nextKey == null) {
                    throw new NoSuchElementException();
                }
                currentKey = nextKey;
                LinkedEntry<K, V> entry = _entries.get(currentKey).orElseThrow(NoSuchElementException::new);
                nextKey = entry.nextKey();
                return Pair.of(currentKey, entry.value());
            }
        };
    }


    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("LinkedAssociation<");
        sb.append(keyType().getSimpleName()).append(",");
        sb.append(_valueType.getSimpleName()).append(">[");
        final int howMany = 8;
        sb = _appendRecursivelyUpTo(sb, howMany);
        int numberOfEntriesLeft = size() - howMany;
        if ( numberOfEntriesLeft > 0 ) {
            sb.append(", ...").append(numberOfEntriesLeft).append(" more entries");
        }
        sb.append("]");
        return sb.toString();
    }

    private StringBuilder _appendRecursivelyUpTo(StringBuilder sb, int howMany) {
        if (howMany <= 0) {
            return sb;
        }
        Iterator<Pair<K, V>> it = iterator();
        int count = 0;
        while (it.hasNext() && count < howMany) {
            Pair<K, V> pair = it.next();
            String keyString = Util._toString(pair.first(), keyType());
            String valueString = Util._toString(pair.second(), valueType());
            sb.append(keyString).append(" ↦ ").append(valueString);
            if (it.hasNext()) {
                sb.append(", ");
            }
            count++;
        }
        return sb;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof LinkedAssociation))
            return false;
        LinkedAssociation<?, ?> other = (LinkedAssociation<?, ?>) o;
        if ( !_valueType.equals(other._valueType) )
            return false;
        if ( !_entries.keyType().equals(other._entries.keyType()) )
            return false;
        if ( _entries.size() != other._entries.size() )
            return false;

        return this.toMap().equals(other.toMap());
    }

    @Override
    public int hashCode() {
        int result = _valueType.hashCode();
        result = 31 * result + _entries.keyType().hashCode();
        result = 31 * result + _entries.size();
        result = 31 * result + toMap().hashCode();
        return result;
    }

}