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

import java.util.Map;
import java.util.Set;
import java.util.function.IntUnaryOperator;
import net.ranides.assira.collection.IntCollection;
import net.ranides.assira.collection.IntCollectionUtils;
import net.ranides.assira.collection.iterators.IntIterator;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.functional.IntProjectionFunction;
import net.ranides.assira.generic.CompareUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class LookupUtils {
    
    private LookupUtils() {
        /* utility class */
    }
    
    public static <K> Lookup<K> map(Lookup<K> map, IntUnaryOperator function) {
        return new MapAdapter<>(map, function);
    }
    
    public static <K> Lookup<K> map(Lookup<K> map, IntProjectionFunction function) {
        return new MapProjection<>(map, function);
    }
    
    @SuppressWarnings("element-type-mismatch")
    private static class MapAdapter<K> extends ALookup<K> {

        private final Lookup<K> content;
        private final IntUnaryOperator function;

        public MapAdapter(Lookup<K> content, IntUnaryOperator function) {
            this.content = content;
            this.function = function;
            this.defRetValue = function.applyAsInt(content.defaultReturnValue());
        }

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

        @Override
        public boolean containsKey(Object key) {
            return content.containsKey(key);
        }
        
        @Override
        public boolean containsValue(int value) {
            IntIterator i = content.values().iterator();
            while(i.hasNext()) {
                if(CompareUtils.equals(value, function.applyAsInt(i.nextInt()))) {
                    return true;
                }
            }
            return false;
        }

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

        @Override
        public int removeInt(Object key) {
            if( !content.containsKey(key) ) {
                return defaultReturnValue();
            }
            return function.applyAsInt(content.remove(key));
        }

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

        @Override
        public IntCollection values() {
            return IntCollectionUtils.map(content.values(), function);
        }
        
        @Override
        public Set<LookupEntry<K>> fastEntrySet() {
            return SetUtils.map(content.fastEntrySet(), e -> new EntryAdapter(e));
        }

        @Override
        public int getInt(Object key) {
            if(!content.containsKey(key)) {
                return defaultReturnValue();
            }
            return function.applyAsInt(content.getInt(key));
        }
        
        private final class EntryAdapter implements LookupEntry<K> {
            
            private final LookupEntry<K> delegate;

            public EntryAdapter(LookupEntry<K> delegate) {
                this.delegate = delegate;
            }

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

            @Override
            public int getIntValue() {
                return function.applyAsInt(delegate.getValue());
            }

            @Override
            public int setValue(int value) {
                throw new UnsupportedOperationException();
            }
            
            @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,Integer> other = (Map.Entry<K,Integer>)obj;
                    return CompareUtils.equals(getKey(), other.getKey()) && CompareUtils.equals(getValue(), other.getValue());
                }
            }
            
        }

    }
    
    private static class MapProjection<K> extends ALookup<K> {

        private final Lookup<K> content;
        private final IntProjectionFunction function;

        public MapProjection(Lookup<K> content, IntProjectionFunction function) {
            this.content = content;
            this.function = function;
        }

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

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

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

        @Override
        public int removeInt(Object key) {
            return function.apply(content.removeInt(key));
        }

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

        @Override
        public IntCollection values() {
            return IntCollectionUtils.map(content.values(), function::apply);
        }
        
        @Override
        public Set<LookupEntry<K>> fastEntrySet() {
            return SetUtils.map(content.fastEntrySet(), e -> new EntryProjection(e));
        }

        @Override
        public int getInt(Object key) {
            return function.applyAsInt(content.getInt(key));
        }

        @Override
        public int put(K key, int value) {
            return function.applyAsInt(content.put(key, function.invert(value)));
        }

        @Override
        public boolean remove(Object key, Object value) {
            return (value instanceof Integer) && content.remove(key, function.invert((Integer)value));
        }

        @Override
        public int defaultReturnValue() {
            return function.applyAsInt(content.defaultReturnValue());
        }

        @Override
        public void defaultReturnValue(int rv) {
            content.defaultReturnValue(function.invert(rv));
        }
        
        private final class EntryProjection implements LookupEntry<K> {
            
            private final LookupEntry<K> delegate;

            public EntryProjection(LookupEntry<K> delegate) {
                this.delegate = delegate;
            }

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

            @Override
            public int getIntValue() {
                return function.apply(delegate.getIntValue());
            }

            @Override
            public int setValue(int 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;
                }
                if (getClass() == obj.getClass()) {
                    @SuppressWarnings("unchecked")
                    EntryProjection other = (EntryProjection) obj;
                    return delegate.equals(other.delegate);
                } else {
                    @SuppressWarnings("unchecked")
                    Map.Entry<K,Integer> other = (Map.Entry<K,Integer>)obj;
                    return CompareUtils.equals(getKey(), other.getKey()) && CompareUtils.equals(getValue(), other.getValue());
                }
            }
            
        }

    }
    
    
}
