package net.ranides.assira.collection.prototype;

import net.ranides.assira.collection.iterators.RemoveIterator;
import net.ranides.assira.collection.maps.AMap;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.*;

import java.util.*;
import java.util.function.Consumer;

/**
 * PrototypeMap is a class which allows you to store properties on any type inside map.
 *
 * It has support for protype inheritance: every map can have prototype.
 * Properties can be stored and retrieved directly from this map or from prototype chain.
 *
 * It could store keys of any type, but it has special generic methods for working with GenericKeys.
 *
 * Please note, that GenericKey(Integer, "path") if different than String key "path"
 */
public class PrototypeMap extends AMap<Object, Object> implements Map<Object, Object> {

    /**
     * Empty map
     */
    public static final PrototypeMap EMPTY = new PrototypeMap(true);

    protected PrototypeMap parent;

    protected Map<Object, Object> declared;

    private transient KeysView keys;
    private transient EntryView entries;
    private transient ValuesView values;

    /**
     * Creates new map without prototype
     */
    public PrototypeMap() {
        this(null, new OpenMap<>());
    }

    /**
     * Creates new map with prototype
     *
     * @param parent parent
     */
    public PrototypeMap(PrototypeMap parent) {
        this(parent, new OpenMap<>());
    }

    /**
     * Creates new map with declared entries copied from provided map
     *
     * @param content content
     */
    public PrototypeMap(Map<Object, ?> content) {
        this(null, new OpenMap<>(content));
    }

    /**
     * Creates new map with prototype and declared entries copied from provided map
     *
     * @param parent parent
     * @param content content
     */
    public PrototypeMap(PrototypeMap parent, Map<Object, ?> content) {
        this.parent = parent;
        this.declared = new OpenMap<>(content);
    }

    protected PrototypeMap(boolean empty) {
        assert empty : "this is constructor for empty map";
        this.parent = null;
        this.declared = Collections.emptyMap();
    }

    /**
     * Returns prototype, or null
     *
     * @return map
     */
    public PrototypeMap getParentMap() {
        return parent;
    }

    /**
     * Retuns mutable view which contains properties stored directly inside this map (ignores prototype chain).
     *
     * @return map
     */
    public Map<Object, Object> getDeclaredMap() {
        return declared;
    }

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

    @Override
    public boolean isEmpty() {
        return declared.isEmpty() && (parent==null || parent.isEmpty());
    }

    @Override
    public boolean containsKey(Object key) {
        return declared.containsKey(key) || (parent!=null && parent.containsKey(key));
    }

    @Override
    public boolean containsValue(Object value) {
        return declared.containsValue(value) || (parent!=null && parent.containsKey(value));
    }

    @Override
    public Object get(Object key) {
        for(PrototypeMap scope = this; scope != null; scope = scope.getParentMap()) {
            Object out = scope.getDeclaredMap().get(key);
            if(out != null) {
                return out;
            }
        }
        return null;
    }

    @Override
    public Object put(Object key, Object value) {
        return declared.put(key, value);
    }

    @Override
    public Object remove(Object key) {
        Object[] out = {null};
        forEachScope(scope -> {
            Object rem = scope.remove(key);
            if (rem != null && out[0] == null) {
                out[0] = rem;
            }
        });
        return out[0];
    }

    @Override
    public void putAll(Map<?, ?> m) {
        declared.putAll(m);
    }

    @Override
    public void clear() {
        forEachScope(Map::clear);
    }

    @Override
    public Set<Object> keySet() {
        if (keys == null) {
            keys = new KeysView();
        }
        return keys;
    }

    @Override
    public Collection<Object> values() {
        if (values == null) {
            values = new ValuesView();
        }
        return values;
    }

    @Override
    public Set<Map.Entry<Object, Object>> entrySet() {
        if (entries == null) {
            entries = new EntryView();
        }
        return entries;
    }

    /**
     * Returns mutable entry stored directly inside this map.
     * Returns none, if this map does not contain key (although prototype could).
     * It does not follow prototype chain.
     *
     * Returned Entry allows changing value assigned with provided key.
     *
     * @param key key
     * @return optional
     */
    public final Ref<PrototypeEntry<?>> getProperty(Object key) {
        if(containsKey(key)) {
            if(key instanceof GenericKey<?,?>) {
                return Ref.of(new FieldEntry<>((GenericKey<?,?>)key));
            }
            if(key instanceof String) {
                return Ref.of(new NameEntry((String)key));
            }
        }
        return Ref.empty();
    }

