package org.implab.gradle.variants.artifacts.internal;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Task;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskProvider;
import org.implab.gradle.common.core.lang.Deferred;
import org.implab.gradle.common.core.lang.ReplayableQueue;
import org.implab.gradle.variants.artifacts.ArtifactAssemblies;
import org.implab.gradle.variants.artifacts.ArtifactAssembly;
import org.implab.gradle.variants.artifacts.ArtifactSlot;

@NonNullByDefault
public class ArtifactAssemblyRegistry implements ArtifactAssemblies {
    private final Map<ArtifactSlot, Deferred<ArtifactAssembly>> assembliesBySlots = new LinkedHashMap<>();
    private final ReplayableQueue<ArtifactAssembly> assemblies = new ReplayableQueue<>();

    public ArtifactAssemblyRegistry() {
    }

    public <T extends Task> ArtifactAssembly register(
            ArtifactSlot slot,
            TaskProvider<T> task,
            Function<? super T, ? extends Provider<? extends FileSystemLocation>> mapOutputArtifact) {

        var deferred = getDeferred(slot);
        if (deferred.resolved()) {
            throw new InvalidUserDataException("Artifact assembly '" + slot + "' is already registered");
        }
        var outputArtifact = task.flatMap(mapOutputArtifact::apply);

        var assembly = new Assembly(outputArtifact, task);
        deferred.resolve(assembly);
        assemblies.add(assembly);
        return assembly;
    }

    @Override
    public Optional<ArtifactAssembly> find(ArtifactSlot slot) {
        // to prevent creation of map entries on lookup use the map directly
        var deferred = assembliesBySlots.get(slot);
        return deferred != null && deferred.resolved()
                ? Optional.of(deferred.value())
                : Optional.empty();
    }

    @Override
    public void when(ArtifactSlot slot, Action<? super ArtifactAssembly> action) {
        getDeferred(slot).whenResolved(action::execute);
    }

    @Override
    public void configureEach(Action<? super ArtifactAssembly> action) {
        assemblies.forEach(action::execute);
    }

    private Deferred<ArtifactAssembly> getDeferred(ArtifactSlot slot) {
        return assembliesBySlots.computeIfAbsent(slot, k -> new Deferred<>());
    }

    static class Assembly implements ArtifactAssembly {

        private final Provider<? extends FileSystemLocation> artifact;
        private final TaskProvider<? extends Task> task;

        Assembly(Provider<? extends FileSystemLocation> artifact, TaskProvider<? extends Task> task) {
            this.artifact = artifact;
            this.task = task;
        }

        @Override
        public Provider<? extends FileSystemLocation> getArtifact() {
            return artifact;
        }

        @Override
        public TaskProvider<? extends Task> getAssemblyTask() {
            return task;
        }

    }
}
