package org.implab.gradle.common.files;

import java.io.File;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.gradle.api.Named;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.model.ObjectFactory;
import org.gradle.util.Configurable;
import org.implab.gradle.common.utils.Closures;

import groovy.lang.Closure;

public abstract class GenericSourceSet
        implements Named, Configurable<GenericSourceSet> {
    private final Logger logger = Logging.getLogger(GenericSourceSet.class);

    private final String name;

    private final NamedDomainObjectContainer<SourceDirectorySet> sourceDirectorySets;

    private final NamedDomainObjectContainer<GenericSourceSetOutput> outputs;

    private final FileCollection allOutputs;

    private final FileCollection allSourceDirectories;

    private final ObjectFactory objects;

    private final Set<String> declaredOutputs = new HashSet<>();

    @Inject
    public GenericSourceSet(String name, ObjectFactory objects, ProjectLayout layout) {
        this.name = name;
        this.objects = objects;

        sourceDirectorySets = objects.domainObjectContainer(
                SourceDirectorySet.class,
                this::createSourceDirectorySet);

        outputs = objects.domainObjectContainer(GenericSourceSetOutput.class);

        allSourceDirectories = objects.fileCollection().from(sourceDirectoriesProvider());

        allOutputs = objects.fileCollection().from(outputsProvider());

        outputs.addRule("Register a declared set on demand", this::registerOutputOnDemand);

        getSourceSetDir().convention(layout
                .getProjectDirectory()
                .dir(Paths.get("src", name).toString()));

        getOutputsDir().convention(layout
                .getBuildDirectory()
                .dir(name));
    }

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

    public abstract DirectoryProperty getSourceSetDir();

    public abstract DirectoryProperty getOutputsDir();

    public NamedDomainObjectContainer<SourceDirectorySet> getSets() {
        return sourceDirectorySets;
    }

    public NamedDomainObjectContainer<GenericSourceSetOutput> getOutputs() {
        return outputs;
    }

    public FileCollection getAllOutputs() {
        return allOutputs;
    }

    public FileCollection getAllSourceDirectories() {
        return allSourceDirectories;
    }

    public FileCollection output(String name) {
        return getOutputs().getAt(name).getFileCollection();
    }

    public void declareOutputs(String name, String... extra) {
        declaredOutputs.add(Objects.requireNonNull(name, "declareOutputs: The output name cannot be null"));
        for (var x : extra)
            declaredOutputs.add(Objects.requireNonNull(x, "declareOutputs: The output name cannot be null"));
    }

    @Override
    public GenericSourceSet configure(@SuppressWarnings("rawtypes") Closure configure) {
        Closures.apply(configure, this);
        return this;
    }

    private void registerOutputOnDemand(String outputName) {
        logger.info("SourceSet '{}': registerOutputOnDemand '{}'", name, outputName);

        if (declaredOutputs.contains(outputName))
            outputs.register(outputName);
    }

    private SourceDirectorySet createSourceDirectorySet(String name) {
        return objects.sourceDirectorySet(name, name);
    }

    private Callable<List<? extends FileCollection>> outputsProvider() {
        return () -> outputs.stream()
                .map(GenericSourceSetOutput::getFileCollection)
                .toList();
    }

    private Callable<Set<File>> sourceDirectoriesProvider() {
        return () -> sourceDirectorySets.stream()
                .flatMap(x -> x.getSrcDirs().stream())
                .collect(Collectors.toSet());
    }

}