    /**
     * Returns mutable entry stored directly inside this map.
     * Returns none, if this map does not contain key (although prototype could).
     * It does not follow prototype chain.
     *
     * Returned Entry allows changing value assigned with provided key.
     *
     * @param key key
     * @return optional
     */
    public final Ref<PrototypeEntry<Object>> getProperty(String key) {
        if(containsKey(key)) {
            return Ref.of(new NameEntry(key));
        }
        return Ref.empty();
    }

    /**
     * Returns mutable entry stored directly inside this map.
     * Returns none, if this map does not contain key (although prototype could).
     * It does not follow prototype chain.
     *
     * Returned Entry allows changing value assigned with provided key.
     *
     * @param key key
     * @param <V> V
     * @return optional
     */
    public final <V> Ref<PrototypeEntry<V>> getProperty(GenericKey<?,V> key) {
        if(containsKey(key)) {
            return Ref.of(new FieldEntry<>(key));
        }
        return Ref.empty();
    }

    /**
     * Returns immutable view of entries stored directly in this map.
     * It does not follow prototype chain.
     *
     * Entries inside returned set allow changing value assigned with provided key.
     *
     * @return view
     */
    public final Set<PrototypeEntry<?>> propertySet() {
        return SetUtils.map(declared.keySet(), this::newEntry);
    }

    /**
     * Returns {@code true} if this map contains a mapping for the specified key.
     *
     * @param key key
     * @return boolean
     */
    public final boolean containsKey(String key) {
        return containsKey((Object)key);
    }

    /**
     * Returns {@code true} if this map contains a mapping for the specified key.
     *
     * @param key key
     * @return boolean
     */
    public final boolean containsKey(GenericKey<?,?> key) {
        return containsKey((Object)key);
    }

    /**
     * Inserts new mapping directly into this mapping.
     * If there was inherited mapping for provided key, it won't be changed inside prototype,
     * but reading from this map will return new, overriden value.
     *
     * Returns value previously stored direclty in this map (not inherited if there's one).
     *
     * @param key key
     * @param value value
     * @return value
     */
    public final Object put(String key, Object value) {
        return put((Object)key, value);
    }

    /**
     * Inserts new mapping directly into this mapping.
     * If there was inherited mapping for provided key, it won't be changed inside prototype,
     * but reading from this map will return new, overriden value.
     *
     * Returns value previously stored direclty in this map (not inherited if there's one).
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return value
     */
    public final <V> V put(GenericKey<?,V> key, V value) {
        if(key == null) {
            return null;
        }
        return key.type().cast(put((Object)key, value));
    }

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * @param key key
     * @return value
     */
    public final Object get(String key) {
        return get((Object)key);
    }

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * @param key key
     * @param <V> V
     * @return value
     */
    public final <V> V get(GenericKey<?,V> key) {
        if(key == null) {
            return null;
        }
        return key.type().cast(get((Object)key));
    }

