package org.implab.gradle.variants;

import java.util.Objects;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
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.sources.CompileUnit;
import org.implab.gradle.variants.sources.CompileUnitSourceSetSpec;
import org.implab.gradle.variants.sources.CompileUnitsView;
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.CompileUnitLayoutConvention;
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";

    private static final String VARIANT_OUTPUTS_DIRECTORY = "variants";

    private static final String VARIANT_SOURCES_DIRECTORY = "src";

    @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 objects = target.getObjects();
        var providers = target.getProviders();

        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(objects);
            lateConfigurationPolicy.finalizePolicy();
            var sourceSetConfiguration = new SourceSetConfigurationRegistry(lateConfigurationPolicy::mode);

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

            var compileUnitLayoutConvention = new CompileUnitLayoutConvention(
                    providers.provider(() -> target.getLayout().getProjectDirectory().dir(VARIANT_SOURCES_DIRECTORY)),
                    target.getLayout().getBuildDirectory().dir(VARIANT_OUTPUTS_DIRECTORY));

            // apply layout convention to all compile unit source sets
            sourceSetConfiguration.addAction(compileUnitLayoutConvention);

            // 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 CompileUnitSourceSetSpec> action) {
                Strings.argumentNotNullOrBlank(variantName, "variantName");
                Objects.requireNonNull(action, "action can't be null");

                lateConfigurationPolicy.finalizePolicy();

                var variant = objects.named(Variant.class, variantName);

                whenAvailable(ctx -> ctx.configureVariant(variant, action));
            }

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

                lateConfigurationPolicy.finalizePolicy();

                var layer = objects.named(Layer.class, layerName);

                whenAvailable(ctx -> ctx.configureLayer(layer, action));
            }

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

                lateConfigurationPolicy.finalizePolicy();

                var variant = objects.named(Variant.class, variantName);
                var layer = objects.named(Layer.class, layerName);
                var unit = new CompileUnit(variant, layer);

                whenAvailable(ctx -> ctx.configureUnit(unit, action));
            }

            @Override
            public void configureEach(Action<? super CompileUnitSourceSetSpec> action) {
                Objects.requireNonNull(action, "action can't be null");

                lateConfigurationPolicy.finalizePolicy();

                whenAvailable(ctx -> ctx.configureEach(action));
            }
        };

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

    }
}
