package org.implab.gradle.common.sources;

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

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.model.ObjectFactory;
import org.implab.gradle.common.sources.VariantArtifactSlot.BindingKey;

/**
 * Resolves artifact-slot inputs from already bound variant source-set usages.
 *
 * <p>This type is the bridge between two models:
 * <ul>
 * <li>{@link VariantSourcesExtension}, which emits resolved
 * {@link SourceSetUsageBinding variant/role/layer -> source-set} bindings;</li>
 * <li>{@link VariantArtifactSlot}, which exposes a DSL over slot contributions
 * and stores the resulting {@link VariantArtifactSlot.BindingResolver
 * contribution resolvers}.</li>
 * </ul>
 *
 * <p>The resolver records each emitted {@link SourceSetUsageBinding} and later
 * materializes a {@link FileCollection} for one concrete variant/slot pair.
 * For each variant/slot pair it asks the slot to materialize its contributions,
 * passes in the resolved source-set bindings for that variant, and
 * deduplicates resulting inputs by {@link BindingKey}. Contributions that do
 * not depend on topology bindings can still emit direct inputs even when that
 * binding collection is empty. The returned files are then typically wired into
 * an {@link ArtifactAssembly} as its sources.
 *
 * <p>Direct clients are infrastructure code rather than build scripts. The
 * typical usage pattern is:
 * <ol>
 * <li>create one resolver per project;</li>
 * <li>subscribe {@link #recordBinding(SourceSetUsageBinding)} to
 * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)};</li>
 * <li>call {@link #files(String, VariantArtifactSlot)} while registering an
 * {@link ArtifactAssembly} or another consumer that needs the slot inputs.</li>
 * </ol>
 *
 * <p>Build-script users normally do not instantiate this class directly. They
 * configure {@code variantArtifacts}, and {@link VariantArtifactsPlugin} uses
 * this resolver internally to turn slot rules into assembly inputs.
 */
@NonNullByDefault
public final class VariantArtifactsResolver {
    private final ObjectFactory objects;
    private final List<SourceSetUsageBinding> boundContexts = new ArrayList<>();

    public VariantArtifactsResolver(ObjectFactory objects) {
        this.objects = objects;
    }

    /**
     * Records one resolved variant source-set usage.
     *
     * <p>Intended to be used as a callback target for
     * {@link VariantSourcesExtension#whenBound(org.gradle.api.Action)}.
     *
     * @param context resolved variant/role/layer usage bound to a source set
     */
    public void recordBinding(SourceSetUsageBinding context) {
        boundContexts.add(context);
    }

    /**
     * Returns all source-set outputs selected by the given slot for the given
     * variant.
     *
     * <p>The result is built from recorded {@link SourceSetUsageBinding}
     * instances whose {@link SourceSetUsageBinding#variantName()} matches
     * {@code variantName}. Each matching binding is then fed into the slot
     * contribution pipeline; if multiple contributions resolve to the same
     * {@link BindingKey}, that source is included only once.
     *
     * <p>This method does not validate the model; validation is expected to be
     * performed earlier by {@link VariantArtifactsExtension}. Unknown variants
     * or slots with no matching rules simply produce an empty collection.
     *
     * @param variantName variant whose bound source-set usages should be scanned
     * @param slot slot definition that selects which outputs should be included
     * @return lazily wired file collection for the selected outputs
     */
    public FileCollection files(String variantName, VariantArtifactSlot slot) {
        var contexts = boundContexts.stream()
                .filter(context -> variantName.equals(context.variantName()))
                .toList();
        var builder = new FileCollectionBuilder(contexts);
        slot.acceptBindings(builder::visitBinding);
        return builder.build();
    }

    /**
     * Local materialization helper for one {@link #files(String, VariantArtifactSlot)}
     * call.
     */
    class FileCollectionBuilder {
        private final ConfigurableFileCollection files;
        private final Set<BindingKey> boundOutputs = new LinkedHashSet<>();
        private final Collection<SourceSetUsageBinding> contexts;

        FileCollectionBuilder(Collection<SourceSetUsageBinding> contexts) {
            this.files = objects.fileCollection();
            this.contexts = contexts;
        }

        FileCollection build() {
            return files;
        }

        void addOutput(VariantArtifactSlot.ResolvedBinding binding) {
            if (boundOutputs.add(binding.key()))
                files.from(binding.files());
        }

        void visitBinding(VariantArtifactSlot.BindingResolver resolver) {
            resolver.resolve(contexts, this::addOutput);
        }
    }

}
