package org.implab.gradle.variants.sources.internal;

import java.text.MessageFormat;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.Named;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.implab.gradle.variants.core.Layer;
import org.implab.gradle.variants.core.Variant;
import org.implab.gradle.variants.sources.CompileUnit;
import org.implab.gradle.variants.sources.GenericSourceSet;

@NonNullByDefault
public class SourceSetConfigurationRegistry {
    private static final Logger logger = Logging.getLogger(SourceSetConfigurationRegistry.class);

    private final Map<Layer, ReplayableQueue<GenericSourceSet>> sourcesByLayer = new LinkedHashMap<>();
    private final Map<Variant, ReplayableQueue<GenericSourceSet>> sourcesByVariant = new LinkedHashMap<>();
    private final Map<CompileUnit, ReplayableQueue<GenericSourceSet>> sourcesByUnit = new LinkedHashMap<>();

    private final Supplier<LateConfigurationMode> lateConfigurationMode;

    public SourceSetConfigurationRegistry(Supplier<LateConfigurationMode> lateConfigurationMode) {
        this.lateConfigurationMode = lateConfigurationMode;
    }

    public void addLayerAction(Layer layer, Action<? super GenericSourceSet> action) {
        addToActions(
                sourcesByLayer.computeIfAbsent(layer, key -> new ReplayableQueue<>()),
                action,
                MessageFormat.format(
                        "Source sets for [layer={0}] layer already materialized",
                        layer.getName()));
    }

    public void addVariantAction(Variant variant, Action<? super GenericSourceSet> action) {
        addToActions(
                sourcesByVariant.computeIfAbsent(variant, key -> new ReplayableQueue<>()),
                action,
                MessageFormat.format(
                        "Source sets for [variant={0}] variant already materialized",
                        variant.getName()));

    }

    public void addCompileUnitAction(CompileUnit unit, Action<? super GenericSourceSet> action) {
        addToActions(
                sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()),
                action,
                MessageFormat.format(
                        "Source set for [variant={0}, layer={1}] already materialed",
                        unit.variant().getName(),
                        unit.layer().getName()));
    }

    private void addToActions(
            ReplayableQueue<GenericSourceSet> actions,
            Action<? super GenericSourceSet> action,
            String assertMessage) {
        assertLazyConfiguration(actions.values(), assertMessage);
        actions.forEach(action::execute);
    }

    void assertLazyConfiguration(List<GenericSourceSet> sets, String message) {
        if (sets.size() == 0)
            return;

        var names = sets.stream().map(Named::getName).collect(Collectors.joining(", "));

        switch (lateConfigurationMode.get()) {
            case FAIL:
                throw new IllegalStateException(message + " [" + names + "]");
            case WARN:
                logger.warn(message + "\n\t" + names);
                break;
            default:
                break;
        }
    }

    public void applyConfiguration(CompileUnit unit, GenericSourceSet sourceSet) {
        sourcesByVariant.computeIfAbsent(unit.variant(), key -> new ReplayableQueue<>()).add(sourceSet);
        sourcesByLayer.computeIfAbsent(unit.layer(), key -> new ReplayableQueue<>()).add(sourceSet);
        sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()).add(sourceSet);
    }

    class ReplayableQueue<T> {
        private final List<Consumer<? super T>> consumers = new LinkedList<>();
        private final List<T> values = new LinkedList<>();

        public void add(T value) {
            consumers.forEach(consumer -> consumer.accept(value));
            values.add(value);
        }

        List<T> values() {
            return List.copyOf(values);
        }

        public void forEach(Consumer<? super T> consumer) {
            values.forEach(consumer);
            consumers.add(consumer);
        }
    }
}
