package org.implab.gradle.common.sources;

import java.util.Optional;

import javax.inject.Inject;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Named;
import org.gradle.api.NamedDomainObjectContainer;
import org.implab.gradle.common.core.lang.Closures;

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

/**
 * Artifact model for one topology variant declared in
 * {@link VariantArtifactsExtension}.
 *
 * <p>A {@code VariantArtifact} groups one or more
 * {@link VariantArtifactSlot artifact representation slots}. The primary slot
 * becomes the main artifact of {@code <variant>Elements}; remaining slots are
 * published as secondary outgoing variants by {@link VariantArtifactsPlugin}.
 */
@NonNullByDefault
public class VariantArtifact implements Named {
    private final String name;
    private final NamedDomainObjectContainer<VariantArtifactSlot> slots;
    private String primarySlotName;
    private boolean finalized;

    @Inject
    public VariantArtifact(String name, NamedDomainObjectContainer<VariantArtifactSlot> slots) {
        this.name = normalize(name, "variant artifact name must not be null or blank");
        this.slots = slots;

        slots.all(slot -> {
            if (finalized)
                throw new InvalidUserDataException(
                        "Variant artifact '" + this.name + "' is finalized and cannot add slot '" + slot.getName() + "'");
        });
    }

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

    public NamedDomainObjectContainer<VariantArtifactSlot> getSlots() {
        return slots;
    }

    public VariantArtifactSlot slot(String name) {
        return slot(name, slot -> {
        });
    }

    public VariantArtifactSlot slot(String name, Action<? super VariantArtifactSlot> configure) {
        ensureMutable("configure slots");
        var slot = slots.maybeCreate(normalize(name, "slot name must not be null or blank"));
        configure.execute(slot);
        return slot;
    }

    public VariantArtifactSlot slot(
            String name,
            @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        return slot(name, Closures.action(configure));
    }

    public Optional<VariantArtifactSlot> findSlot(String slotName) {
        return Optional.ofNullable(slots.findByName(normalize(slotName, "slot name must not be null or blank")));
    }

    public void primarySlot(String slotName) {
        ensureMutable("configure primary slot");
        primarySlotName = normalize(slotName, "primary slot name must not be null or blank");
    }

    public VariantArtifactSlot primarySlot(String slotName, Action<? super VariantArtifactSlot> configure) {
        ensureMutable("configure primary slot");
        var slot = slot(slotName, configure);
        primarySlot(slot.getName());
        return slot;
    }

    public VariantArtifactSlot primarySlot(
            String slotName,
            @DelegatesTo(value = VariantArtifactSlot.class, strategy = Closure.DELEGATE_FIRST) Closure<?> configure) {
        return primarySlot(slotName, Closures.action(configure));
    }

    public Optional<String> findPrimarySlotName() {
        return Optional.ofNullable(primarySlotName)
                .or(() -> slots.getNames().size() == 1 ? Optional.of(slots.iterator().next().getName()) : Optional.empty());
    }

    public String requirePrimarySlotName() {
        return findPrimarySlotName()
                .orElseThrow(() -> new InvalidUserDataException(
                        "Variant artifact '" + name + "' must declare primary slot because it has multiple slots"));
    }

    public Optional<VariantArtifactSlot> findPrimarySlot() {
        return findPrimarySlotName().flatMap(this::findSlot);
    }

    public VariantArtifactSlot requirePrimarySlot() {
        var resolvedPrimarySlotName = requirePrimarySlotName();
        return findSlot(resolvedPrimarySlotName)
                .orElseThrow(() -> new InvalidUserDataException(
                        "Variant artifact '" + name + "' declares unknown primary slot '" + resolvedPrimarySlotName + "'"));
    }

    public VariantArtifactSlot requireSlot(String slotName) {
        var normalizedSlotName = normalize(slotName, "slot name must not be null or blank");
        return Optional.ofNullable(slots.findByName(normalizedSlotName))
                .orElseThrow(() -> new InvalidUserDataException(
                        "Variant artifact '" + name + "' doesn't declare slot '" + normalizedSlotName + "'"));
    }

    void finalizeModel() {
        if (finalized)
            return;

        for (var slot : slots)
            slot.finalizeModel();

        finalized = true;
    }

    static String normalize(String value, String message) {
        return Optional.ofNullable(value)
                .map(String::trim)
                .filter(trimmed -> !trimmed.isEmpty())
                .orElseThrow(() -> new InvalidUserDataException(message));
    }

    private void ensureMutable(String operation) {
        if (finalized)
            throw new InvalidUserDataException("Variant artifact '" + name + "' is finalized and cannot " + operation);
    }
}
