package net.ranides.assira.system.impl;

import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.maps.TypeMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.IAttribute;
import net.ranides.assira.reflection.IClass;
import net.ranides.assira.reflection.IConstructor;
import net.ranides.assira.system.HostContext;
import net.ranides.assira.trace.LoggerUtils;
import org.slf4j.Logger;

import java.util.Comparator;
import java.util.Map;
import java.util.NoSuchElementException;

public class HostSimpleContext implements HostContext {

    private static final Logger LOGGER = LoggerUtils.getLogger();

    private final TypeMap<IClass<?>> type2prototype = new TypeMap<>();

    private final Map<String, IClass<?>> name2prototype = new OpenMap<>();

    private final TypeMap<Object> type2instance = new TypeMap<>();

    private final Map<String, Object> name2instance = new OpenMap<>();

    @Override
    public Object getBean(String name) {
        if(name2instance.containsKey(name)) {
            return name2instance.get(name);
        }
        if(name2prototype.containsKey(name)) {
            return construct(name2prototype.get(name));
        }
        throw new NoSuchElementException("No bean with name: " + name);
    }

    @Override
    public <T> T getBean(IClass<T> type) {
        if(type2instance.matches(type)) {
            return type.cast(type2instance.match(type));
        }
        if(type2prototype.matches(type)) {
            Object o = construct(type2prototype.match(type));
            return type.cast(o);
        }
        throw new NoSuchElementException("No bean with type: " + type);
    }

    @Override
    public <T> T getBean(Class<T> type) {
        return getBean(IClass.typeinfo(type));
    }

    @Override
    public boolean isPrototype(String name) {
        return name2prototype.containsKey(name);
    }

    @Override
    public boolean isSingleton(String name) {
        return name2instance.containsKey(name);
    }

    @Override
    public void registerPrototype(String name, IClass<?> type) {
        type2prototype.add(type, type);
        if(name != null) {
            if(name2prototype.put(name, type) != null) {
                LOGGER.warn("Bean conflict for name: " + name);
            }
        }
    }

    @Override
    public void registerPrototype(String name, Class<?> type) {
        registerPrototype(name, IClass.typeinfo(type));
    }

    @Override
    public void registerSingleton(String name, Object instance) {
        type2instance.add(IClass.typefor(instance), instance);
        if(name != null) {
            if(name2instance.put(name, instance) != null) {
                LOGGER.warn("Bean conflict for name: " + name);
            }
        }
    }

    private Object construct(IClass<?> type) {
        return type.constructors()
            .require(IAttribute.PUBLIC)
            .stream()
            .sort(Comparator.comparingInt(v -> v.arguments().count()))
            .reverse()
            .map(c -> construct(c))
            .filter(o -> o.isPresent())
            .first()
            .orElseThrow(() -> new NoSuchElementException("There is no constructor for bean prototype " + type))
            .orElseThrow(() -> new NoSuchElementException("There is no constructor for bean prototype " + type));
    }

    private Ref<Object> construct(IConstructor<?> ctor) {
        final Object[] args;
        try {
            args = ctor.arguments().stream().map(a -> {
                if(a.type().isPrimitive() || a.type().isArray()) {
                    // skip if argument is not bean
                    throw new NoSuchElementException("primitive");
                }
                if(a.type().isSuper(ctor.parent())) {
                    // skip if argument is super class of requested bean
                    // (it's heuristic: we ignore "copying constructors")
                    throw new NoSuchElementException("copy");
                }
                return getBean(a.type());
            }).array();
        } catch (Exception e) {
            return Ref.empty();
        }
        try {
            return Ref.of(ctor.apply(args));
        } catch (Exception e) {
            return Ref.empty();
        }
    }
}
