package com.livingworld.regions;

/**
 * Immutable representation of a region coordinate within a dimension.
 *
 * <p>A RegionCoordinate stores the region index (x, z) rather than raw chunk or block
 * coordinates. The region index identifies which 8×8-chunk region file contains a given
 * position.</p>
 *
 * <p>Negative coordinates are handled correctly using {@code Math#floorDiv}, matching
 * Minecraft's own coordinate system where block -1 belongs to chunk -1, not chunk 0.</p>
 */
public record RegionCoordinate(String dimensionId, int x, int z) {

    private static final int CHUNK_SIZE = 16;

    /**
     * Creates a region coordinate for the given dimension and region indices.
     *
     * @param dimensionId the dimension identifier (must not be null or blank)
     * @param x           the region X index
     * @param z           the region Z index
     */
    public RegionCoordinate {
        if (dimensionId == null || dimensionId.isBlank()) {
            throw new IllegalArgumentException("dimensionId must not be null or blank");
        }
    }

    // ------------------------------------------------------------------
    // Static factory methods
    // ------------------------------------------------------------------

    /**
     * Creates a RegionCoordinate from chunk coordinates.
     *
     * <p>The chunk coordinates are divided by {@code regionSizeChunks} using floor division
     * to determine the region index.</p>
     *
     * @param dimensionId      the dimension identifier (must not be null or blank)
     * @param chunkX           the chunk X coordinate
     * @param chunkZ           the chunk Z coordinate
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     * @return a RegionCoordinate representing the region that contains the given chunk
     */
    public static RegionCoordinate fromChunk(String dimensionId, int chunkX, int chunkZ, int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        int regionX = Math.floorDiv(chunkX, regionSizeChunks);
        int regionZ = Math.floorDiv(chunkZ, regionSizeChunks);
        return new RegionCoordinate(dimensionId, regionX, regionZ);
    }

    /**
     * Creates a RegionCoordinate from block coordinates.
     *
     * <p>Block coordinates are first converted to chunk coordinates using floor division
     * by {@link #CHUNK_SIZE}, then the resulting chunk coordinates are passed to
     * {@link #fromChunk(String, int, int, int)}.</p>
     *
     * @param dimensionId      the dimension identifier (must not be null or blank)
     * @param blockX           the block X coordinate
     * @param blockZ           the block Z coordinate
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     * @return a RegionCoordinate representing the region that contains the given block
     */
    public static RegionCoordinate fromBlock(String dimensionId, int blockX, int blockZ, int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
        int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
        return fromChunk(dimensionId, chunkX, chunkZ, regionSizeChunks);
    }

    // ------------------------------------------------------------------
    // Validation helpers
    // ------------------------------------------------------------------

    private static void validateRegionSize(int regionSizeChunks) {
        if (regionSizeChunks < 1) {
            throw new IllegalArgumentException("regionSizeChunks must be >= 1, got: " + regionSizeChunks);
        }
    }

    // ------------------------------------------------------------------
    // Chunk boundary methods
    // ------------------------------------------------------------------

    /**
     * Returns the minimum chunk X coordinate covered by this region.
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int minChunkX(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        return x() * regionSizeChunks;
    }

    /**
     * Returns the minimum chunk Z coordinate covered by this region.
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int minChunkZ(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        return z() * regionSizeChunks;
    }

    /**
     * Returns the maximum chunk X coordinate covered by this region.
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int maxChunkX(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        return (x() + 1) * regionSizeChunks - 1;
    }

    /**
     * Returns the maximum chunk Z coordinate covered by this region.
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int maxChunkZ(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        return (z() + 1) * regionSizeChunks - 1;
    }

    // ------------------------------------------------------------------
    // Block center methods
    // ------------------------------------------------------------------

    /**
     * Returns the X block coordinate of the centre of this region.
     *
     * <p>The centre is calculated as {@code floorDiv(minBlockX + maxBlockX, 2)} so that
     * negative coordinates behave correctly.</p>
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int centerBlockX(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        int minBlockX = minChunkX(regionSizeChunks) * CHUNK_SIZE;
        int maxBlockX = (maxChunkX(regionSizeChunks) + 1) * CHUNK_SIZE - 1;
        return Math.floorDiv(minBlockX + maxBlockX, 2);
    }

    /**
     * Returns the Z block coordinate of the centre of this region.
     *
     * @param regionSizeChunks the number of chunks along one axis in a region (must be >= 1)
     */
    public int centerBlockZ(int regionSizeChunks) {
        validateRegionSize(regionSizeChunks);
        int minBlockZ = minChunkZ(regionSizeChunks) * CHUNK_SIZE;
        int maxBlockZ = (maxChunkZ(regionSizeChunks) + 1) * CHUNK_SIZE - 1;
        return Math.floorDiv(minBlockZ + maxBlockZ, 2);
    }

    // ------------------------------------------------------------------
    // Stable identifier
    // ------------------------------------------------------------------

    /**
     * Returns a stable string representation suitable for use as a HashMap key.
     *
     * <p>The format is {@code "dimensionId:x:z"} which is deterministic and does not
     * depend on object identity.</p>
     */
    public String stableId() {
        return dimensionId() + ":" + x() + ":" + z();
    }

    // ------------------------------------------------------------------
    // Standard overrides
    // ------------------------------------------------------------------
    // equals() and hashCode() are provided by the record compiler.
    // Only toString is overridden for a more readable debug format.

    @Override
    public String toString() {
        return "RegionCoordinate{dimensionId='" + dimensionId() + "', x=" + x() + ", z=" + z() + "}";
    }
}