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

package net.ranides.assira.collection.maps;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.function.Function;

import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.Wrapper;
import net.ranides.assira.generic.Wrappers.AbstractWrapper;
import net.ranides.assira.generic.Wrappers.ValueWrapper;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IField;
import net.ranides.assira.reflection.IMethod;

/**
 * This is inverted key pattern.
 *
 * It is not designed to be used as key inside maps.
 * It is designed to make queries against maps in typesafe way.
 *
 * Class contains information about key object and about value type.
 *
 * For example, you can put into {@code Map<object,object>} few tags of different type, and retrieve them later
 * only if value associated with "tag.key" has correct type.
 *
 * @param <K> K
 * @param <V> V
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class Tag<K,V> implements Function<Map<K,? super V>,V> {

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

    /**
     * Creates new key which assigns V value to specifed key
     *
     * @param type value type
     * @param key key
     */
    public Tag(Class<V> type, K key) {
        this.type = IClass.typeinfo(type);
        this.key = key;
    }

    /**
     * Retrieves from provided map value associated with "this" tag.
     * If value inside map has invalid type, returns null
     *
     * @param map map
     * @return value or null
     */
    public V get(Map<K,? super V> map) {
        return cast(type, map.get(key));
    }

    /**
     * Retrieves from provided map values associated with "this" tag.
     * If value inside map has invalid type, it is ignored
     * Returns empty collection if there is no mapping
     *
     * @param map map
     * @return values
     */
    public Collection<? super V> getAll(MultiMap<K, ? super V> map) {
        return CollectionUtils.filter(map.getAll(key), type::isInstance);
    }

    /**
     * Inserts value into provided map
     *
     * @param map map
     * @param value value
     */
    public void put(Map<K, ? super V> map, V value) {
        map.put(key, value);
    }

    /**
     * Inserts values into provided map
     *
     * @param map map
     * @param value value
     */
    public void putAll(MultiMap<K, ? super V> map, Collection<? extends V> value) {
        map.putAll(key, value);
    }

    /**
     * Inserts value into provided map.
     * It takes optional as an argument, if optional is empty, then it won't create association.
     *
     * @param map map
     * @param value value
     */
    public void put(Map<K, ? super V> map, Ref<V> value) {
        value.ifPresent(v -> map.put(key, v));
    }

    /**
     * Checks if map contains value of correct type
     *
     * @param map map
     * @return bool
     */
    public boolean contains(Map<K, ? super V> map) {
        return type.isInstance(map.get(key));
    }

    /**
     * Equivalent to {@link #get(Map)}
     *
     * @param source map
     * @return value or null
     */
    @Override
    public V apply(Map<K, ? super V> source) {
        return get(source);
    }

    /**
     * Gets entry from provided map associated with "this" tag
     * It returns null, if there is no mapping.
     *
     * @param map map
     * @return entry or null
     */
    public Entry<K,V> entry(Map<K, ? super V> map) {
        if(map instanceof TreeMap) {
            return EntryAccess.TREE.$invoke(map, key);
        }
        if(EntryAccess.CS_TREE_SUBMAP.isInstance(map)) {
            Object treemap = EntryAccess.TREE_FIELD.get(map);
            return EntryAccess.TREE.$invoke(treemap, key);
        }
        if(map instanceof HashMap) {
            return EntryAccess.HASH.$invoke(map, key);
        }
        if(map instanceof WeakHashMap) {
            return EntryAccess.WEAK.$invoke(map, key);
        }
        if(contains(map)) {
            return new DVEntry<>(type, key, map);
        }
        return null;
    }

    /**
     * Creates Wrapper object which allows modification of "this" mapping inside provided map
     *
     * Please note, that it returns wrapper (with null value) even if there is no association.
     * This wrapper can be used to initial insert into map.
     *
     * @param map map map
     * @return wrapper wrapper
     */
    public Wrapper<V> wrapper(Map<K, ? super V> map) {
        if(!contains(map)) {
            return new DVEntry<>(type, key, map);
        }
        if(map instanceof TreeMap) {
            return new DVWrapper<>(type, EntryAccess.TREE.$invoke(map, key));
        }
        if(EntryAccess.CS_TREE_SUBMAP.isInstance(map)) {
            Object treemap = EntryAccess.TREE_FIELD.get(map);
            return new DVWrapper<>(type, EntryAccess.TREE.$invoke(treemap, key));
        }
        if(map instanceof HashMap) {
            return new DVWrapper<>(type, EntryAccess.HASH.$invoke(map, key));
        }
        if(map instanceof WeakHashMap) {
            return new DVWrapper<>(type, EntryAccess.WEAK.$invoke(map, key));
        }
        return new DVEntry<>(type, key, map);
    }

    @SuppressWarnings("unchecked")
    private static <T> T cast(IClass<T> type, Object value) {
        return type.isInstance(value) ? (T) value : null;
    }


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

        static final IMethod TREE;
        static final IMethod HASH;
        static final IMethod WEAK;

        static final IClass<?> CS_TREE_SUBMAP;
        static final IField TREE_FIELD;

        static {
            TREE = method(TreeMap.class);
            CS_TREE_SUBMAP = IClass.typeinfo("java.util.TreeMap$NavigableSubMap");
            TREE_FIELD = CS_TREE_SUBMAP.field("m").get();
            
            HASH = method(HashMap.class);
            WEAK = method(WeakHashMap.class);
        }

        private static IMethod method(Class<?> type) {
            return IClass.typeinfo(type).methods().require(IAttribute.DECLARED).require("getEntry").first().get();
        }

    }
    
    private static final class DVWrapper<K,V> extends ValueWrapper<V> {
        
        private final IClass<V> itype;
        private final Entry<K,V> entry;

        public DVWrapper(IClass<V> itype, Entry<K,V> entry) {
            this.itype = itype;
            this.entry = entry;
        }

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

        @Override
        public void set(V value) {
            entry.setValue(value);
        }

        @Override
        public IClass<V> type() {
            return itype;
        }
        
    }

    private static final class DVEntry<K,V> extends AMap.AEntry<K, V> implements AbstractWrapper<V> {

        private final IClass<V> type;
        private final K key;
        private final Map<K,? super V> map;

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

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

        @Override
        public V getValue() {
            return Tag.cast(type, map.get(key));
        }

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

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

        @Override
        public void set(V value) {
            setValue(value);
        }

        @Override
        public IClass<V> type() {
            return type;
        }
        
    }

}
