/*
 * @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.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.collection.SetUtils;
import net.ranides.assira.generic.BiFunction;
import net.ranides.assira.generic.Function;

/**
 * Abstrakcyjna klasa - adapter, po której dziedziczą implementacje prezentujące
 * obiekty funkcyjne jako interfejs map. Zobacz opis metody {@link VirtualMap#produce}.
 * @author ranides
 * @param <K>
 * @param <V>
 */
public abstract class VirtualMap<K, V> implements Map<K, V> {

    private final String message;

    /**
     *
     * @param message
     */
    protected VirtualMap(String message) {
        this.message = message;
    }

    //<editor-fold defaultstate="collapsed" desc="default implementation: always throws exceptions">

    @Override
    public int size() {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public boolean isEmpty() {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public boolean containsKey(Object key) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public V get(Object key) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public V put(K key, V value) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public V remove(Object key) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> values) {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public Set<K> keySet() {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public Collection<V> values() {
        throw new UnsupportedOperationException(message);
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        throw new UnsupportedOperationException(message);
    }

    //</editor-fold>

    /**
     * Tworzy adapter, który podaną funkcję prezentuje jako interfejs {@code Map}.
     * Uwaga: zwrócona mapa nie obsługuje większości metod:
     * <ul>
     *   <li>nie można modyfikować</li>
     *   <li>nie można iterować po wartościach</li>
     *   <li>nie można sprawdzić rozmiaru</li>
     *   <li>można sprawdzić istnienie klucza</li>
     *   <li>można odczytać wartość przypisaną do klucza</li>
     * </ul>
     *
     * @param <K>
     * @param <V>
     * @param generator
     * @return
     */
    public static <K,V> Map<K,V> produce(Function<V,K> generator) {
        return new VirtualMap.Apply<>(generator);
    }

    /**
     * Tworzy adapter, który podaną funkcję prezentuje jako interfejs {@code Map}.
     * Mapa obsługuje większość metod, jednak nie można modyfikować wartości
     * przypisanych do kluczy ani dodawać nowych. Jeśli przekazany argument
     * {@code domain} pozwala na usuwanie elementów, to można z mapy przypisania usuwać.
     *
     * @param <K>
     * @param <V>
     * @param domain
     * @param generator
     * @return
     */
    public static <K,V> Map<K,V> produce(Set<K> domain, BiFunction<V,K> generator) {
        return new VirtualMap.DomApply<>(domain, generator);
    }

    private static final class Apply<K, V> extends VirtualMap<K, V> {

        private final Function<V, K> generator;

        public Apply(Function<V, K> generator) {
            super("Not supported by infinite virtual maps.");
            this.generator = generator;
        }

        @Override
        public boolean containsKey(Object key) {
            return null != generator.apply((K) key);
        }

        @Override
        public V get(Object key) {
            return generator.apply((K) key);
        }

    }

    private static final class DomApply<K,V> extends VirtualMap<K, V> {

        private final Set<K> domain;
        private final BiFunction<V,K> generator;

        public DomApply(Set<K> domain, BiFunction<V, K> generator) {
            super("Not supported by virtual maps.");
            this.domain = domain;
            this.generator = generator;
        }

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

        @Override
        public boolean isEmpty() {
            return domain.isEmpty();
        }

        @Override
        public boolean containsKey(Object key) {
            return domain.contains(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return domain.contains(generator.inverse((V)value));
        }

        @Override
        public V get(Object key) {
            if( domain.contains(key)) {
                return generator.apply((K)key);
            } else {
                return null;
            }
        }

        @Override
        public V remove(Object key) {
            if(domain.remove(key)) {
                return generator.apply((K)key);
            } else {
                return null;
            }
        }

        @Override
        public void clear() {
            domain.clear();
        }

        @Override
        public Set<K> keySet() {
            return domain;
        }

        @Override
        public Collection<V> values() {
            return CollectionUtils.adapt(domain, generator);
        }

        @Override
        public Set<Entry<K, V>> entrySet() {
            return SetUtils.adapt(domain, new BiFunction<Entry<K, V>,K>(){
                @Override
                public Entry<K, V> apply(K source) {
                    return new AbstractMap.SimpleImmutableEntry<>(source, generator.apply(source));
                }
                @Override
                public K inverse(Entry<K, V> value) {
                    return value.getKey();
                }
            });
        }

    }

}
