package net.ranides.assira.grammar.properties;

import net.ranides.assira.generic.Ref;
import net.ranides.assira.grammar.G4Node;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.WeakHashMap;

/**
 * Utility class. Allows assigning properties with specified nodes without introducing changes into ANTLR codebase.
 * Properties are inherited by children nodes.
 *
 * Information stored inside node can be e.g. language, country or Parser instance.
 * These properties can be accessed by visitors without tight coupling with Huawei framework.
 *
 */
public class G4NodeProperties {

    /**
     * This map is used to store all assigned properties.
     * We use ThreadLocal, because we do not want interaction between threads.
     * We use Node as key in the map.
     * Every key is assigned with collection of all properties assigned to specified node.
     */
    private static final ThreadLocal<Map<Object, G4NodeProperties>> MAP = ThreadLocal.withInitial(WeakHashMap::new);

    private final Map<Object, Object> data;

    public G4NodeProperties() {
        this.data = new HashMap<>();
    }

    public static Map<Object, Object> getAll(G4Node node) {
        Object unwrapped = node.unwrap();
        return Collections.unmodifiableMap(getReadableScope(unwrapped));
    }

    public static boolean has(G4Node node, String name) {
        return get(node, name).isPresent();
    }

    public static boolean has(G4Node node, G4NodeProperty<?> key) {
        return get(node, key).isPresent();
    }

    public static boolean has(G4Node node, G4NodeProperty<?> key, Object value) {
        return get(node, key).filter(v -> v.equals(value)).isPresent();
    }

    /**
     * Get property associated with specified node. If node does not have property, method will search
     * for it in parent nodes. In this way, every property behaves like it was inherited from parents.
     *
     * @param node tree fragment
     * @param name property
     * @return value associated with property
     */
    public static Ref<Object> get(G4Node node, String name) {
        return getValue(node, name);
    }

    public static <V> Ref<V> get(G4Node node, G4NodeProperty<V> key) {
        return getValue(node, key).map(key.type()::cast);
    }

    public Ref<Object> get(String name) {
        return Ref.ofNullable(data.get(name));
    }

    public <V> Ref<V> get(G4NodeProperty<V> key) {
        return Ref.ofNullable(key.type().cast(data.get(key)));
    }

    public static void put(G4Node tree, String name, Object value) {
        Object unwrapped = tree.unwrap();
        getWritableScope(unwrapped).put(name, value);
    }

    public static <V> void put(G4Node tree, G4NodeProperty<V> key, V value) {
        Object unwrapped = tree.unwrap();
        getWritableScope(unwrapped).put(key, value);
    }

    public static void put(G4Node tree, G4NodeProperties values) {
        Object unwrapped = tree.unwrap();
        getWritableScope(unwrapped).data.putAll(values.data);
    }

    public void put(String name, Object value) {
        data.put(name, value);
    }

    public <V> void put(G4NodeProperty<V> key, V value) {
        data.put(key, value);
    }

    private static <K> Ref<Object> getValue(G4Node node, K key) {
        Object output = null;
        G4Node current = node;
        while (current != null && output == null) {
            Object unwrapped = current.unwrap();
            output = getReadableScope(unwrapped).get(key);
            current = current.parent().orElse(null);
        }
        return Ref.ofNullable(output, () -> new NoSuchElementException("Node " + node + " has no property " + key));
    }

    private static <T> G4NodeProperties getWritableScope(T reference) {
        return MAP.get().computeIfAbsent(reference, ref -> new G4NodeProperties());
    }

    private static <T> Map<Object, Object> getReadableScope(T reference) {
        G4NodeProperties map = MAP.get().get(reference);
        return map != null ? map.data : Collections.emptyMap();
    }
}
