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

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.implab.gradle.common.core.lang.FilePaths;
import org.implab.gradle.variants.artifacts.ArtifactAssemblySpec;
import org.implab.gradle.variants.artifacts.ArtifactSlot;
import org.implab.gradle.variants.sources.CompileUnit;
import org.implab.gradle.variants.sources.CompileUnitsView;
import org.implab.gradle.variants.sources.RoleProjectionsView;
import org.implab.gradle.variants.sources.SourceSetMaterializer;

/**
 * Адаптер между фрагментами артефактов, представленными в виде
 * {@link SlotContribution},
 * и ArtifactAssemblyRegistry, который оперирует уже собранными
 * {@link ArtifactAssembly}.
 *
 * Данный класс отвечает за сборку отдельных фрагментов артефактов в единый
 * каталог, который затем регистрируется в ArtifactAssemblyRegistry в виде
 * {@link ArtifactAssembly}. Сборка конечного артефакта происходит при помощи
 * задачи, которая копирует все входные файлы в выходной каталог. Задача
 * создается для каждого {@link ArtifactSlot} и использует
 * {@link SlotAssembly#inputs()} как источник входных данных.
 *
 * Для сборки используется паттерн Visitor: каждый фрагмент артефакта
 * представлен в виде реализации интерфейса {@link SlotContribution}, который
 * имеет метод accept, принимающий Visitor.
 * Visitor реализован во внутреннем классе {@link ContributionVisitor}, который
 * знает, как обрабатывать каждый тип фрагмента и добавлять его в сборку.
 * Для каждого {@link SlotContribution} создается ключ {@link SlotInputKey},
 * который используется для дедупликации: если фрагмент с таким же ключом уже
 * был добавлен, то он игнорируется.
 */
@NonNullByDefault
public class ArtifactAssemblyHandler {
    private final ObjectFactory objects;

    private final ArtifactAssemblyRegistry assemblyRegistry;

    private final DirectoryProperty assembliesDirectory;

    private final TaskContainer tasks;

    private final CompileUnitsView compileUnitsView;

    private final RoleProjectionsView roleProjectionsView;

    private final SourceSetMaterializer sourceSetMaterializer;

    private final Map<ArtifactSlot, SlotAssembly> slotInputs = new HashMap<>();

    public ArtifactAssemblyHandler(
            ObjectFactory objects,
            TaskContainer tasks,
            ArtifactAssemblyRegistry assemblyRegistry,
            CompileUnitsView compileUnitsView,
            RoleProjectionsView roleProjectionsView,
            SourceSetMaterializer sourceSetMaterializer) {
        this.objects = objects;
        this.tasks = tasks;
        this.assemblyRegistry = assemblyRegistry;
        this.compileUnitsView = compileUnitsView;
        this.roleProjectionsView = roleProjectionsView;
        this.sourceSetMaterializer = sourceSetMaterializer;

        assembliesDirectory = objects.directoryProperty();
    }

    public DirectoryProperty getAssembliesDirectory() {
        return assembliesDirectory;
    }

    public void configureAssembly(ArtifactSlot artifactSlot, Action<? super ArtifactAssemblySpec> action) {
        var visitor = contributionVisitor(artifactSlot);
        var spec = new DefaultArtifactAssemblySpec(objects, c -> c.accept(visitor));
        action.execute(spec);
    }

    public SlotContributionVisitor contributionVisitor(ArtifactSlot artifactSlot) {
        var assembly = slotInputs.computeIfAbsent(artifactSlot, this::createSlotAssembly);
        return new ContributionVisitor(artifactSlot, assembly);
    }

    /**
     * Создает сборку для указанного слота артефакта, сборка регистрируется в
     * ArtifactAssemblyRegistry, если для слота сборка уже была зарегистрирована
     * кем-то еще, то возникает ошибка.
     */
    private SlotAssembly createSlotAssembly(ArtifactSlot artifactSlot) {
        var assembly = new SlotAssembly();
        var fileCollection = assembly.inputs();

        var outputDirectory = outputDirectory(artifactSlot);

        var task = tasks.register(assembleTaskName(artifactSlot), Sync.class, copy -> {
            copy.setGroup(LifecycleBasePlugin.BUILD_GROUP);
            copy.into(outputDirectory);
            copy.from(fileCollection);
        });

        assemblyRegistry.register(artifactSlot, task, t -> outputDirectory);

        return assembly;
    }

    private String assembleTaskName(ArtifactSlot artifactSlot) {
        var variantName = artifactSlot.variant().getName();
        var slotName = artifactSlot.slot().getName();

        return "assembleVariantArtifactSlot"
                + "_v" + variantName.length() + "_" + variantName
                + "_s" + slotName.length() + "_" + slotName;
    }

    private Provider<Directory> outputDirectory(ArtifactSlot artifactSlot) {
        return assembliesDirectory.dir(
                FilePaths.cat(
                        artifactSlot.variant().getName(),
                        artifactSlot.slot().getName()));
    }

    /**
     * Собирает отдельные фрагменты артефактов в единый источник
     * {@link ConfigurableFileCollection}.
     * Для фрагментов {@link SlotContribution} используется механизм дедупликации:
     * для каждого
     * водящего фрагмента создается ключ {@link SlotInputKey} и добавляются
     * фрагменты только
     * с уникальным ключом, повторы игнорируются.
     */
    private class ContributionVisitor implements SlotContributionVisitor {
        // artifact slot for this assembly
        private final ArtifactSlot artifactSlot;

        // seen inputs, used for deduplication
        private final SlotAssembly assembly;

        ContributionVisitor(ArtifactSlot artifactSlot, SlotAssembly assembly) {
            this.artifactSlot = artifactSlot;
            this.assembly = assembly;
        }

        @Override
        public void visit(DirectContribution contribution) {
            contribute(
                    SlotInputKey.newUniqueKey("Direct input for " + artifactSlot),
                    contribution.input());
        }

        @Override
        public void visit(VariantOutputsContribution contribution) {
            var units = compileUnitsView.getUnitsForVariant(artifactSlot.variant());
            contributeCompileUnits(units, contribution.outputs());
        }

        @Override
        public void visit(RoleOutputsContribution contribution) {
            var roleProjection = roleProjectionsView.requireProjection(artifactSlot.variant(),
                    contribution.role());
            var units = roleProjectionsView.getUnits(roleProjection);

            contributeCompileUnits(units, contribution.outputs());

        }

        @Override
        public void visit(LayerOutputsContribution contribution) {
            var unit = compileUnitsView.requireUnit(artifactSlot.variant(), contribution.layer());
            contributeCompileUnits(Set.of(unit), contribution.outputs());
        }

        private void contributeCompileUnits(Set<CompileUnit> units, Set<String> outputs) {
            units.stream()
                    // expand variant compile units, make (compileUnit, outputName) pairs
                    .flatMap(unit -> outputs.stream()
                            .map(output -> new CompileUnitOutputKey(unit, output)))
                    .forEach(key -> contribute(
                            key,
                            sourceSetMaterializer.getSourceSet(key.unit())
                                    .map(s -> s.output(key.outputName()))));
        }

        private void contribute(SlotInputKey key, Object resolvedInput) {
            assembly.addSlotInput(key, resolvedInput);
        }
    }

    /** Состояние для отдельного слота */
    class SlotAssembly {
        private final ConfigurableFileCollection inputs = objects.fileCollection();
        private final Set<SlotInputKey> seen = new HashSet<>();

        public void addSlotInput(SlotInputKey key, Object input) {
            if (!seen.add(key))
                return;
            inputs.from(input);
        }

        public FileCollection inputs() {
            return inputs;
        }
    }
}
