package org.implab.gradle.variants;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectCollection;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.implab.gradle.common.core.lang.Deferred;
import org.implab.gradle.common.core.lang.Strings;
import org.implab.gradle.common.sources.GenericSourceSet;
import org.implab.gradle.common.sources.SourcesPlugin;
import org.implab.gradle.variants.model.Layer;
import org.implab.gradle.variants.model.Variant;
import org.implab.gradle.variants.model.VariantsExtension;
import org.implab.gradle.variants.sources.LayerProjectionRule;
import org.implab.gradle.variants.sources.SourceSetMaterializer;
import org.implab.gradle.variants.sources.SourceSetProjection;
import org.implab.gradle.variants.sources.SourceSetProjections;
import org.implab.gradle.variants.sources.VariantLayerBinding;
import org.implab.gradle.variants.sources.VariantSourcesContext;
import org.implab.gradle.variants.sources.VariantSourcesExtension;

@NonNullByDefault
public abstract class VariantSourcesPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
        // Apply the main VariantsPlugin to ensure the core variant model is available.
        target.getPlugins().apply(VariantsPlugin.class);
        target.getPlugins().apply(SourcesPlugin.class);
        // Access the VariantsExtension to configure variant sources.
        var variantsExtension = target.getExtensions().getByType(VariantsExtension.class);
        var objectFactory = target.getObjects();

        var sources = SourcesPlugin.getSourcesExtension(target);

        var deferred = new Deferred<VariantSourcesContext>();
        var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class);

        var variantSourcesExtension = new VariantSourcesExtension() {
            @Override
            public NamedDomainObjectContainer<LayerProjectionRule> getLayerRules() {
                return layerProjectionRules;
            }

            @Override
            public void whenFinalized(Action<? super VariantSourcesContext> action) {
                deferred.whenResolved(action::execute);
            }
        };
        target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension);

        // create convention to automatically create layer projection rules for each
        // variant layer
        variantsExtension.getLayers().all(layer -> {
            // Automatically create a layer projection rule for each variant layer.
            variantSourcesExtension.layer(layer.getName(), rule -> {
                // Configure the source set name pattern based on the layer name.
                rule.getSourceSetNamePattern()
                        .convention("{variant}{layerCapitalized}")
                        .finalizeValueOnRead();
            });
        });

        var projections = objectFactory.domainObjectContainer(SourceSetProjection.class);

        Map<String, Set<VariantLayerBinding>> projectionBindings = new HashMap<>();

        var sourceSetProjections = new SourceSetProjections() {
            @Override
            public NamedDomainObjectCollection<SourceSetProjection> getProjections() {
                return projections;
            }

            @Override
            public Set<VariantLayerBinding> getBindings(String sourceSetName) {
                return projectionBindings.getOrDefault(sourceSetName, Set.of());
            }

            @Override
            public Set<VariantLayerBinding> getBindings(SourceSetProjection projection) {
                return getBindings(projection.getName());
            }
        };

        Set<String> materializedSourceSets = new HashSet<>();

        var materializer = new SourceSetMaterializer() {
            @Override
            public NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName) {
                return materializedSourceSets.add(sourceSetName)
                        ? sources.register(sourceSetName)
                        : sources.named(sourceSetName);
            }
        };

        var bindings = new VariantBindings();

        target.afterEvaluate(t -> {
            // Once the project is evaluated, resolve the deferred context and finalize the
            // sources configuration.
            variantsExtension.getLayers().all(bindings::addLayer);
            variantsExtension.getVariants().all(bindings::addVariant);

            variantsExtension.getLayers().all(layer -> {
                // For each layer, apply the projection rules to generate source set projections.

                var rule = layerProjectionRules.maybeCreate(layer.getName());
                var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}");
                // Generate source set names based on the pattern and variant/layer information.
                // This is a simplified example; real implementation would need to consider
                // all variants and layers.
                var sourceSetName = pattern.replace("{layer}", layer.getName())
                        .replace("{variant}", "main") // Placeholder for actual variant name
                        .replace("{layerCapitalized}", Strings.capitalize(layer.getName()));

                var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName);
                projections.add(projection);
                // Bind the projection to the corresponding variant layer.
                projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>())
                        .add(new VariantLayerBinding(layer.getName(), projection));
            });

            var context = new VariantSourcesContext() {

                @Override
                public SourceSetProjections getProjections() {
                    return sourceSetProjections;
                }

                @Override
                public SourceSetMaterializer getMaterializer() {
                    return materializer;
                }

                // Implementation of the context that provides access to variant and layer
                // information.
            };
            deferred.resolve(context);
        });
    }

    class VariantBindings {
        private final Set<Layer> layers = new HashSet<>();
        private final Set<Variant> variants = new HashSet<>();

        private final List<Consumer<? super VariantLayerBinding>> listeners = new ArrayList<>();

        void addLayer(Layer layer) {
            layers.add(layer);
            variants.stream()
                    .map(variant -> VariantLayerBinding.of(variant, layer))
                    .forEach(this::notifyBindingAdded);
        }

        void addVariant(Variant variant) {
            variants.add(variant);
            layers.stream()
                    .map(layer -> VariantLayerBinding.of(variant, layer))
                    .forEach(this::notifyBindingAdded);
        }

        void whenBindingAdded(Consumer<? super VariantLayerBinding> listener) {
            layers.stream()
                    .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer)))
                    .forEach(listener);
            listeners.add(listener);
        }

        private void notifyBindingAdded(VariantLayerBinding binding) {
            listeners.forEach(listener -> listener.accept(binding));
        }
    }

}
