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.Map;
import java.util.Optional;

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.NamedDomainObjectContainer;
import org.gradle.api.model.ObjectFactory;

import groovy.lang.Closure;

public abstract class BuildVariantsExtension {
    private final ObjectFactory objects;
    private final LinkedHashMap<String, LayoutLayer> layersByName = new LinkedHashMap<>();
    private final NamedDomainObjectContainer<BuildVariant> variants;
    private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
    private boolean finalized;

    @Inject
    public BuildVariantsExtension(ObjectFactory objects) {
        this.objects = objects;
        variants = objects.domainObjectContainer(BuildVariant.class);

        variants.all(variant -> {
            if (finalized)
                throw new InvalidUserDataException(
                        "Variants model is finalized and cannot add variant '" + variant.getName() + "'");
        });
    }

    public Collection<LayoutLayer> getLayers() {
        return Collections.unmodifiableCollection(layersByName.values());
    }

    public NamedDomainObjectContainer<BuildVariant> getVariants() {
        return variants;
    }

    public LayoutLayer layer(String name, Action<? super LayoutLayer> configure) {
        ensureMutable("configure layers");
        var layer = layersByName.computeIfAbsent(requireName(name, "Layer name must not be null or blank"), this::newLayer);
        configure.execute(layer);
        return layer;
    }

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

    public LayoutLayer layer(String name) {
        return layer(name, it -> {
        });
    }

    public BuildVariant variant(String name, Action<? super BuildVariant> configure) {
        ensureMutable("configure variants");
        var variant = variants.maybeCreate(name);
        configure.execute(variant);
        return variant;
    }

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

    public BuildVariant variant(String name) {
        return variant(name, it -> {
        });
    }

    public void all(Action<? super BuildVariant> action) {
        variants.all(action);
    }

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

    public Collection<BuildVariant> getAll() {
        var all = new ArrayList<BuildVariant>();
        variants.forEach(all::add);
        return Collections.unmodifiableList(all);
    }

    public Optional<LayoutLayer> findLayer(String name) {
        var normalizedName = normalize(name);
        return normalizedName == null ? Optional.empty() : Optional.ofNullable(layersByName.get(normalizedName));
    }

    public LayoutLayer requireLayer(String name) {
        return findLayer(name)
                .orElseThrow(() -> new InvalidUserDataException("Layer '" + name + "' isn't defined"));
    }

    public Optional<BuildVariant> find(String name) {
        return Optional.ofNullable(variants.findByName(name));
    }

    public BuildVariant require(String name) {
        return find(name)
                .orElseThrow(() -> new InvalidUserDataException("Variant '" + name + "' isn't defined"));
    }

    public void whenFinalized(Action<? super BuildVariantsExtension> action) {
        if (finalized) {
            action.execute(this);
            return;
        }
        finalizedActions.add(action);
    }

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

    public boolean isFinalized() {
        return finalized;
    }

    public void finalizeModel() {
        if (finalized)
            return;

        validate();

        for (var variant : variants)
            variant.finalizeModel();

        finalized = true;

        var actions = new ArrayList<>(finalizedActions);
        finalizedActions.clear();
        for (var action : actions)
            action.execute(this);
    }

    public void validate() {
        var errors = new ArrayList<String>();

        for (var variant : variants)
            validateVariant(variant, layersByName, errors);

        if (!errors.isEmpty()) {
            var message = new StringBuilder("Invalid variants model:");
            for (var error : errors)
                message.append("\n - ").append(error);

            throw new InvalidUserDataException(message.toString());
        }
    }

    private static void validateVariant(BuildVariant variant, Map<String, LayoutLayer> layersByName, List<String> errors) {
        var variantName = normalize(variant.getName());
        if (variantName == null) {
            errors.add("Variant name must not be blank");
            return;
        }

        validateRoleNames(variant, errors);
        validateRoleMappings(variant, layersByName, errors);
    }

    private static void validateRoleNames(BuildVariant variant, List<String> errors) {
        var roleNames = new LinkedHashSet<String>();
        for (var role : variant.getRoles()) {
            var roleName = normalize(role.getName());
            if (roleName == null) {
                errors.add("Variant '" + variant.getName() + "' contains blank role name");
                continue;
            }
            if (!roleNames.add(roleName)) {
                errors.add("Variant '" + variant.getName() + "' contains duplicated role name '" + roleName + "'");
            }
        }
    }

    private static void validateRoleMappings(BuildVariant variant, Map<String, LayoutLayer> layersByName,
            List<String> errors) {
        for (var role : variant.getRoles()) {
            var seenLayers = new LinkedHashSet<String>();
            for (var layerName : role.getLayers().getOrElse(List.of())) {
                var normalizedLayerName = normalize(layerName);
                if (normalizedLayerName == null) {
                    errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
                    continue;
                }

                var layer = layersByName.get(normalizedLayerName);
                if (layer == null) {
                    errors.add("Variant '" + variant.getName() + "' references unknown layer '" + normalizedLayerName + "'");
                    continue;
                }

                if (!seenLayers.add(normalizedLayerName)) {
                    errors.add("Variant '" + variant.getName() + "', role '" + role.getName()
                            + "' contains duplicated layer reference '" + normalizedLayerName + "'");
                }
            }
        }
    }

    private static String normalize(String value) {
        if (value == null)
            return null;

        var trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private static String requireName(String value, String errorMessage) {
        var normalized = normalize(value);
        if (normalized == null)
            throw new InvalidUserDataException(errorMessage);
        return normalized;
    }

    private LayoutLayer newLayer(String name) {
        return objects.newInstance(LayoutLayer.class, name);
    }

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