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

import lombok.experimental.UtilityClass;
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.covariant.IntProjectionFunction;
import net.ranides.assira.generic.CompareUtils;

import java.util.Set;
import java.util.function.IntUnaryOperator;

/**
 * Static methods working with Lookup
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class LookupUtils {

    /**
     * Returns lookup view which maps values (not keys) using provided function.
     * Changes introduced to source collection are visible in view.
     *
     * Returned view supports removing, but not set or add operations.
     *
     * Please note, that it allows only mapping preserving type "int".
     *
     * @param values values
     * @param function function
     * @param <K> K
     * @return view
     */
    public static <K> Lookup<K> map(Lookup<K> values, IntUnaryOperator function) {
        return new MapAdapter<>(values, function);
    }

    /**
     * Returns lookup view which maps values (not keys) using provided function.
     * Changes introduced to source collection are visible in view.
     *
     * Returned view supports all operations: remove, set or add.
     *
     * Please note, that it allows only mapping preserving type "int".
     *
     * @param values values
     * @param function function
     * @param <K> K
     * @return view
     */
    public static <K> Lookup<K> map(Lookup<K> values, IntProjectionFunction function) {
        return new MapProjection<>(values, 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 extends ALookupEntry<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();
            }

        }

    }
    
    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 extends ALookupEntry<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 Integer getValue() {
                return getIntValue();
            }

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

        }

    }
    
    
}
