package com.r35157.nenjim.service.registry.impl.ref;

import com.r35157.nenjim.component.NenjimApplication;
import com.r35157.nenjim.component.NenjimComponent;
import com.r35157.nenjim.service.registry.NenjimRegistryService;
import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/** Reference Registry storage with read-only public and internal admin views. */
final class NenjimRegistryServiceImpl implements
        NenjimRegistryService,
        NenjimRegistryServiceAdmin,
        NenjimApplication {
    @Override
    public synchronized void start() {
        if (startAttempted) {
            throw new IllegalStateException(
                    "Nenjim Registry service start has already been attempted");
        }

        startAttempted = true;
        started = true;
    }

    @Override
    public synchronized void registerComponent(
            @NotNull NenjimComponentId componentId,
            @NotNull NenjimComponent component) {
        Objects.requireNonNull(componentId, "componentId");
        Objects.requireNonNull(component, "component");

        NenjimComponent existingComponent = componentsById.get(componentId);
        if (existingComponent != null) {
            throw new IllegalArgumentException(
                    "Duplicate Nenjim component ID '" + componentId.value()
                            + "': existing implementation "
                            + existingComponent.getClass().getName()
                            + ", attempted implementation "
                            + component.getClass().getName());
        }

        Set<Class<? extends NenjimComponent>> componentInterfaces =
                findComponentInterfaces(component.getClass());

        componentsById.put(componentId, component);
        for (Class<? extends NenjimComponent> componentInterface
                : componentInterfaces) {
            componentIdsByInterface
                    .computeIfAbsent(
                            componentInterface,
                            ignored -> new ArrayList<>())
                    .add(componentId);
        }
    }

    @Override
    public synchronized @NotNull List<NenjimComponentId> getComponentIds(
            @NotNull Class<? extends NenjimComponent> componentInterface) {
        validateComponentInterface(componentInterface);
        requireStarted();

        List<NenjimComponentId> componentIds =
                componentIdsByInterface.get(componentInterface);
        return componentIds == null ? List.of() : List.copyOf(componentIds);
    }

    @Override
    public synchronized @Nullable <T extends NenjimComponent> T getComponent(
            @NotNull NenjimComponentId componentId,
            @NotNull Class<T> expectedInterface) {
        Objects.requireNonNull(componentId, "componentId");
        validateComponentInterface(expectedInterface);
        requireStarted();

        NenjimComponent component = componentsById.get(componentId);
        if (component == null) {
            return null;
        }
        if (!expectedInterface.isInstance(component)) {
            throw new IllegalArgumentException(
                    "Nenjim component '" + componentId.value()
                            + "' was requested as " + expectedInterface.getName()
                            + " but is implemented by "
                            + component.getClass().getName());
        }
        return expectedInterface.cast(component);
    }

    private void requireStarted() {
        if (!started) {
            throw new IllegalStateException(
                    "Nenjim Registry service has not started successfully");
        }
    }

    private static void validateComponentInterface(@NotNull Class<?> type) {
        Objects.requireNonNull(type, "componentInterface");
        if (!type.isInterface()
                || !NenjimComponent.class.isAssignableFrom(type)) {
            throw new IllegalArgumentException(
                    "Expected an interface extending "
                            + NenjimComponent.class.getName()
                            + ", but received " + type.getName());
        }
    }

    private static @NotNull Set<Class<? extends NenjimComponent>>
    findComponentInterfaces(@NotNull Class<?> implementationClass) {
        LinkedHashSet<Class<? extends NenjimComponent>> componentInterfaces =
                new LinkedHashSet<>();

        for (Class<?> currentClass = implementationClass;
                currentClass != null;
                currentClass = currentClass.getSuperclass()) {
            for (Class<?> implementedInterface : currentClass.getInterfaces()) {
                collectComponentInterfaces(
                        implementedInterface,
                        componentInterfaces);
            }
        }
        return componentInterfaces;
    }

    private static void collectComponentInterfaces(
            @NotNull Class<?> interfaceType,
            @NotNull Set<Class<? extends NenjimComponent>> destination) {
        if (NenjimComponent.class.isAssignableFrom(interfaceType)) {
            @SuppressWarnings("unchecked")
            Class<? extends NenjimComponent> componentInterface =
                    (Class<? extends NenjimComponent>) interfaceType;
            destination.add(componentInterface);
        }

        for (Class<?> parentInterface : interfaceType.getInterfaces()) {
            collectComponentInterfaces(parentInterface, destination);
        }
    }

    private final Map<NenjimComponentId, NenjimComponent> componentsById =
            new LinkedHashMap<>();
    private final Map<Class<? extends NenjimComponent>, List<NenjimComponentId>>
            componentIdsByInterface = new LinkedHashMap<>();
    private boolean startAttempted;
    private boolean started;
}
