package org.implab.gradle.common.sources;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
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.InvalidUserDataException;
import org.gradle.api.Named;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.attributes.Attribute;

import groovy.lang.Closure;

public abstract class BuildVariant implements Named {
    private final String name;
    private final ObjectFactory objects;
    private boolean finalized;

    /**
     * Variant aggregate parts.
     */
    private final VariantAttributes attributes;
    private final LinkedHashMap<String, BuildRole> roles = new LinkedHashMap<>();
    private final List<LayerLink> links = new ArrayList<>();
    private final LinkedHashMap<String, BuildArtifactSlot> artifactSlots = new LinkedHashMap<>();

    @Inject
    public BuildVariant(String name, ObjectFactory objects, ProviderFactory providers) {
        this.name = name;
        this.objects = objects;
        attributes = new VariantAttributes(providers);
    }

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

    /**
     * Generic variant attributes interpreted by adapters.
     */
    public VariantAttributes getAttributes() {
        return attributes;
    }

    public void attributes(Action<? super AttributesSpec> action) {
        ensureMutable("configure attributes");
        action.execute(new AttributesSpec(attributes));
    }

    public void attributes(Closure<?> configure) {
        attributes(Closures.action(configure));
    }

    public <T> void attribute(Attribute<T> key, T value) {
        ensureMutable("set attributes");
        attributes.attribute(key, value);
    }

    public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
        ensureMutable("set attributes");
        attributes.attributeProvider(key, value);
    }

    public Collection<BuildRole> getRoles() {
        return Collections.unmodifiableCollection(roles.values());
    }

    public void roles(Action<? super RolesSpec> action) {
        ensureMutable("configure roles");
        action.execute(new RolesSpec());
    }

    public void roles(Closure<?> configure) {
        roles(Closures.action(configure));
    }

    public BuildRole role(String name, Action<? super BuildRole> configure) {
        ensureMutable("configure roles");
        var role = roles.computeIfAbsent(name, this::newRole);
        configure.execute(role);
        return role;
    }

    public BuildRole role(String name, Closure<?> configure) {
        return role(name, Closures.action(configure));
    }

    public BuildRole role(String name) {
        return role(name, r -> {
        });
    }

    public BuildRole getRoleByName(String name) {
        return roles.get(name);
    }

    public Collection<LayerLink> getLinks() {
        return Collections.unmodifiableList(links);
    }

    public void links(Action<? super LinksSpec> action) {
        ensureMutable("configure links");
        action.execute(new LinksSpec());
    }

    public void links(Closure<?> configure) {
        links(Closures.action(configure));
    }

    public LayerLink link(String from, String to, String kind) {
        ensureMutable("add links");
        var link = new LayerLink(from, to, kind);
        links.add(link);
        return link;
    }

    public Collection<BuildArtifactSlot> getArtifactSlots() {
        return Collections.unmodifiableCollection(artifactSlots.values());
    }

    public void artifactSlots(Action<? super ArtifactSlotsSpec> action) {
        ensureMutable("configure artifact slots");
        action.execute(new ArtifactSlotsSpec());
    }

    public void artifactSlots(Closure<?> configure) {
        artifactSlots(Closures.action(configure));
    }

    public BuildArtifactSlot artifactSlot(String name) {
        return artifactSlot(name, it -> {
        });
    }

    public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
        ensureMutable("configure artifact slots");
        var slot = artifactSlots.computeIfAbsent(name, this::newArtifactSlot);
        configure.execute(slot);
        return slot;
    }

    public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
        return artifactSlot(name, Closures.action(configure));
    }

    public BuildArtifactSlot getArtifactSlotByName(String name) {
        return artifactSlots.get(name);
    }

    Set<String> declaredLayerNames() {
        var result = new LinkedHashSet<String>();

        for (var role : roles.values())
            result.addAll(role.getLayers().getOrElse(java.util.List.of()));

        return result;
    }

    void finalizeModel() {
        if (finalized)
            return;

        for (var role : roles.values())
            role.finalizeModel();

        attributes.finalizeModel();
        finalized = true;
    }

    private BuildRole newRole(String roleName) {
        return objects.newInstance(BuildRole.class, roleName);
    }

    private BuildArtifactSlot newArtifactSlot(String slotName) {
        return objects.newInstance(BuildArtifactSlot.class, slotName);
    }

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

    public final class RolesSpec {
        public BuildRole role(String name, Action<? super BuildRole> configure) {
            return BuildVariant.this.role(name, configure);
        }

        public BuildRole role(String name, Closure<?> configure) {
            return BuildVariant.this.role(name, configure);
        }

        public BuildRole role(String name) {
            return BuildVariant.this.role(name);
        }

        public Collection<BuildRole> getAll() {
            return BuildVariant.this.getRoles();
        }

        public BuildRole getByName(String name) {
            return BuildVariant.this.getRoleByName(name);
        }
    }

    public final class LinksSpec {
        public LayerLink link(String from, String to, String kind) {
            return BuildVariant.this.link(from, to, kind);
        }

        public Collection<LayerLink> getAll() {
            return BuildVariant.this.getLinks();
        }
    }

    public final class ArtifactSlotsSpec {
        public BuildArtifactSlot artifactSlot(String name, Action<? super BuildArtifactSlot> configure) {
            return BuildVariant.this.artifactSlot(name, configure);
        }

        public BuildArtifactSlot artifactSlot(String name, Closure<?> configure) {
            return BuildVariant.this.artifactSlot(name, configure);
        }

        public BuildArtifactSlot artifactSlot(String name) {
            return BuildVariant.this.artifactSlot(name);
        }

        public Collection<BuildArtifactSlot> getAll() {
            return BuildVariant.this.getArtifactSlots();
        }

        public BuildArtifactSlot getByName(String name) {
            return BuildVariant.this.getArtifactSlotByName(name);
        }
    }

    public static final class AttributesSpec {
        private final VariantAttributes attributes;

        AttributesSpec(VariantAttributes attributes) {
            this.attributes = attributes;
        }

        public <T> void attribute(Attribute<T> key, T value) {
            attributes.attribute(key, value);
        }

        public <T> void attributeProvider(Attribute<T> key, Provider<? extends T> value) {
            attributes.attributeProvider(key, value);
        }

        public void string(String name, String value) {
            attribute(Attribute.of(name, String.class), value);
        }

        public void string(String name, Provider<? extends String> value) {
            attributeProvider(Attribute.of(name, String.class), value);
        }

        public void bool(String name, boolean value) {
            attribute(Attribute.of(name, Boolean.class), value);
        }

        public void bool(String name, Provider<? extends Boolean> value) {
            attributeProvider(Attribute.of(name, Boolean.class), value);
        }

        public void integer(String name, int value) {
            attribute(Attribute.of(name, Integer.class), value);
        }

        public void integer(String name, Provider<? extends Integer> value) {
            attributeProvider(Attribute.of(name, Integer.class), value);
        }

        public VariantAttributes asAttributes() {
            return attributes;
        }
    }
}
