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

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.functional.ProjectionFunction;
import net.ranides.assira.generic.CompareUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class MapUtils {
    
    private MapUtils() {
        /* utility class */
    }
    
    public static <K, TV, RV> Map<K,RV> map(Map<K,TV> map, Function<? super TV, ? extends RV> function) {
        return new MapAdapter<>(map, function, null);
    }
    
    public static <K, TV, RV> Map<K,RV> map(Map<K,TV> map, Function<? super TV, ? extends RV> function, BiFunction<? super K, ? super RV, ? extends RV> inserter) {
        return new MapAdapter<>(map, function, inserter);
    }
    
    public static <K, TV, RV> Map<K,RV> map(Map<K,TV> map, ProjectionFunction<TV, RV> function) {
        return new MapProjection<>(map, function);
    }
    
    public static <K, TV, RV> MultiMap<K,RV> map(MultiMap<K,TV> map, Function<? super TV, ? extends RV> function) {
        return new MMapAdapter<>(map, function, null);
    }
    
    public static <K, TV, RV> MultiMap<K,RV> map(MultiMap<K,TV> map, Function<? super TV, ? extends RV> function, BiFunction<? super K, ? super RV, ? extends RV> inserter) {
        return new MMapAdapter<>(map, function, inserter);
    }
    
    public static <K, TV, RV> MultiMap<K,RV> map(MultiMap<K,TV> map, ProjectionFunction<TV, RV> function) {
        return new MMapProjection<>(map, function);
    }

    public static <K, TV, RV> Map<K,RV> wrap(Map<K,TV> map, Function<? super TV, ? extends RV> getter, BiFunction<? super TV, ? super RV, ? extends RV> setter) {
        return new MapWrapper<>(map, getter, setter);
    }
    
    public static <K, TV, RV> MultiMap<K,RV> wrap(MultiMap<K,TV> map, Function<? super TV, ? extends RV> getter, BiFunction<? super TV, ? super RV, ? extends RV> setter) {
        return new MMapWrapper<>(map, getter, setter);
    }
    
    private static class MMapAdapter<K, TV, RV> extends MapAdapter<K, TV, RV> implements MultiMap<K, RV> {
        
        private static final long serialVersionUID = 1L;
        
        private final MultiMap<K, TV> mcontent;

        public MMapAdapter(MultiMap<K, TV> mcontent, Function<? super TV, ? extends RV> function, BiFunction<? super K, ? super RV, ? extends RV> setter) {
            super(mcontent, function, setter);
            this.mcontent = mcontent;
        }
        
        @Override
        public Collection<RV> getAll(Object key) {
            return CollectionUtils.map(mcontent.getAll(key), function);
        }
        
    }
    
    private static class MapAdapter<K, TV, RV> extends AMap<K, RV> implements Map<K, RV> {
        
        private static final long serialVersionUID = 1L;
        
        @SuppressWarnings("unchecked")
        private static final BiFunction NSE = (Serializable & BiFunction)(a,b) -> { 
            throw new UnsupportedOperationException(); 
        };
        
        protected final Map<K, TV> content;
        protected final Function<? super TV, ? extends RV> function;
        protected final BiFunction<? super K, ? super RV, ? extends RV> inserter;
        
        public MapAdapter(Map<K, TV> content, Function<? super TV, ? extends RV> function, BiFunction<? super K, ? super RV, ? extends RV> setter) {
            this.content = content;
            this.function = function;
            this.inserter = null != setter ? setter : NSE;
        }

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

        @Override
        public boolean containsKey(Object 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(Object key) {
            TV prev = content.remove(key);
            return prev==null ? null : function.apply(prev);
        }

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

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

        @Override
        public RV get(Object key) {
            return function.apply(content.get(key));
        }

        @Override
        public RV put(K key, RV value) {
            return inserter.apply(key, value);
        }
        
        private final class EntryAdapter implements Entry<K, RV> {
            
            private final Entry<K,TV> delegate;

            public EntryAdapter(Entry<K, TV> delegate) {
                this.delegate = delegate;
            }

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

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

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

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (!(obj instanceof Map.Entry)) {
                    return false;
                }
                if (getClass() == obj.getClass()) {
                    @SuppressWarnings("unchecked")
                    EntryAdapter other = (EntryAdapter) obj;
                    return delegate.equals(other.delegate);
                } else {
                    @SuppressWarnings("unchecked")
                    Map.Entry<K,RV> other = (Map.Entry<K,RV>)obj;
                    return CompareUtils.equals(getKey(), other.getKey()) && CompareUtils.equals(getValue(), other.getValue());
                }
            }
            
        }

    }
    
    private static class MMapProjection<K, TV, RV> extends MapProjection<K,TV,RV> implements MultiMap<K, RV> {
        
        private static final long serialVersionUID = 1L;
        
        private final MultiMap<K, TV> mcontent;
        
        public MMapProjection(MultiMap<K, TV> content, ProjectionFunction<TV, RV> function) {
            super(content, function);
            this.mcontent = content;
        }
        
        @Override
        public Collection<RV> getAll(Object key) {
            return CollectionUtils.map(mcontent.getAll(key), function);
        }

    }
    
    private static class MapProjection<K, TV, RV> extends AMap<K, RV> implements Map<K, RV> {
        
        private static final long serialVersionUID = 1L;

        protected final Map<K, TV> content;

        protected final ProjectionFunction<TV, RV> function;

        public MapProjection(Map<K, TV> content, ProjectionFunction<TV, RV> function) {
            this.content = content;
            this.function = function;
        }
        
        public MapProjection(MultiMap<K, TV> content, ProjectionFunction<TV, RV> function) {
            this.content = content;
            this.function = function;
        }

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

        @Override
        public boolean containsKey(Object key) {
            return content.containsKey(key);
        }
        
        @SuppressWarnings("unchecked")
        @Override
        public boolean containsValue(Object value) {
            try {
                return content.containsValue(function.invert((RV)value));
            } catch(ClassCastException _ex) {
                return false;
            }
        }

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

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

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

        @Override
        public Collection<RV> values() {
            return CollectionUtils.map(content.values(), function);
        }
        
        @Override
        public Set<Map.Entry<K, RV>> entrySet() {
            return SetUtils.map(content.entrySet(), e -> new EntryProjection(e));
        }

        @Override
        public RV get(Object key) {
            return function.apply(content.get(key));
        }
        
        @Override
        public RV put(K key, RV value) {
            return function.apply(content.put(key, function.invert(value)));
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean remove(Object key, Object value) {
            Object v;
            try {
                v = function.invert((RV)value);
            } catch(ClassCastException _e) {
                return false;
            }
            return content.remove(key, v);
        }

        private final class EntryProjection implements Map.Entry<K, RV> {
            
            private final Map.Entry<K,TV> delegate;

            public EntryProjection(Map.Entry<K, TV> delegate) {
                this.delegate = delegate;
            }

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

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

            @Override
            public RV setValue(RV value) {
                return function.apply(content.put(getKey(), function.invert(value)));
            }

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

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (!(obj instanceof Map.Entry)) {
                    return false;
                }
                @SuppressWarnings("unchecked")
                Map.Entry<K,RV> other = (Map.Entry<K,RV>)obj;
                return CompareUtils.equals(getKey(), other.getKey()) 
                    && CompareUtils.equals(getValue(), other.getValue());
            }
            
        }

    }
    
    private static class MMapWrapper<K, TV, RV> extends MapWrapper<K,TV,RV> implements MultiMap<K, RV> { 
        
        private static final long serialVersionUID = 1L;

        private final MultiMap<K, TV> mcontent;
                
        public MMapWrapper(MultiMap<K, 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(Object key) {
            return CollectionUtils.map(mcontent.getAll(key), getter);
        }
        
    }
    
    private static class MapWrapper<K, TV, RV> extends AMap<K, RV> implements Map<K, RV> { 

        private static final long serialVersionUID = 1L;

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

        public MapWrapper(Map<K,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(Object key) {
            return content.containsKey(key);
        }

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

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

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

        @Override
        public Collection<RV> values() {
            return CollectionUtils.map(content.values(), getter);
        }
        
        @Override
        public Set<Entry<K, RV>> entrySet() {
            return SetUtils.map(content.entrySet(), e -> new EntryWrapper(e));
        }
        
        private final class EntryWrapper implements Map.Entry<K, RV> {
            
            private final Map.Entry<K,TV> delegate;

            public EntryWrapper(Map.Entry<K, TV> delegate) {
                this.delegate = delegate;
            }

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

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

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

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

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (!(obj instanceof Map.Entry)) {
                    return false;
                }
                @SuppressWarnings("unchecked")
                Map.Entry<K,RV> other = (Map.Entry<K,RV>)obj;
                return CompareUtils.equals(getKey(), other.getKey()) 
                    && CompareUtils.equals(getValue(), other.getValue());
            }
            
        }
        
    }
    
    static class EmptyMM<K,V> extends AMap<K,V> implements MultiMap<K, V> {
        
        private static final long serialVersionUID = 1L;

        @Override
        public Set<Map.Entry<K, V>> entrySet() {
            return Collections.emptySet();
        }

        @Override
        public int size() {
            return 0;
        }

        @Override
        public boolean containsKey(Object key) {
            return false;
        }

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

        @Override
        public V get(Object key) {
            return null;
        }
        
        @Override
        public Collection<V> getAll(Object key) {
            return Collections.emptyList();
        }

    }
    
}
