package org.implab.gradle.variants;

import java.util.Objects;
import java.util.function.Predicate;

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.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.variants.core.Layer;
import org.implab.gradle.variants.core.Variant;
import org.implab.gradle.variants.core.VariantsExtension;
import org.implab.gradle.variants.core.VariantsView;
import org.implab.gradle.variants.sources.CompileUnit;
import org.implab.gradle.variants.sources.CompileUnitsView;
import org.implab.gradle.variants.sources.GenericSourceSet;
import org.implab.gradle.variants.sources.RoleProjectionsView;
import org.implab.gradle.variants.sources.VariantSourcesContext;
import org.implab.gradle.variants.sources.VariantSourcesExtension;
import org.implab.gradle.variants.sources.internal.CompileUnitNamer;
import org.implab.gradle.variants.sources.internal.DefaultCompileUnitNamingPolicy;
import org.implab.gradle.variants.sources.internal.DefaultLateConfigurationPolicySpec;
import org.implab.gradle.variants.sources.internal.DefaultVariantSourcesContext;
import org.implab.gradle.variants.sources.internal.SourceSetConfigurationRegistry;
import org.implab.gradle.variants.sources.internal.SourceSetRegistry;

@NonNullByDefault
public abstract class VariantSourcesPlugin implements Plugin<Project> {
    public static final String VARIANT_SOURCES_EXTENSION = "variantSources";

    @Override
    public void apply(Project target) {
        var extensions = target.getExtensions();

        // Apply the main VariantsPlugin to ensure the core variant model is available.
        target.getPlugins().apply(VariantsPlugin.class);
        // Access the VariantsExtension to configure variant sources.
        var variantsExtension = extensions.getByType(VariantsExtension.class);
        var objectFactory = target.getObjects();

        var deferred = new Deferred<VariantSourcesContext>();

        var lateConfigurationPolicy = new DefaultLateConfigurationPolicySpec();
        var namingPolicy = new DefaultCompileUnitNamingPolicy();

        variantsExtension.whenFinalized(variants -> {
            // create variant views
            var compileUnits = CompileUnitsView.of(variants);
            var roleProjections = RoleProjectionsView.of(variants);

            // create registries
            var sourceSetRegistry = new SourceSetRegistry(objectFactory);
            lateConfigurationPolicy.finalizePolicy();
            var sourceSetConfiguration = new SourceSetConfigurationRegistry(lateConfigurationPolicy::mode);

            // build compile unit namer
            var compileUnitNamer = CompileUnitNamer.builder()
                .addUnits(compileUnits.getUnits())
                .nameCollisionPolicy(namingPolicy.policy())
                .build();

            // create the context
            var context = new DefaultVariantSourcesContext(
                variants,
                compileUnits,
                roleProjections,
                compileUnitNamer,
                sourceSetRegistry,
                sourceSetConfiguration
            );
            deferred.resolve(context);
        });

        var variantSourcesExtension = new VariantSourcesExtension() {
            @Override
            public void whenAvailable(Action<? super VariantSourcesContext> action) {
                deferred.whenResolved(action::execute);
            }

            @Override
            public void lateConfigurationPolicy(Action<? super LateConfigurationPolicySpec> action) {
                action.execute(lateConfigurationPolicy);
            }

            @Override
            public void namingPolicy(Action<? super NamingPolicySpec> action) {
                action.execute(namingPolicy);
            }

            @Override
            public void variant(String variantName, Action<? super GenericSourceSet> action) {
                Strings.argumentNotNullOrBlank(variantName, "variantName");
                Objects.requireNonNull(action, "action can't be null");

                lateConfigurationPolicy.finalizePolicy();

                whenAvailable(ctx -> ctx.configureVariant(resolveVariant(ctx.getVariants(), variantName), action));
            }

            @Override
            public void layer(String layerName, Action<? super GenericSourceSet> action) {
                // protect external DSL
                Strings.argumentNotNullOrBlank(layerName, "layerName");
                Objects.requireNonNull(action, "action can't be null");

                lateConfigurationPolicy.finalizePolicy();

                whenAvailable(ctx -> ctx.configureLayer(resolveLayer(ctx.getVariants(), layerName), action));
            }

            @Override
            public void unit(String variantName, String layerName, Action<? super GenericSourceSet> action) {
                Strings.argumentNotNullOrBlank(layerName, "layerName");
                Strings.argumentNotNullOrBlank(variantName, "variantName");
                Objects.requireNonNull(action, "action can't be null");

                lateConfigurationPolicy.finalizePolicy();

                whenAvailable(ctx -> ctx.configureUnit(resolveCompileUnit(ctx, variantName, layerName), action));
            }
        };

        extensions.add(VariantSourcesExtension.class, VARIANT_SOURCES_EXTENSION, variantSourcesExtension);

    }

    private static Layer resolveLayer(VariantsView variants, String name) {
        return variants.getLayers().stream()
                .filter(named(name))
                .findAny()
                .orElseThrow(() -> new InvalidUserDataException("Layer '" + name + "' isn't declared"));
    }

    private static Variant resolveVariant(VariantsView variants, String name) {
        return variants.getVariants().stream()
                .filter(named(name))
                .findAny()
                .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't declared"));
    }

    private static CompileUnit resolveCompileUnit(VariantSourcesContext ctx, String variantName, String layerName) {
        return ctx.getCompileUnits().findUnit(
                resolveVariant(ctx.getVariants(), variantName),
                resolveLayer(ctx.getVariants(), layerName))
                .orElseThrow(() -> new InvalidUserDataException(
                        "The CompileUnit isn't declared for variant '" + variantName + "', layer '" + layerName + "'"));
    }

    private static Predicate<Named> named(String name) {
        return named -> named.getName().equals(name);
    }

}
