Cache.java

package neureka.common.utility;

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

/**
 *  This is a simple, fixed size cache for immutable objects which are
 *  shared throughout the library runtime...
 *  This is an internal class which should not be used outside Neurekas internals.
 *
 * @param <O> The type that should be cached, this may be an {@link neureka.ndim.config.NDConfiguration} or {@code int[]} array.
 */
public final class Cache<O> {

    private final Object[] _buffer;
    private int _size = 0;

    public Cache( int size ) {
        _buffer = new Object[ size ];
    }

    /**
     * @param newObject The object which may or may not be cached.
     * @return Either the provided object or the object found inside the cache...
     */
    public <T extends O> T process( T newObject ) {

        int index = _indexFor(newObject);

        O found = _getAt(index);

        if ( _equalsFor( found, newObject ) ) return (T) found;
        else _setAt( index, newObject );

        return newObject;
    }

    public boolean has( O o ) {
        O found = (O) _buffer[ _indexFor( o ) ];
        return found != null && found.equals( o );
    }

    public int size() { return _size; }

    private O _getAt( int index ) {
        return (O) _buffer[ index ];
    }

    private void _setAt( int index, O o ) {
        if ( _buffer[ index ] == null && o != null ) _size++;
        _buffer[ index ] = o;
    }

    private boolean _equalsFor( Object a, Object b ) {
        if ( a != null && b != null ) {
            if (a instanceof int[] && b instanceof int[])
                return Arrays.equals((int[]) a, (int[]) b);
            else
                return Objects.equals(a, b);
        }
        else return false;
    }

    private int _indexFor( Object o ) {
        return o instanceof int[] ? _index((int[]) o) : _index( o.hashCode() );
    }

    private int _index( int key ) {
        return Math.abs(key)% _buffer.length;
    }

    private int _index( int[] data )
    {
        long key = 0;
        for ( int e : data ) {
            if      ( e <=              10 ) key *=              10;
            else if ( e <=             100 ) key *=             100;
            else if ( e <=           1_000 ) key *=           1_000;
            else if ( e <=          10_000 ) key *=          10_000;
            else if ( e <=         100_000 ) key *=         100_000;
            else if ( e <=       1_000_000 ) key *=       1_000_000;
            else if ( e <=      10_000_000 ) key *=      10_000_000;
            else if ( e <=     100_000_000 ) key *=     100_000_000;
            else if ( e <=   1_000_000_000 ) key *=   1_000_000_000;
            key += Math.abs( e ) + 1;
        }
        int rank = data.length;
        while ( rank != 0 ) {
            rank /= 10;
            key *= 10;
        }
        key += data.length;
        return _index(Long.valueOf(key).hashCode());
    }

    /**
     *  Lazy cache entries are entries whose values will be calculated
     *  only when the entry is being stored in the cache.
     *
     * @param <K> The key type parameter.
     * @param <V> The value type parameter.
     */
    public static class LazyEntry<K,V> {

        private final K _key;
        private final Function<K,V> _valueSupplier;

        private V _value = null;

        public LazyEntry( K directory, Function<K,V> valueSupplier ) {
            _key = directory;
            _valueSupplier = valueSupplier;
        }

        public V getValue() {
            if ( _value == null ) _value = _valueSupplier.apply(_key);
            return _value;
        }

        @Override
        public boolean equals(Object o) {
            if ( this == o ) return true;
            if ( o == null || getClass() != o.getClass() ) return false;
            LazyEntry that = (LazyEntry) o;
            return _key.equals(that._key);
        }

        @Override
        public int hashCode() {
            return Objects.hash(_key);
        }

    }

}