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

import java.text.MessageFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.InvalidUserDataException;
import org.gradle.api.Named;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.implab.gradle.common.core.lang.ReplayableQueue;
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.CompileUnitSourceSetSpec;
import org.implab.gradle.variants.sources.GenericSourceSet;

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

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

    private final Supplier<LateConfigurationMode> lateConfigurationMode;

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

    public void addAction(Action<? super CompileUnitSourceSetSpec> action) {
        addToActions(
                sources,
                action,
                "Source sets already materialized");
    }

    public void addLayerAction(Layer layer, Action<? super CompileUnitSourceSetSpec> 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 CompileUnitSourceSetSpec> 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 CompileUnitSourceSetSpec> action) {
        addToActions(
                sourcesByUnit.computeIfAbsent(unit, key -> new ReplayableQueue<>()),
                action,
                MessageFormat.format(
                        "Source set for [variant={0}, layer={1}] already materialized",
                        unit.variant().getName(),
                        unit.layer().getName()));
    }

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

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

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

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

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