package org.implab.gradle.common.sources;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;

import org.implab.gradle.common.core.lang.Closures;
import org.gradle.api.Action;
import org.gradle.api.Named;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.provider.Property;

import groovy.lang.Closure;
import groovy.lang.DelegatesTo;

/**
 * Maps a logical layer to per-source-set hooks.
 */
public abstract class BuildLayerBinding implements Named {
    public static final String DEFAULT_SOURCE_SET_NAME_PATTERN = "{variant}{layerCap}";

    private final String name;

    private final List<Action<? super GenericSourceSet>> sourceSetConfigureActions = new ArrayList<>();
    private final List<Action<? super SourceSetContext>> registeredActions = new ArrayList<>();
    private final List<Action<? super SourceSetContext>> boundActions = new ArrayList<>();
    private final List<NamedDomainObjectProvider<GenericSourceSet>> registeredSourceSets = new ArrayList<>();
    private final List<SourceSetContext> registeredContexts = new ArrayList<>();
    private final List<SourceSetContext> boundContexts = new ArrayList<>();
    private final Set<String> registeredSourceSetNames = new LinkedHashSet<>();

    @Inject
    public BuildLayerBinding(String name) {
        this.name = name;
        getSourceSetNamePattern().convention(DEFAULT_SOURCE_SET_NAME_PATTERN);
    }

    @Override
    public String getName() {
        return name;
    }

    public abstract Property<String> getSourceSetNamePattern();

    /**
     * Action applied to every registered source set for this layer.
     * Already registered source sets are configured immediately (replay).
     */
    public void configureSourceSet(Action<? super GenericSourceSet> configure) {
        sourceSetConfigureActions.add(configure);
        for (var sourceSet : registeredSourceSets)
            sourceSet.configure(configure);
    }

    public void configureSourceSet(
            @DelegatesTo(value = GenericSourceSet.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        configureSourceSet(Closures.action(configure));
    }

    /**
     * Layer-local callback fired after source-set registration.
     * Already emitted contexts are delivered immediately (replay).
     * For simple callbacks you can use delegate-only style
     * (for example {@code whenRegistered { sourceSetName() }}).
     * For nested closures prefer explicit parameter
     * ({@code whenRegistered { ctx -> ... }}).
     */
    public void whenRegistered(Action<? super SourceSetContext> action) {
        registeredActions.add(action);
        for (var context : registeredContexts)
            action.execute(context);
    }

    public void whenRegistered(
            @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
        whenRegistered(Closures.action(action));
    }

    /**
     * Layer-local callback fired for every resolved variant/role/layer usage.
     * Already emitted contexts are delivered immediately (replay).
     * For simple callbacks you can use delegate-only style
     * (for example {@code whenBound { variantName() }}).
     * For nested closures prefer explicit parameter
     * ({@code whenBound { ctx -> ... }}).
     */
    public void whenBound(Action<? super SourceSetContext> action) {
        boundActions.add(action);
        for (var context : boundContexts)
            action.execute(context);
    }

    public void whenBound(
            @DelegatesTo(value = SourceSetContext.class, strategy = Closure.DELEGATE_FIRST) Closure<?> action) {
        whenBound(Closures.action(action));
    }

    void notifyRegistered(SourceSetContext context) {
        if (registeredSourceSetNames.add(context.sourceSetName())) {
            var sourceSet = context.sourceSet();
            registeredSourceSets.add(sourceSet);

            for (var action : sourceSetConfigureActions)
                sourceSet.configure(action);
        }

        registeredContexts.add(context);
        for (var action : registeredActions)
            action.execute(context);
    }

    void notifyBound(SourceSetContext context) {
        boundContexts.add(context);
        for (var action : boundActions)
            action.execute(context);
    }
}
