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

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
import org.implab.gradle.internal.ReplayableQueue;
import org.implab.gradle.variants.artifacts.OutgoingVariant;
import org.implab.gradle.variants.artifacts.Slot;
import org.implab.gradle.variants.core.Variant;

/**
 * Реестр исходящих вариантов. Связывает исходящие конфигурации с вариантами
 * сборки. Связь устанавливается 1:1.
 */
@NonNullByDefault
public class OutgoingRegistry {
    private final Map<Variant, OutgoingVariant> outgoingByVariant = new LinkedHashMap<>();
    private final ReplayableQueue<OutgoingVariant> outgoingVariants = new ReplayableQueue<>();
    private final ConfigurationContainer configurations;
    private final ObjectFactory objects;
    private final Set<Variant> declaredVariants;

    public OutgoingRegistry(
            ConfigurationContainer configurations,
            ObjectFactory objects,
            Set<Variant> declaredVariants) {
        this.configurations = configurations;
        this.objects = objects;
        this.declaredVariants = declaredVariants;
    }

    public Optional<OutgoingVariant> find(Variant variant) {
        return Optional.ofNullable(outgoingByVariant.get(variant));
    }

    public OutgoingVariant maybeCreate(Variant variant) {
        return find(variant).orElseGet(() -> create(variant));
    }

    public OutgoingVariant create(Variant variant) {
        if (!declaredVariants.contains(variant))
            throw new InvalidUserDataException("Variant " + variant + " isn't declared");
        if (outgoingByVariant.containsKey(variant))
            throw new InvalidUserDataException("Outgoing variant " + variant + " already exists");

        var configuration = configurations.consumable(outgoingConfigurationName(variant));
        var outgoing = new Outgoing(variant, configuration);

        outgoingByVariant.put(variant, outgoing);

        outgoingVariants.add(outgoing);

        return outgoing;
    }

    /**
     * Replayable hook which is applied when an outgoing variant is defined
     *
     * @param action
     */
    public void configureEach(Consumer<? super OutgoingVariant> action) {
        outgoingVariants.forEach(action);
    }

    private String outgoingConfigurationName(Variant variant) {
        return variant.getName() + "Elements";
    }

    private class Outgoing implements OutgoingVariant {

        private final Variant variant;

        private final NamedDomainObjectProvider<? extends Configuration> configurationProvider;

        private final NamedDomainObjectContainer<Slot> slots;

        private final Property<Slot> primarySlot;

        public Outgoing(
                Variant variant,
                NamedDomainObjectProvider<? extends Configuration> configurationProvider) {
            this.variant = variant;
            this.configurationProvider = configurationProvider;
            this.slots = objects.domainObjectContainer(Slot.class);
            this.primarySlot = objects.property(Slot.class);
            primarySlot.finalizeValueOnRead();
        }

        @Override
        public Property<Slot> getPrimarySlot() {
            return primarySlot;
        }

        @Override
        public Variant getVariant() {
            return variant;
        }

        @Override
        public NamedDomainObjectProvider<? extends Configuration> getConfiguration() {
            return configurationProvider;
        }

        @Override
        public NamedDomainObjectContainer<Slot> getSlots() {
            return slots;
        }
    }

}
