/*
 * @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.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.WeakHashMap;
import net.ranides.assira.generic.Function;
import net.ranides.assira.reflection.GenericField;
import net.ranides.assira.reflection.GenericMethod;
import net.ranides.assira.reflection.inspect.ClassInspector;
import net.ranides.assira.reflection.inspect.FieldInspector;
import net.ranides.assira.reflection.inspect.MethodInspector;

/**
 * Klasa, która pozwala na zapisanie nazwy i typu kolumny, która może być potem
 * odczytywana i zapisywana do mapy. Pozwala na opisanie zawartości mapy w type-safe
 * sposób, ambitniej iniż ma to zazwyczaj miejsce za pomocą stringów reprezentujących
 * po prostu nazwy kluczy.
 * @author ranides
 * @param <K>
 * @param <V>
 */
public class Column<K,V> implements Function<V, Map<K,? super V>> {

    private final K key;
    private final Class<V> type;

    /**
     * Tworzy nową kolumnę o podanej nazwie i typie.
     * @param key
     * @param type
     */
    public Column(K key, Class<V> type) {
        this.key = key;
        this.type = type;
    }

    /**
     * Tworzy nową kolumnę o podanej nazwie i typie. Metoda statyczna pozwalająca
     * na type-inference.
     * @param <K>
     * @param <V>
     * @param key
     * @param type
     * @return
     */
    public static <K,V> Column<K,V> make(K key, Class<V> type) {
        return new Column<>(key, type);
    }

    /**
     * Pobiera z podanej mapy wartość zapisaną pod kluczem powiązanym z kolumną.
     * @param map
     * @return
     */
    public V get(Map<K, ? super V> map) {
        return type.cast(map.get(key));
    }

    /**
     * Wstawia podaną wartość do mapy, przypisując do klucza powiązanego z kolumną.
     * @param map
     * @param value
     */
    public void put(Map<K, ? super V> map, V value) {
        map.put(key, value);
    }

    /**
     * @see Column#get
     * @param source
     * @return
     */
    @Override
    public V apply(Map<K, ? super V> source) {
        return get(source);
    }

    /**
     * Zwraca obiekt Entry powiązany z daną kolumną. Wszystkie zmiany wprowadzone
     * w entry są widoczne w mapie źródłowej, i vice versa. Takie Entry pozwala
     * na wielokrotne odczyty i zapisy do podanej kolumny bez konieczności wielokrotnego
     * wyszukiwania klucza w mapie. Map zasadniczo pozytywny wpływ na wydajność
     * w przypadku {@link TreeMap} - używanie Entry dla innych rodzajów map
     * sprawdzają się (minimalnie) gorzej, niż ręczna praca z mapą.
     * @param map
     * @return
     */
    public Entry<K,V> entry(Map<K, V> map) {
        return entry(map, key);
    }

    /**
     * Zwraca obiekt Entry powiązany z kluczem o podanej nazwie. Wszystkie zmiany wprowadzone
     * w entry są widoczne w mapie źródłowej, i vice versa.
     * <p>Metoda statyczna, pozwalająca na utworzenie Entry bez konieczności
     * wcześniejszego definiowania kolumny.</p>
     * @param <K>
     * @param <V>
     * @param map
     * @param key
     * @return
     */
    public static <K,V> Entry<K,V> entry(Map<K, V> map, K key) {
        if(map instanceof TreeMap) {
            return (Map.Entry<K,V>)EntryAccess.TREE.invoke(map, key);
        }
        if(EntryAccess.CS_TREE_SUBMAP.isInstance(map)) {
            Object treemap = EntryAccess.TREE_FIELD.get(map);
            return (Map.Entry<K,V>)EntryAccess.TREE.invoke(treemap, key);
        }
        if(map instanceof HashMap) {
            return (Map.Entry<K,V>)EntryAccess.HASH.invoke(map, key);
        }
        if(map instanceof WeakHashMap) {
            return (Map.Entry<K,V>)EntryAccess.WEAK.invoke(map, key);
        }
        return new DVEntry<>(map, key);
    }


    static final class EntryAccess { // NOPMD - lazy init idiom

        static final GenericMethod TREE;
        static final GenericMethod HASH;
        static final GenericMethod WEAK;

        static final Class<?> CS_TREE_SUBMAP;
        static final GenericField<?> TREE_FIELD;

        static {
            TREE = MethodInspector.inclass(TreeMap.class).declared().name("getEntry").first();
            CS_TREE_SUBMAP = ClassInspector.forName("java.util.TreeMap$NavigableSubMap");
            TREE_FIELD = FieldInspector.find(CS_TREE_SUBMAP, "m");
            
            HASH = MethodInspector.inclass(HashMap.class).declared().name("getEntry").first();
            WEAK = MethodInspector.inclass(WeakHashMap.class).declared().name("getEntry").first();
        }

    }

    private static final class DVEntry<K,V> implements Map.Entry<K, V> {

        private final Map<K,V> map;
        private final K key;

        public DVEntry(Map<K, V> map, K key) {
            this.map = map;
            this.key = key;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return map.get(key);
        }

        @Override
        public V setValue(V value) {
            return map.put(key, value);
        }

    }

}
