/*
 * @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.function.IntUnaryOperator;
import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.ACollection;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.collection.sets.IntSet;
import net.ranides.assira.collection.sets.IntSetUtils;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.functional.covariant.ProjectionFunction;
import net.ranides.assira.generic.CompareUtils;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.IntPredicate;

/**
 * Utility methods for IntMap, especially virtual transformations.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class IntMapUtils {

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view does not support modifications.
     *
     * @param map source map
     * @param function mapping function
     * @param <TV> source type
     * @param <RV> target type
     * @return read-only view
     */
    public static <TV, RV> IntMap<RV> valueAdapter(IntMap<TV> map, Function<? super TV, ? extends RV> function) {
        return new MapAdapter<>(map, function, null);
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses passed projection for conversion in both directions.
     *
     * @param map source map
     * @param projection mapping projection
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMap<RV> valueProjection(IntMap<TV> map, ProjectionFunction<TV, RV> projection) {
        return new MapAdapter<>(map, projection, (Serializable & BiFunction<Integer,RV,RV>)(k,v) -> projection.apply(map.put(k, projection.invert(v))));

    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses "inserter" function to change map.
     * It passes to inserter "key" and "new value".
     *
     * @param map source map
     * @param function mapping function
     * @param inserter inverted mapping, used for insertion
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMap<RV> valueProjection(IntMap<TV> map, Function<? super TV, ? extends RV> function, BiFunction<Integer, ? super RV, ? extends RV> inserter) {
        return new MapAdapter<>(map, function, inserter);
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses "replacer" function to change map.
     * It passes to replacer "old value" and "new value".
     *
     * @param map source map
     * @param function function
     * @param replacer replacer
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMap<RV> valueDelegate(IntMap<TV> map, Function<? super TV, ? extends RV> function, BiFunction<? super TV, ? super RV, ? extends RV> replacer) {
        return new MapDelegate<>(map, function, replacer);
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view does not support modifications.
     *
     * Please note, that mapping should avoid degenerating multimap into multimap which
     * contains more than one pair of (k,v) where BOTH k and v are equal.
     *
     * @param map source map
     * @param function mapping
     * @param <TV> source type
     * @param <RV> target type
     * @return read-only view
     */
    public static <TV, RV> IntMultiMap<RV> valueAdapter(IntMultiMap<TV> map, Function<? super TV, ? extends RV> function) {
        return new MultiMapAdapter<>(map, function, null);
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses passed projection for conversion in both directions.
     *
     * Please note, that mapping should avoid degenerating multimap into multimap which
     * contains more than one pair of (k,v) where BOTH k and v are equal.
     *
     * @param map source map
     * @param projection mapping projection
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMultiMap<RV> valueProjection(IntMultiMap<TV> map, ProjectionFunction<TV, RV> projection) {
        return new MultiMapAdapter<>(map, projection, (k,v) -> projection.apply(map.put(k, projection.invert(v))));
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses second function to apply inverted conversion.
     *
     * Please note, that mapping should avoid degenerating multimap into multimap which
     * contains more than one pair of (k,v) where BOTH k and v are equal.
     *
     * @param map source map
     * @param function mapping function
     * @param inserter inverted mapping, used for insertion
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMultiMap<RV> valueProjection(IntMultiMap<TV> map, Function<? super TV, ? extends RV> function, BiFunction<Integer, ? super RV, ? extends RV> inserter) {
        return new MultiMapAdapter<>(map, function, inserter);
    }

    /**
     * Creates view which transforms values stored inside source map.
     * Returned view supports modifications: it uses "replacer" function to change map.
     * It passes to replacer "old value" and "new value".
     *
     * Please note, that mapping should avoid degenerating multimap into multimap which
     * contains more than one pair of (k,v) where BOTH k and v are equal.
     *
     * @param map source map
     * @param function function
     * @param replacer replacer
     * @param <TV> source type
     * @param <RV> target type
     * @return mutable view
     */
    public static <TV, RV> IntMultiMap<RV> valueDelegate(IntMultiMap<TV> map, Function<? super TV, ? extends RV> function, BiFunction<? super TV, ? super RV, ? extends RV> replacer) {
        return new MultiMapDelegate<>(map, function, replacer);
    }

    /**
     * Returns read-only view which filters keys and maps them by provided function
     *
     * At access time it checks if provided key is accepted by "acceptor"
     * At access time it maps accepted key by "function" and returns value from source map.
     *
     * @param content content
     * @param acceptor acceptor
     * @param <V> V
     * @return view
     */
    public static <V> IntMap<V> keyFilter(IntMap<V> content, IntPredicate acceptor) {
        return new KeyFilter<>(content, acceptor);
    }

    /**
     * It cretes read-only view of map which keys are created by provided functions.
     * Maps keys at access time, before lookup.
     *
     * @param map map
     * @param keyMapper keyMapper
     * @param <V> V
     * @return view
     */
    public static <V> IntMap<V> keyMap(IntMap<? extends V> map, IntUnaryOperator keyMapper) {
        return new KeyAdapter<>(map, keyMapper, Function.identity());
    }

    /**
     * It cretes read-only view of map which keys are created by provided functions.
     * Maps keys at access time, before lookup.
     *
     * @param map map
     * @param keyMapper keyMapper
     * @param <TV> TV
     * @param <RV> RV
     * @return view
     */
    public static <TV, RV> IntMap<RV> keyMap(IntMap<? extends TV> map, IntUnaryOperator keyMapper, Function<TV, RV> valueMapper) {
        return new KeyAdapter<>(map, keyMapper, valueMapper);
    }

    private static class MultiMapAdapter<TV, RV> extends MapAdapter<TV, RV> implements IntMultiMap<RV> {

        private static final long serialVersionUID = 1L;

        private final IntMultiMap<TV> mcontent;

        public MultiMapAdapter(IntMultiMap<TV> mcontent, Function<? super TV, ? extends RV> function, BiFunction<Integer, ? super RV, ? extends RV> setter) {
            super(mcontent, function, setter);
            this.mcontent = mcontent;
        }

        @Override
        public Collection<RV> getAll(int key) {
            return CollectionUtils.map(mcontent.getAll(key), function);
        }

    }

    private static class MultiMapDelegate<TV, RV> extends MapDelegate<TV,RV> implements IntMultiMap<RV> {

        private static final long serialVersionUID = 1L;

        private final IntMultiMap<TV> mcontent;

        public MultiMapDelegate(IntMultiMap<TV> content, Function<? super TV, ? extends RV> getter, BiFunction<? super TV, ? super RV, ? extends RV> setter) {
            super(content, getter, setter);
            this.mcontent = content;
        }

        @Override
        public Collection<RV> getAll(int key) {
            return CollectionUtils.map(mcontent.getAll(key), getter);
        }

    }

    private static class MapAdapter<TV, RV> extends AIntMap<RV> {
        
        private static final long serialVersionUID = 1L;
        
        @SuppressWarnings("raw")
        private static final BiFunction NSE = (Serializable & BiFunction)(a,b) -> {
            throw new UnsupportedOperationException(); 
        };

        protected final IntMap<TV> content;
        protected final Function<? super TV, ? extends RV> function;
        protected final BiFunction<Integer, ? super RV, ? extends RV> inserter;

        @SuppressWarnings("unchecked")
        public MapAdapter(IntMap<TV> content, Function<? super TV, ? extends RV> function, BiFunction<Integer, ? super RV, ? extends RV> inserter) {
            this.content = content;
            this.function = function;
            this.inserter = null != inserter ? inserter : NSE;
        }

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

        @Override
        public boolean containsKey(int key) {
            return content.containsKey(key);
        }
        
        @Override
        public boolean containsValue(Object value) {
            return content.values().stream().map(function).anyMatch(rv -> CompareUtils.equals(rv, value));
        }

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

        @Override
        public RV remove(int key) {
            TV prev = content.remove(key);
            return prev==null ? null : function.apply(prev);
        }

        @Override
        public IntSet keySet() {
            return content.keySet();
        }

        @Override
        public Collection<RV> values() {
            return CollectionUtils.map(content.values(), function);
        }
        
        @Override
        public Set<IntEntry<RV>> fastEntrySet() {
            return SetUtils.map(content.fastEntrySet(), e -> new EntryAdapter(e));
        }

        @Override
        public RV get(int key) {
            return function.apply(content.get(key));
        }
        
        @Override
        public RV put(int key, RV value) {
            return inserter.apply(key, value);
        }
        

        private final class EntryAdapter extends AIntEntry<RV> {
            
            private final IntEntry<TV> delegate;

            public EntryAdapter(IntEntry<TV> delegate) {
                this.delegate = delegate;
            }

            @Override
            public int getIntKey() {
                return delegate.getIntKey();
            }

            @Override
            public RV getValue() {
                return function.apply(delegate.getValue());
            }

            @Override
            public RV setValue(RV value) {
                return inserter.apply(delegate.getKey(), value);
            }

        }

    }

    private static class MapDelegate<TV, RV> extends AIntMap<RV> implements IntMap<RV> {

        private static final long serialVersionUID = 1L;

        protected final IntMap<TV> content;        
        protected final Function<? super TV, ? extends RV> getter;
        protected final BiFunction<? super TV, ? super RV, ? extends RV> setter;

        public MapDelegate(IntMap<TV> content, Function<? super TV, ? extends RV> getter, BiFunction<? super TV, ? super RV, ? extends RV> setter) {
            this.content = content;
            this.getter = getter;
            this.setter = setter;
        }

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

        @Override
        public boolean containsKey(int key) {
            return content.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return values().contains(value);
        }

        @Override
        public RV get(int key) {
            TV var = content.get(key);
            return null!=var ? getter.apply(var) : null;
        }

        @Override
        public RV put(int key, RV value) {
            TV var = content.get(key);
            return null!=var ? setter.apply(var, value) : null;
        }
        
        @Override
        public IntSet keySet() {
            return content.keySet();
        }

        @Override
        public Collection<RV> values() {
            return CollectionUtils.map(content.values(), getter);
        }
        
        @Override
        public Set<IntEntry<RV>> fastEntrySet() {
            return SetUtils.map(content.fastEntrySet(), e -> new EntryWrapper(e));
        }
        
        private final class EntryWrapper extends AIntEntry<RV> {
            
            private final IntEntry<TV> delegate;

            public EntryWrapper(IntEntry<TV> delegate) {
                this.delegate = delegate;
            }

            @Override
            public int getIntKey() {
                return delegate.getKey();
            }

            @Override
            public RV getValue() {
                return getter.apply(delegate.getValue());
            }

            @Override
            public RV setValue(RV value) {
                return setter.apply(delegate.getValue(), value);
            }

        }
        
    }

    private static class KeyAdapter<TV, RV> extends AIntMap<RV> {

        private final IntMap<? extends TV> content;

        private final IntUnaryOperator kmap;

        private final Function<TV, RV> vmap;

        public KeyAdapter(IntMap<? extends TV> content, IntUnaryOperator kmap, Function<TV, RV> vmap) {
            this.content = content;
            this.kmap = kmap;
            this.vmap = vmap;
        }

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

        @Override
        public boolean containsKey(int key) {
            return content.containsKey(kmap.applyAsInt(key));
        }

        @Override
        public RV get(int key) {
            return vmap.apply(content.get(kmap.applyAsInt(key)));
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean containsValue(Object value) {
            try {
                return content.containsValue(vmap.apply((TV) value));
            } catch (ClassCastException e) {
                return false;
            }
        }

        @Override public IntSet keySet() {
            return IntSetUtils.map(content.keySet(), kmap);
        }

        @Override public Collection<RV> values() {
            return CollectionUtils.map(content.values(), vmap);
        }

        @Override public void putAll(Map<? extends Integer, ? extends RV> values) {
            super.putAll(values);
        }

        @Override public Set<IntEntry<RV>> fastEntrySet() {
            return SetUtils.map(content.entrySet(), e -> new BasicEntry<>(kmap.applyAsInt(e.getKey()), vmap.apply(e.getValue())));
        }

    }

    private static class KeyFilter<V> extends AIntMap<V> {

        protected final IntMap<V> content;
        protected final IntPredicate filter;

        KeyFilter(IntMap<V> content, IntPredicate acceptor) {
            this.content = content;
            this.filter = acceptor;
        }

        @Override public Set<IntEntry<V>> fastEntrySet() {
            return new KeyMapEntrySet();
        }

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

        @Override
        public boolean containsValue(Object value) {
            return content.containsValue(value);
        }

        @Override
        public V get(int key) {
            return filter.test(key) ? content.get(key) : null;
        }

        @Override
        public V getOrDefault(Object key, V defaultValue) {
            return (key instanceof Integer) && filter.test((int)key) ? content.getOrDefault(key, defaultValue) : defaultValue;
        }

        @Override
        public boolean containsKey(int key) {
            return filter.test(key) && content.containsKey(key);
        }

        @Override
        public V put(int key, V value) {
            return content.put(key, value);
        }

        @Override
        public void putAll(Map<? extends Integer, ? extends V> m) {
            for (Map.Entry<? extends Integer, ? extends V> e : m.entrySet()) {
                int key = e.getKey();
                V value = e.getValue();
                content.put(key, value);
            }
        }

        @Override
        public V remove(int key) {
            return filter.test(key) ? content.remove(key) : null;
        }

        @Override
        public V putIfAbsent(Integer key, V value) {
            return content.putIfAbsent(key, value);
        }

        @Override
        public boolean remove(int key, Object value) {
            return filter.test(key) && content.remove(key, value);
        }

        @Override
        public boolean replace(Integer key, V oldValue, V newValue) {
            return content.replace(key, oldValue, newValue);
        }

        @Override
        public V replace(Integer key, V value) {
            return content.replace(key, value);
        }

        @Override
        public V computeIfAbsent(Integer key, Function<? super Integer, ? extends V> mappingFunction) {
            return content.computeIfAbsent(key, mappingFunction);
        }

        @Override
        public V computeIfPresent(Integer key, BiFunction<? super Integer, ? super V, ? extends V> remappingFunction) {
            return content.computeIfPresent(key, remappingFunction);
        }

        @Override
        public V compute(Integer key, BiFunction<? super Integer, ? super V, ? extends V> remappingFunction) {
            return content.compute(key, remappingFunction);
        }

        @Override
        public V merge(Integer key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
            return content.merge(key, value, remappingFunction);
        }

        @SuppressWarnings("unchecked")
        private IntEntry<V> $toSimpleEntry(IntEntry<?> entry) {
            int k = entry.getIntKey();
            V v = (V)entry.getValue();
            return new BasicEntry<>(k, v);
        }

        private final class KeyMapEntrySet extends ACollection<IntEntry<V>> implements Set<IntEntry<V>>, Serializable {

            private static final long serialVersionUID = 1L;

            private final Set<IntEntry<V>> entries = content.fastEntrySet();

            @Override
            public boolean contains(Object o) {
                return o instanceof Entry<?,?> && entries.contains($toSimpleEntry((IntEntry<?>)o));
            }

            @Override
            public boolean add(IntEntry<V> kvEntry) {
                return entries.add($toSimpleEntry(kvEntry));
            }

            @Override
            public Iterator<IntEntry<V>> iterator() {
                return entries.iterator();
            }

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

            @Override
            public int hashCode() {
                return entries.hashCode();
            }

            @Override
            public boolean equals(Object obj) {
                if(obj instanceof Set) {
                    @SuppressWarnings("unchecked")
                    Set<IntEntry<V>> that = (Set<IntEntry<V>>)obj;
                    return this.size() == that.size() && this.containsAll(that);
                }
                return false;
            }

        }

    }

}
