package com.capgemini.framework.common;

import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.*;

/**
 * This class is a {@link Map} which authorizes a weak object comparison, that is provided by an {@link IdMaker} object.<br>
 * This class allows you to bypass your key's implementation of equals(Object), and make it change according to your needs.
 * <p>
 * You can for instance keep your key object's compare method as a comparator of all the key's relevant fields (Item id + item rev + uid +...), but by providing this map with an IdMaker which uses only the item id, two objects with differing revisions but same item id will be considered the same
 * (the map will have unique Items as Keys, not unique ItemRevisions as keys).
 * 
 * @param <K> this Map's Keys type
 * @param <V> This Map's Values type
 */
@Slf4j
public class HashMapComparingStringIDs<K extends Comparable<K>, V> implements Map<K, V>, Serializable {

    private static final String VALUE_IS_NOT_OF_THE_CORRECT_TYPE = "Value is not of the correct type";
    private static final String KEY_IS_NOT_OF_THE_CORRECT_TYPE = "Key is not of the correct type";

    private static final long serialVersionUID = 3826358508367637107L;

    private final HashMap<String, K> stringToKey = new HashMap<>();
    private final HashMap<String, V> stringToValue = new HashMap<>();
    private final Class<? extends K> keyClass;
    private final Class<? extends V> valueClass;
    private final IdMaker<K> idMaker;

    /**
     * This interface provides a way to make a unique identifier for a Key, to be used in the {@link HashMapComparingStringIDs}.
     * 
     * @param <K> the Keys type
     */
    public interface IdMaker<K> extends Serializable {

        /**
         * Provides a unique {@link String} identifier for any key.
         * 
         * @param key the key for which this class should make an identifier.
         * @return the identifier String
         */
        String makeId(final K key);
    }

    /**
     * Creates an instance of this HashMap.
     * 
     * @param idMaker this object will be use to identify any incoming key.<br>
     *        If two different Keys produce the same Identifier through this IdMaker, the keys will be considered equal. This way, the key's own eqauls method is bypassed.
     * @param keyClass the common class instance of all the Key.
     * @param valueClass the common class instance of all the Values.
     */
    public HashMapComparingStringIDs(final IdMaker<K> idMaker, final Class<K> keyClass, final Class<V> valueClass) {
        this.keyClass = keyClass;
        this.valueClass = valueClass;
        this.idMaker = idMaker;
    }

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

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

    @Override
    public boolean containsKey(final Object key) {
        if (isEmpty()) {
            return false;
        }
        try {
            K aKey = keyClass.cast(key);
            String strKey = idMaker.makeId(aKey);
            return stringToKey.containsKey(strKey);
        } catch (ClassCastException e) {
            log.trace(KEY_IS_NOT_OF_THE_CORRECT_TYPE, e);
            return false; // wrong type
        }
    }

    @Override
    public boolean containsValue(final Object value) {
        if (isEmpty()) {
            return false;
        }
        try {
            V aValue = valueClass.cast(value);
            return stringToValue.containsValue(aValue);
        } catch (ClassCastException e) {
            log.trace(VALUE_IS_NOT_OF_THE_CORRECT_TYPE, e);
            return false; // wrong type
        }
    }

    @Override
    public V get(final Object key) {
        if (isEmpty()) {
            return null;
        }
        try {
            K aKey = keyClass.cast(key);
            String strKey = idMaker.makeId(aKey);
            return stringToValue.get(strKey);
        } catch (ClassCastException e) {
            log.trace(KEY_IS_NOT_OF_THE_CORRECT_TYPE, e);
            return null; // wrong type
        }
    }

    @Override
    public V put(final K key, final V value) {
        String identifier = idMaker.makeId(key);
        if (containsKey(key)) {
            return stringToValue.put(identifier, value);
        } else {
            stringToKey.put(identifier, key);
            stringToValue.put(identifier, value);
            return null;
        }
    }

    @Override
    public V remove(final Object key) {
        if (isEmpty()) {
            return null;
        }
        try {
            if (containsKey(key)) {
                K aKey = keyClass.cast(key);
                String identifier = idMaker.makeId(aKey);
                stringToKey.remove(identifier);
                return stringToValue.remove(identifier);
            }
        } catch (ClassCastException e) {
            log.trace(KEY_IS_NOT_OF_THE_CORRECT_TYPE, e);
        }
        return null; // Not found or ClassCastException
    }

    @Override
    public void putAll(final Map<? extends K, ? extends V> map) {
        for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
            String identifier = idMaker.makeId(entry.getKey());
            K key = entry.getKey();
            stringToKey.put(identifier, key);
            stringToValue.put(identifier, entry.getValue());
        }
    }

    @Override
    public void clear() {
        stringToKey.clear();
        stringToValue.clear();
    }

    @Override
    public Set<K> keySet() {
        return new TreeSet<>(stringToKey.values()); // This set is also sorted
    }

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

    @Override
    public Set<Entry<K, V>> entrySet() { // This set is also sorted
        Set<Entry<K, V>> entrySet = new TreeSet<>();
        for (Entry<String, K> keyEntry : stringToKey.entrySet()) {
            K key = keyEntry.getValue();
            String identifier = keyEntry.getKey();
            V value = stringToValue.get(identifier);
            entrySet.add(new MyEntry(key, value, identifier, this));
        }
        return entrySet;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("{");
        for (Entry<K, V> entry : entrySet()) {
            builder.append(entry.getKey()).append(':').append(entry.getValue()).append(", ");
        }
        return builder.append("}").toString();
    }

    private class MyEntry implements Entry<K, V>, Comparable<MyEntry> {
        private final K key;
        private final V value;
        private final String identifier;
        private final HashMapComparingStringIDs<K, V> theBackingMap;

        protected MyEntry(final K key, final V value, final String identifier, final HashMapComparingStringIDs<K, V> theBackingMap) {
            this.key = key;
            this.value = value;
            this.identifier = identifier;
            this.theBackingMap = theBackingMap;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(final V value) {
            return theBackingMap.stringToValue.put(identifier, value);
        }

        @Override
        public int compareTo(final MyEntry other) {
            return key.compareTo(other.getKey());
        }
    }
}
