/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.collection.map;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import net.ranides.assira.generic.Function;
import net.ranides.assira.reflection.util.ObjectUtils;

/**
 * Mapa, która nie zwraca {@code null}, w przypadku, gdy mapowanie nie istnieje, lecz
 * tworząca nowe wiązanie na żądanie. Może tworzyć wiązania dopiero podczas odczytu
 * klucza (np metodą "get") albo natychmiast po wywołaniu metody {@code containsKey}.
 * 
 * <p>
 * Domyślnie kolekcja stara się ograniczyć tworzenie i niszczenie obiektów - raz
 * utworzony obiekt jest zapisywany do mapy. Nawet po jego usunięciu jest przez 
 * pewien czas nadal przechowywany w pamięci podręcznej, i przywracany w razie 
 * potrzeby.
 * </p>
 * <p>
 * Z tego powodu klasa dobrze sprawdza się jako bufor pośredni pomiędzy zewnętrznymi 
 * wolnymi źródłami danych.
 * </p>
 * <p>
 * Uwaga! Jeśli mapa przechowuje wartości, które mogą zmieniać stan, lepiej wyłączyć
 * pamięć podręczną, ponieważ usunięte elementy w późniejszym czasie mogą zostać
 * przywrócone ze starym stanem.
 * </p>
 * @param <K> 
 * @param <V> 
 * @author ranides
 */
@SuppressWarnings("unchecked")
public abstract class LazyMap<K,V> extends HashMap<K,V> implements Function<V,K> {

    private final boolean instant;
    private final Map<K,V> cache;

    /**
     * Tworzy nową mapę z argumentami {@code instant=true} oraz {@code cachesize = 32}
     */
    public LazyMap() {
        this.instant = true;
        this.cache = new CacheMap<>(32);
    }

    /**
     * Tworzy nową mapę z określoną strategią tworzenia obiektów oraz określonym
     * rozmiarem pamięci podręcznej.
     * @param instant 
     * decyduje o tym, kiedy zostanie wysłane żądanie utworzenia
     * nowego powiązania. 
     * <p>Jeśli wartość równa {@code true}, to funkcja {@contains}
     * stara się załadować obiekt i zwrócić {@code true}, jeśli tylko otrzymany wynik
     * będzie różny od {@code null}. Jeśli argument jest ustawiony na {@code false}
     * metoda {@code contains} zwraca informację na temat aktualnej zawartości 
     * w mapie - jeśli klucz nie instnieje, to nie próbuje doładowywać danych.
     * </p><p>
     * Bez względu na wartość argumentu, metoda {@code get} zawsze stara się doładować
     * obiekt, jeśli powiązanie nie znajduje się w mapie.
     * </p>
     *  
     * @param cachesize rozmiar pamięci podręcznej na usunięte elementy. 
     * <p>
     * Klasa stara się minimalizować ilość wywołań funkcji łądującej dane. 
     * W pamięci podręcznej są przechowywane obiekty, które usunięto z mapy, 
     * ale które mogą zostać przywrócone, jeśli nastapi żądanie ich odczytu.
     * </p><p>
     * Jeśli argument = 0, to pamięć podręczna jest wyłączona
     * </p>
     */
    public LazyMap(boolean instant, int cachesize) {
        this.instant = instant;
        this.cache = (0==cachesize) ? null : new CacheMap<K,V>(32);
    }
    
    private V runApply(K key) {
        if(null!=cache && cache.containsKey(key)) {
            return cache.remove(key);
        } else {
            return apply(key);
        }
    }

    @Override
    public boolean containsKey(Object key) {
        K kkey = (K)key;
        if( super.containsKey(kkey) ) {
            return true;
        }
        if( instant ) {
            V value = runApply(kkey);
            if(null != value) {
                put(kkey, value);
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    @Override
    public V get(Object key) {
        K kkey = (K)key;
        if( containsKey(kkey) ) {
            return super.get(kkey);
        }
        if( !instant ) {
            V value = runApply(kkey);
            put(kkey, value);
            return value;
        }
        return null;
    }

    @Override
    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException("Not supported by LazyMap");
    }
    
    @Override
    public V remove(Object key) {
        K kkey = (K)key;
        if(super.containsKey(kkey)) {
            V value = super.remove(kkey);
            if(null!=cache) { cache.put(kkey, value); }
            return value;
        }
        return null;
    }
    
    @Override
    public void clear() {
        clear(false);
    }
    
    /**
     * Czyści zawartość mapy oraz pamięci podręcznej (jeśli argument {@code flush}
     * równy {@code true}. Należy zwrócić uwagę, że zwykła metoda {@link #clear() clear()}
     * nie czyści pamięci podręcznej, lecz stara się do niej przenieść tak wiele
     * ostatnio używanych elementów, jak to możliwe.
     * @param flush czy wyczyścić również pamięć podręczną
     */
    public void clear(boolean flush) {
        if(null!=cache) { 
            if(flush) { cache.clear(); } else { cache.putAll(this); }
        }
        super.clear();
    }
    
    private static final class CacheMap<K,V> extends LinkedHashMap<K, V> {
        
        private static final long serialVersionUID = 3L;
        
        private final int size;
        
        public CacheMap(int size) {
            this.size = size;
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return size() >= size;
        }
    };

/* ************************************************************************** */


    /**
     * Tworzy nową mapę, która w razie potrzeby tworzy nowe obiekty podanej klasy
     * za pomocą konstruktora domyślnego. Argument {@code instant = true}, zaś 
     * pamięć podręczna jest oczywiście wyłączona.
     * @param <K>
     * @param <V>
     * @param value
     * @return
     */
    public static <K,V> LazyMap<K,V> makeInstance(final Class<?> value) {
        return makeInstance(true, value);
    }

    /**
     * Tworzy nową mapę, która w razie potrzeby tworzy nowe obiekty podanej klasy
     * za pomocą konstruktora domyślnego. Pamięć podręczna jest oczywiście wyłączona.
     * @param <K>
     * @param <V>
     * @param instant
     * @param value
     * @return
     */
    public static <K,V> LazyMap<K,V> makeInstance(boolean instant, final Class<?> value) {
        return new LazyMap<K, V>(instant, 0) {
            @Override
            public V apply(K source) {
                return (V)ObjectUtils.create(value);
            }
        };
    }

}
