/*
 * @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.AbstractList;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.text.StrBuilder;

/**
 * Usuwanie jest realizowane w czasie O(N), reszta idzie bardzo ładnie.
 * <p>
 * Wcześniej mieliśmy implementację która się opierała naiwnie na apache.commons.
 * Naiwnie - bo oni także mieli złe złożoności przy usuwaniu elementów, a nawet
 * dużo dużo częściej (tak samo używali ArrayList do przechowywania kolejności
 * kluczy).
 * </p>
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class RandomAccessMap<K,V> implements ListableMap<K, V> {
    
    private final List<K> keys;
    private final Map<K, V> map;

    public RandomAccessMap() {
        keys = new ArrayList<>();
        map = new HashMap<>();
    }

    @Override
    public List<V> asListAdapter() {
        return new ValueListAdapter();
    }

    @Override
    public List<K> keysList() {
        return new KeyListAdapter();
    }

    @Override
    public List<Entry<K, V>> entryList() {
        return new EntryListAdapter();
    }

    @Override
    public V at(int index) {
        return map.get(keys.get(index));
    }

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

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

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

    @Override
    @SuppressWarnings("element-type-mismatch")
    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }

    @Override
    public V get(Object key) {
        return map.get(key);
    }

    @Override
    public V put(K key, V value) {
        if( map.containsKey(key) ) {
            // just change value
            return map.put(key, value);
        } else {
            // add new value
            keys.add(key);
            map.put(key, value);
            return null;
        }
    }

    @Override
    @SuppressWarnings("element-type-mismatch")
    public V remove(Object key) {
        if( !map.containsKey(key) ) {
            return null;
        }
        // remove from keys: O(N)
        keys.remove(key);
        return map.remove(key);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> values) {
        for(Map.Entry<? extends K, ? extends V> entry : values.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public void clear() {
        keys.clear();
        map.clear();
    }

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

    @Override
    public Collection<V> values() {
        return asListAdapter();
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        return new EntryListAdapter();
    }

    @Override
    public Iterator<V> iterator() {
        return asListAdapter().iterator();
    }
	
	@Override
	public String toString() {
		StrBuilder builder = new StrBuilder();
		builder.append('{');
		builder.list(", ");
		for(K key : keys) {
			builder.item().append(key).append('=').append(map.get(key));
		}
		builder.append('}');
		return builder.toString();
	}
    
    private class ValueListAdapter extends AbstractList<V> implements RandomAccess {

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

        @Override
        public void clear() {
            RandomAccessMap.this.clear();
        }

        @Override
        public V get(int index) {
            return RandomAccessMap.this.at(index);
        }

        @Override
        public V set(int index, V element) {
            return map.put(keys.get(index), element);
        }

        @Override
        public V remove(int index) {
            // remove from keys: O(N)
            return map.remove(keys.remove(index));
        }
        
        @Override
        @SuppressWarnings("element-type-mismatch")
        public boolean contains(Object value) {
            return map.containsValue(value);
        }
        
    }
    
    private class KeyListAdapter extends AbstractList<K> implements Set<K>, RandomAccess {

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

        @Override
        @SuppressWarnings("element-type-mismatch")
        public boolean remove(Object key) {
            return null != RandomAccessMap.this.remove(key);
        }
        
        @Override
        public K remove(int index) {
            // remove from keys: O(N)
            K key = keys.remove(index);
            map.remove(key);
            return key;
        }

        @Override
        public void clear() {
            RandomAccessMap.this.clear();
        }

        @Override
        public K get(int index) {
            return keys.get(index);
        }

        @Override
        public K set(int index, K element) {
            if( map.containsKey(element) ) {
                throw new IllegalArgumentException("key conflict. Key already exists");
            }
			K prev = keys.get(index);
            map.put(element, map.remove(prev));
            keys.set(index, element);
            return prev;
        }

    }
    
    private class EntryListAdapter extends AbstractList<Entry<K,V>> implements Set<Entry<K,V>>, RandomAccess {

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

        @Override
        public boolean contains(Object object) {
            if(!(object instanceof Entry)) {
                return false;
            }
            Entry<?,?> element = (Entry<?,?>)object;
			Object key = element.getKey();
			Object val = element.getValue();
			V found = map.get(key);
			if(null == found) {
				return (null == val) && map.containsKey(key);
			} else {
				return found.equals(val);
			}
        }

        @Override
        public boolean add(Entry<K, V> entry) {
            RandomAccessMap.this.put(entry.getKey(), entry.getValue());
            return true;
        }

        @Override
        @SuppressWarnings("element-type-mismatch")
        public boolean remove(Object object) {
            if(!(object instanceof Entry)) {
                return false;
            }
            Entry<?,?> entry = (Entry<?,?>)object;
            Object key = entry.getKey();
            if ( !ValueUtils.equals(entry.getValue(), map.get(key)) ) {
                return false;
            }
            // remove from keys: O(N)
            keys.remove(key);
            map.remove(key);
            return true;
        }
        
        @Override
        public Entry<K, V> remove(int index) {
            K key = keys.get(index);
            V val = map.get(key);
            // remove from keys: O(N)
            keys.remove(key);
            map.remove(key);
            return new AbstractMap.SimpleImmutableEntry<>(key, val);
        }

        @Override
        public void clear() {
            RandomAccessMap.this.clear();
        }

        @Override
        public Entry<K, V> get(int index) {
            return new EntryAdapter(RandomAccessMap.this, index);
        }

        @Override
        public Entry<K, V> set(int index, Entry<K, V> element) {
            K pkey = keys.get(index);
            V pval = map.get(pkey);
			
			if( !pkey.equals(element.getKey()) && map.containsKey(element.getKey()) ) {
                throw new IllegalArgumentException("key conflict. Key already exists");
            }
            
			map.remove(keys.get(index));
            map.put(element.getKey(), element.getValue());
            keys.set(index, element.getKey());
            
            return new AbstractMap.SimpleImmutableEntry(pkey, pval);
        }

    }
    
    private static class EntryAdapter<K,V> implements Entry<K,V> {
        
        private final RandomAccessMap<K,V> parent;
        private final int index;

        public EntryAdapter(RandomAccessMap<K,V> parent, int index) {
			this.parent = parent;
            this.index = index;
        }

        @Override
        public K getKey() {
            return parent.keys.get(index);
        }

        @Override
        public V getValue() {
            return parent.map.get(parent.keys.get(index));
        }

        @Override
        public V setValue(V value) {
            return parent.map.put(parent.keys.get(index), value);
        }

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

		@Override
		public boolean equals(Object object) {
			if(object instanceof EntryAdapter<?,?>) {
				EntryAdapter<?,?> entry = (EntryAdapter<?,?>)object;
				return entry.parent==parent && entry.index == index;
			}
			if(object instanceof Entry) {
				final Entry<?,?> entry = (Entry<?,?>)object;
				return ValueUtils.equals(entry.getKey(), getKey()) && ValueUtils.equals(entry.getValue(), getValue());
			}
			return false;
		}
		
    }
}