    public final Ref<Object> getOptional(Object key) {
        return Ref.ofNullable(get(key));
    }

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code none} if this map contains no mapping for the key.
     *
     * @param key key
     * @return value
     */
    public final Ref<Object> getOptional(String key) {
        return Ref.ofNullable(get(key));
    }

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code none} if this map contains no mapping for the key.
     *
     * @param key key
     * @param <V> V
     * @return value
     */
    public final <V> Ref<V> getOptional(GenericKey<?,V> key) {
        return Ref.ofNullable(get(key));
    }

    /**
     * Removes specified key from map.
     * Returns previously stored value or null.
     *
     * @param key key
     * @return value
     */
    public final Object remove(String key) {
        return remove((Object)key);
    }

    /**
     * Removes specified key from map.
     * Returns previously stored value or null.
     *
     * @param key key
     * @param <V> V
     * @return value
     */
    public final <V> V remove(GenericKey<?,V> key) {
        if(key == null) {
            return null;
        }
        return key.type().cast(remove((Object)key));
    }

    protected void forEachScope(Consumer<Map<Object, Object>> consumer) {
        for(PrototypeMap scope = this; scope != null; scope = scope.getParentMap()) {
            consumer.accept(scope.getDeclaredMap());
        }
    }

    protected PrototypeEntry<?> newEntry(Object key) {
        if(key instanceof String) {
            return new NameEntry((String) key);
        } else {
            return new FieldEntry<>((GenericKey<?,?>)key);
        }
    }

    private class EntryView extends AbstractSet<Map.Entry<Object, Object>> {
        @Override
        public boolean contains(Object other) {
            if(other instanceof Map.Entry<?,?>) {
                Map.Entry<?,?> entry = (Map.Entry<?,?>)other;
                return Objects.equals(declared.get(entry.getKey()), entry.getValue());
            }
            return false;
        }

        @Override
        public boolean add(Map.Entry<Object, Object> entry) {
            Object key = entry.getKey();
            Object val = entry.getValue();

            if(key instanceof GenericKey<?,?>) {
                GenericKey<?,?> field = (GenericKey<?,?>) key;
                if(!field.type().isInstance(val)) {
                    throw new ClassCastException(val + " can't be cast to " + field.type());
                }
            }
            declared.put(key, val);
            return true;
        }

        @Override
        public boolean remove(Object other) {
            if(other instanceof Map.Entry<?,?>) {
                Map.Entry<?,?> entry = (Map.Entry<?,?>)other;
                return declared.remove(entry.getKey(), entry.getValue());
            }
            return false;
        }

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

        @Override
        public Iterator<Map.Entry<Object, Object>> iterator() {
            return new RemoveIterator<>(aggregate().iterator(), this::remove);
        }

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

        public Set<Map.Entry<Object, Object>> aggregate() {
            Set<Map.Entry<Object, Object>> set = new HashSet<>();
            forEachScope(scope -> set.addAll(scope.entrySet()));
            return set;
        }
    }

    private class ValuesView extends AbstractSet<Object> {
        @Override
        public boolean contains(Object other) {
            return PrototypeMap.this.containsValue(other);
        }

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

        @Override
        public Iterator<Object> iterator() {
            return aggregate().iterator();
        }

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

        public Collection<Object> aggregate() {
            if(parent == null) {
                return declared.values();
            }
            Set<Object> inserted = new HashSet<>();
            List<Object> list = new ArrayList<>();
            forEachScope(scope -> {
                scope.forEach((key, value) -> {
                    if(inserted.add(key)) {
                        list.add(value);
                    }
                });
            });

            return list;
        }
    }

    private class KeysView extends AbstractSet<Object> {
        @Override
        public boolean contains(Object other) {
            return PrototypeMap.this.containsKey(other);
        }

        @Override
        public boolean remove(Object key) {
            return PrototypeMap.this.remove(key) != null;
        }

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

        @Override
        public Iterator<Object> iterator() {
            return new RemoveIterator<>(aggregate().iterator(), this::remove);
        }

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

        public Set<Object> aggregate() {
            Set<Object> set = new HashSet<>();
            forEachScope(scope -> set.addAll(scope.keySet()));
            return set;
        }
    }

    private final class NameEntry extends AEntry<Object, Object> implements PrototypeEntry<Object> {
        private final String name;

        public NameEntry(String name) {
            this.name = name;
        }

        @Override
        public IClass<Object> getType() {
            return IClass.OBJECT;
        }

        @Override
        public Object getKey() {
            return name;
        }

        @Override
        public Object getValue() {
            return declared.get(name);
        }

        @Override
        public Object setValue(Object value) {
            return declared.put(name, value);
        }

        @Override
        public PrototypeMap getMap() {
            return PrototypeMap.this;
        }
    }

    private final class FieldEntry<V> extends AEntry<Object, V> implements PrototypeEntry<V> {
        private final GenericKey<?,V> field;

        public FieldEntry(GenericKey<?,V> field) {
            this.field = field;
        }

        @Override
        public IClass<V> getType() {
            return field.type();
        }

        @Override
        public Object getKey() {
            return field;
        }

        @Override
        public V getValue() {
            return field.type().cast(declared.get(field));
        }

        @Override
        public V setValue(V value) {
            return field.type().cast(declared.put(field, value));
        }

        @Override
        public PrototypeMap getMap() {
            return PrototypeMap.this;
        }
    }
}
