package p.studio.compiler.workspaces.stages;

import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.ProjectDescriptor;
import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.workspaces.PipelineStage;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlySet;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
public class LoadSourcesPipelineStage implements PipelineStage {

    @Override
    public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) {
        final var issues = BuildingIssueSink.empty();
        ctx.resolvedWorkspace.stack().topologicalOrder.forEach(pId -> visitProjectSources(pId, ctx, logs, issues));
        return issues;
    }

    private static void visitProjectSources(
            final ProjectId pId,
            final BuilderPipelineContext ctx,
            final LogAggregator logs,
            final BuildingIssueSink issues) {
        final var pd = ctx.resolvedWorkspace.graph().projectTable().get(pId);
        logs.using(log).debug("Project [ " + pd.getName() + " ] source loading...");

        final var allowedExtensions = normalize(ctx.resolvedWorkspace.frontendSpec().getAllowedExtensions());
        // Iterates source roots; crawls files; registers them
        for (final var sourceRootPath : pd.getSourceRoots()) {
            logs.using(log).debug("Walking source root [ " + sourceRootPath + " ]");

            final List<Path> paths = new ArrayList<>();
            final var sourceCrawler = new SourceCrawler(allowedExtensions, paths);

            try {
                Files.walkFileTree(sourceRootPath, sourceCrawler);
                paths.sort(Path::compareTo);
            } catch (IOException e) {
                issues.report(builder -> builder
                        .error(true)
                        .message("Failed to load project [ " + pd.getName() + " ]")
                        .exception(e));
            }

            registerFiles(pId, pd, paths, ctx, logs, issues);
        }
    }

    private static void registerFiles(
            final ProjectId pId,
            final ProjectDescriptor pd,
            final List<Path> canonPaths,
            final BuilderPipelineContext ctx,
            final LogAggregator logs,
            final BuildingIssueSink issues) {
        for (var canonPath : canonPaths) {
            final long size;
            final long lastModified;
            try {
                final BasicFileAttributes attributes = Files.readAttributes(canonPath, BasicFileAttributes.class);
                size = attributes.size();
                lastModified = attributes.lastModifiedTime().toMillis();
            } catch (IOException e) {
                issues.report(builder -> builder
                        .error(true)
                        .message("Failed to read attributes for file: " + canonPath)
                        .exception(e));
                continue;
            }
            // register in dense tables
            final var relativePath = pd.getRootPath().relativize(canonPath);
            final var sourceHandle = new SourceHandle(pId, relativePath, canonPath, size, lastModified, ctx.sourceProviderFactory);
            ctx.fileTable.register(sourceHandle);
        }
    }

    private static ReadOnlySet<String> normalize(final ReadOnlySet<String> extensions) {
        return ReadOnlySet
                .wrap(extensions
                .map(String::toLowerCase)
                .map(s -> s.startsWith(".") ? s.substring(1) : s)
                .collect(Collectors.toSet()));
    }
}
