package p.packer.repositories;

import p.packer.messages.PackerProjectContext;
import p.packer.models.PackerRuntimeSnapshot;
import p.packer.services.PackerProjectRuntime;

import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;

public final class PackerRuntimeRegistry {
    private final PackerRuntimeSnapshotLoader loader;
    private final ConcurrentMap<ProjectKey, PackerProjectRuntime> runtimes = new ConcurrentHashMap<>();
    private final AtomicLong nextGeneration = new AtomicLong(1L);

    public PackerRuntimeRegistry(PackerRuntimeSnapshotLoader loader) {
        this.loader = Objects.requireNonNull(loader, "loader");
    }

    public PackerProjectRuntime getOrLoad(PackerProjectContext project) {
        final PackerProjectContext safeProject = Objects.requireNonNull(project, "project");
        final ProjectKey key = ProjectKey.from(safeProject);
        return runtimes.compute(key, (ignored, current) -> {
            if (current != null && !current.disposed()) {
                return current;
            }
            return new PackerProjectRuntime(safeProject, loader.load(safeProject, nextGeneration.getAndIncrement()));
        });
    }

    public Optional<PackerProjectRuntime> find(PackerProjectContext project) {
        return Optional.ofNullable(runtimes.get(ProjectKey.from(Objects.requireNonNull(project, "project"))));
    }

    public PackerProjectRuntime refresh(PackerProjectContext project) {
        final PackerProjectContext safeProject = Objects.requireNonNull(project, "project");
        final ProjectKey key = ProjectKey.from(safeProject);
        return runtimes.compute(key, (ignored, current) -> {
            final var snapshot = loader.load(safeProject, nextGeneration.getAndIncrement());
            if (current == null || current.disposed()) {
                return new PackerProjectRuntime(safeProject, snapshot);
            }
            current.replaceSnapshot(snapshot);
            return current;
        });
    }

    public PackerProjectRuntime update(
            PackerProjectContext project,
            BiFunction<PackerRuntimeSnapshot, Long, PackerRuntimeSnapshot> updater) {
        final PackerProjectContext safeProject = Objects.requireNonNull(project, "project");
        final BiFunction<PackerRuntimeSnapshot, Long, PackerRuntimeSnapshot> safeUpdater =
                Objects.requireNonNull(updater, "updater");
        final ProjectKey key = ProjectKey.from(safeProject);
        return runtimes.compute(key, (ignored, current) -> {
            if (current == null || current.disposed()) {
                return new PackerProjectRuntime(safeProject, loader.load(safeProject, nextGeneration.getAndIncrement()));
            }
            try {
                final long generation = nextGeneration.getAndIncrement();
                current.updateSnapshot(snapshot -> safeUpdater.apply(snapshot, generation));
            } catch (RuntimeException exception) {
                current.replaceSnapshot(loader.load(safeProject, nextGeneration.getAndIncrement()));
            }
            return current;
        });
    }

    public void dispose(PackerProjectContext project) {
        final PackerProjectRuntime removed = runtimes.remove(ProjectKey.from(Objects.requireNonNull(project, "project")));
        if (removed != null) {
            removed.dispose();
        }
    }

    public void disposeAll() {
        runtimes.values().forEach(PackerProjectRuntime::dispose);
        runtimes.clear();
    }

    private record ProjectKey(
            String projectId,
            Path rootPath) {

        private static ProjectKey from(PackerProjectContext project) {
            return new ProjectKey(
                    Objects.requireNonNull(project.projectId(), "projectId"),
                    Objects.requireNonNull(project.rootPath(), "rootPath").toAbsolutePath().normalize());
        }
    }
}
