package org.implab.gradle.common.sources;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
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.NamedDomainObjectContainer;
import org.gradle.api.model.ObjectFactory;

import groovy.lang.Closure;

public abstract class BuildVariantsExtension {
    private final NamedDomainObjectContainer<BuildLayer> layers;
    private final NamedDomainObjectContainer<BuildVariant> variants;
    private final List<Action<? super BuildVariantsExtension>> finalizedActions = new ArrayList<>();
    private boolean finalized;

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

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

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

    public NamedDomainObjectContainer<BuildLayer> getLayers() {
        return layers;
    }

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

    public void layers(Action<? super NamedDomainObjectContainer<BuildLayer>> action) {
        ensureMutable("configure layers");
        action.execute(layers);
    }

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

    public void variants(Action<? super NamedDomainObjectContainer<BuildVariant>> action) {
        ensureMutable("configure variants");
        action.execute(variants);
    }

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

    public BuildLayer layer(String name, Action<? super BuildLayer> configure) {
        ensureMutable("configure layers");
        var layer = layers.maybeCreate(name);
        configure.execute(layer);
        return layer;
    }

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

    public BuildLayer 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 BuildVariant getByName(String name) {
        return variants.findByName(name);
    }

    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>();

        var layersByName = new LinkedHashMap<String, BuildLayer>();
        for (var layer : layers)
            layersByName.put(layer.getName(), layer);

        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, BuildLayer> layersByName, List<String> errors) {
        var variantLayers = validateRoleMappings(variant, layersByName, errors);
        validateLinks(variant, variantLayers, errors);
    }

    private static Set<String> validateRoleMappings(BuildVariant variant, Map<String, BuildLayer> layersByName,
            List<String> errors) {
        var variantLayers = new LinkedHashSet<String>();

        for (var role : variant.getRoles()) {
            for (var layerName : role.getLayers().getOrElse(List.of())) {
                if (isBlank(layerName)) {
                    errors.add("Variant '" + variant.getName() + "', role '" + role.getName() + "' contains blank layer name");
                    continue;
                }

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

                variantLayers.add(layerName);
            }
        }

        return variantLayers;
    }

    private static void validateLinks(BuildVariant variant, Set<String> variantLayers, List<String> errors) {
        var seenLinks = new HashSet<String>();
        var edgesByKind = new HashMap<String, Map<String, Set<String>>>();

        for (var link : variant.getLinks()) {
            var from = normalize(link.from());
            var to = normalize(link.to());
            var kind = normalize(link.kind());

            if (from == null || to == null || kind == null) {
                errors.add("Variant '" + variant.getName() + "' has incomplete link (from/to/kind are required)");
                continue;
            }

            if (!variantLayers.contains(from)) {
                errors.add("Variant '" + variant.getName() + "' link references unknown source layer '"
                        + from + "'");
                continue;
            }

            if (!variantLayers.contains(to)) {
                errors.add("Variant '" + variant.getName() + "' link references unknown target layer '"
                        + to + "'");
                continue;
            }

            var linkKey = from + "\u0000" + to + "\u0000" + kind;
            if (!seenLinks.add(linkKey)) {
                errors.add("Variant '" + variant.getName() + "' has duplicated link tuple (from='" + from
                        + "', to='" + to + "', kind='" + kind + "')");
            }

            edgesByKind
                    .computeIfAbsent(kind, x -> new LinkedHashMap<>())
                    .computeIfAbsent(from, x -> new LinkedHashSet<>())
                    .add(to);
        }

        for (var entry : edgesByKind.entrySet()) {
            if (hasCycle(variantLayers, entry.getValue())) {
                errors.add("Variant '" + variant.getName() + "' contains cycle in links with kind '" + entry.getKey() + "'");
            }
        }
    }

    private static boolean hasCycle(Set<String> nodes, Map<String, Set<String>> edges) {
        var state = new HashMap<String, Integer>();

        for (var node : nodes) {
            if (dfs(node, state, edges))
                return true;
        }

        return false;
    }

    private static boolean dfs(String node, Map<String, Integer> state, Map<String, Set<String>> edges) {
        var current = state.getOrDefault(node, 0);
        if (current == 1)
            return true;
        if (current == 2)
            return false;

        state.put(node, 1);

        for (var next : edges.getOrDefault(node, Set.of())) {
            if (dfs(next, state, edges))
                return true;
        }

        state.put(node, 2);
        return false;
    }

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

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

    private static boolean isBlank(String value) {
        return normalize(value) == null;
    }

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