package com.livingworld.regions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import com.livingworld.config.SimulationConfig;
import com.livingworld.regions.cache.RegionCache;
import com.livingworld.regions.query.RegionQueryEngine;

/**
 * Coordinates region creation, loading, caching, dirty tracking, and unloading.
 */
public final class RegionManager implements com.livingworld.core.simulation.RegionManager {

    private final RegionFactory regionFactory;
    private final RegionStorage regionStorage;
    private final RegionCache regionCache;
    private final RegionQueryEngine queryEngine;
    private final RegionLifecycleController lifecycleController;
    private final SimulationConfig simulationConfig;

    public RegionManager(
            RegionFactory regionFactory,
            RegionStorage regionStorage,
            RegionCache regionCache,
            RegionQueryEngine queryEngine,
            RegionLifecycleController lifecycleController,
            SimulationConfig simulationConfig) {
        this.regionFactory = requireNonNull(regionFactory, "regionFactory");
        this.regionStorage = requireNonNull(regionStorage, "regionStorage");
        this.regionCache = requireNonNull(regionCache, "regionCache");
        this.queryEngine = requireNonNull(queryEngine, "queryEngine");
        this.lifecycleController = requireNonNull(lifecycleController, "lifecycleController");
        this.simulationConfig = requireNonNull(simulationConfig, "simulationConfig");
        this.simulationConfig.validate();
    }

    public Region getOrCreateRegion(RegionCoordinate coordinate) {
        requireNonNull(coordinate, "coordinate");

        Optional<Region> cached = regionCache.get(coordinate);
        if (cached.isPresent()) {
            return cached.get();
        }

        Region region = regionStorage.load(coordinate)
                .map(this::prepareLoadedRegion)
                .orElseGet(() -> regionFactory.createNewRegion(coordinate, 0L));

        if (!region.getCoordinate().equals(coordinate)) {
            throw new IllegalStateException(
                    "Loaded region coordinate " + region.getCoordinate()
                            + " does not match requested coordinate " + coordinate);
        }

        regionCache.put(region);
        if (region.isDirty()) {
            regionStorage.markDirty(region);
        }
        return region;
    }

    public Optional<Region> findRegion(RegionCoordinate coordinate) {
        requireNonNull(coordinate, "coordinate");
        return queryEngine.getRegion(coordinate);
    }

    public Region getOrCreateRegionAtBlock(
            String dimensionId,
            int blockX,
            int blockZ) {
        RegionCoordinate coordinate = RegionCoordinate.fromBlock(
                dimensionId,
                blockX,
                blockZ,
                simulationConfig.getRegionSizeChunks());
        return getOrCreateRegion(coordinate);
    }

    public Collection<Region> getActiveRegions() {
        return regionCache.allActive();
    }

    @Override
    public void markDirty(Region region) {
        requireNonNull(region, "region");
        Region cached = regionCache.get(region.getCoordinate())
                .orElseThrow(() -> new IllegalArgumentException(
                        "region is not managed by this RegionManager: " + region.getCoordinate()));
        if (cached != region) {
            throw new IllegalArgumentException(
                    "a different region instance is already cached for " + region.getCoordinate());
        }
        regionStorage.markDirty(region);
    }

    public void saveDirtyRegions() {
        regionStorage.saveDirtyRegions();
    }

    public void flushAll() {
        regionStorage.flushAll();
    }

    public void unloadRegion(RegionCoordinate coordinate) {
        requireNonNull(coordinate, "coordinate");
        Optional<Region> existing = regionCache.get(coordinate);
        if (existing.isEmpty()) {
            return;
        }

        Region region = existing.get();
        if (region.isDirty()) {
            regionStorage.save(region);
            region.clearDirty();
        }

        transitionForUnload(region);
        regionCache.remove(coordinate);
    }

    @Override
    public Optional<Region> resolve(RegionCoordinate coordinate) {
        return findRegion(coordinate);
    }

    @Override
    public List<Region> resolveAll(List<RegionCoordinate> coordinates) {
        requireNonNull(coordinates, "coordinates");
        List<Region> resolved = new ArrayList<>();
        for (RegionCoordinate coordinate : coordinates) {
            findRegion(requireNonNull(coordinate, "coordinate"))
                    .ifPresent(resolved::add);
        }
        return List.copyOf(resolved);
    }

    private Region prepareLoadedRegion(Region region) {
        requireNonNull(region, "loaded region");
        region.validate();

        switch (region.getLifecycleState()) {
            case UNLOADED, FAILED -> {
                lifecycleController.transition(region, RegionLifecycleState.LOADING);
                lifecycleController.transition(region, RegionLifecycleState.ACTIVE);
            }
            case LOADING -> lifecycleController.transition(region, RegionLifecycleState.ACTIVE);
            case ACTIVE, DIRTY -> {
                // Already available for simulation.
            }
            case SAVING, UNLOADING -> throw new IllegalStateException(
                    "Cannot activate a region in state " + region.getLifecycleState());
        }
        return region;
    }

    private void transitionForUnload(Region region) {
        switch (region.getLifecycleState()) {
            case ACTIVE -> {
                lifecycleController.transition(region, RegionLifecycleState.UNLOADING);
                lifecycleController.transition(region, RegionLifecycleState.UNLOADED);
                region.clearDirty();
            }
            case UNLOADED -> {
                // Nothing to do.
            }
            default -> throw new IllegalStateException(
                    "Cannot unload a region in state " + region.getLifecycleState());
        }
    }

    private static <T> T requireNonNull(T value, String name) {
        if (value == null) {
            throw new IllegalArgumentException(name + " must not be null");
        }
        return value;
    }
}
