using Theriapolis.Core.Data;
using Theriapolis.Core.World.Polylines;

namespace Theriapolis.Core.World;

/// <summary>
/// The runtime world model. Holds all canonical simulation data for the generated continent.
/// Arrays are indexed [x, y] with (0,0) at the top-left (north-west).
/// </summary>
public sealed class WorldState
{
    public ulong WorldSeed { get; init; }

    // ── Canonical arrays ─────────────────────────────────────────────────────
    public WorldTile[,] Tiles { get; } = new WorldTile[C.WORLD_WIDTH_TILES, C.WORLD_HEIGHT_TILES];

    // Convenience accessors into the tile array (avoid struct copies in hot paths)
    public ref WorldTile TileAt(int x, int y) => ref Tiles[x, y];

    // ── Macro grid ────────────────────────────────────────────────────────────
    public MacroCell[,]? MacroGrid { get; set; }

    // ── Content defs (loaded from JSON, not generated) ────────────────────────
    public BiomeDef[]?    BiomeDefs   { get; set; }
    public FactionDef[]?  FactionDefs { get; set; }

    // ── Phase 2+3: Polylines (source of truth for linear features) ────────────
    public List<Polyline> Rivers { get; } = new();
    public List<Polyline> Roads  { get; } = new();
    public List<Polyline> Rails  { get; } = new();

    // ── Phase 2+3: Settlements ───────────────────────────────────────────────
    public List<Settlement> Settlements { get; } = new();

    // ── Phase 2+3: Bridges (road/rail crossings over rivers) ────────────────
    public List<Bridge> Bridges { get; } = new();

    // ── Phase 2+3: Computed maps ─────────────────────────────────────────────
    public float[,]?           Habitability    { get; set; }
    public float[,]?           EncounterDensity { get; set; }
    public FactionInfluenceMap? FactionInfluence { get; set; }

    // ── Stage hashes for save integrity ───────────────────────────────────────
    // Each stage appends its hash here after completing.
    public Dictionary<string, ulong> StageHashes { get; } = new();

    // ── Helper: macro cell for a given world tile coordinate ─────────────────
    /// <summary>
    /// Looks up a macro cell by unwarped grid position.  Use this only for
    /// pre-ElevationGen stages or places where you explicitly want the raw
    /// grid lookup.  Most callers should use <see cref="MacroCellForTile"/>.
    /// </summary>
    public MacroCell MacroCellAt(int tileX, int tileY)
    {
        if (MacroGrid is null) throw new InvalidOperationException("MacroGrid not loaded yet.");
        int mx = Math.Clamp(tileX / (C.WORLD_WIDTH_TILES  / C.MACRO_GRID_WIDTH),  0, C.MACRO_GRID_WIDTH  - 1);
        int my = Math.Clamp(tileY / (C.WORLD_HEIGHT_TILES / C.MACRO_GRID_HEIGHT), 0, C.MACRO_GRID_HEIGHT - 1);
        return MacroGrid[mx, my];
    }

    /// <summary>
    /// Returns the macro cell stored on the given tile.  <see cref="ElevationGenStage"/>
    /// overwrites each tile's <see cref="WorldTile.MacroX"/> and <see cref="WorldTile.MacroY"/>
    /// with border-warped coordinates so that macro cell boundaries follow
    /// organic wiggly curves instead of grid-aligned lines (Addendum A §1).
    /// All post-ElevationGen stages and tests should use this method.
    /// </summary>
    public MacroCell MacroCellForTile(in WorldTile tile)
    {
        if (MacroGrid is null) throw new InvalidOperationException("MacroGrid not loaded yet.");
        return MacroGrid[tile.MacroX, tile.MacroY];
    }

    // ── Sea level constant ─────────────────────────────────────────────────────
    // Tiles with elevation < SeaLevel are ocean.
    public const float SeaLevel = 0.35f;

    // ── Fast hash for determinism tests ──────────────────────────────────────
    /// <summary>FNV-1a hash over all elevation values (for determinism tests).</summary>
    public ulong HashElevation()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
            for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
            {
                uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Elevation);
                hash = (hash ^ bits) * FNV_PRIME;
            }
        return hash;
    }

    public ulong HashMoisture()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
            for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
            {
                uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Moisture);
                hash = (hash ^ bits) * FNV_PRIME;
            }
        return hash;
    }

    public ulong HashTemperature()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
            for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
            {
                uint bits = BitConverter.SingleToUInt32Bits(Tiles[x, y].Temperature);
                hash = (hash ^ bits) * FNV_PRIME;
            }
        return hash;
    }

    public ulong HashBiomes()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        for (int y = 0; y < C.WORLD_HEIGHT_TILES; y++)
            for (int x = 0; x < C.WORLD_WIDTH_TILES; x++)
                hash = (hash ^ (byte)Tiles[x, y].Biome) * FNV_PRIME;
        return hash;
    }

    /// <summary>FNV-1a hash over all settlements (sorted by ID).</summary>
    public ulong HashSettlements()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        foreach (var s in Settlements.OrderBy(s => s.Id))
        {
            hash = (hash ^ (ulong)s.Id)    * FNV_PRIME;
            hash = (hash ^ (ulong)s.Tier)  * FNV_PRIME;
            hash = (hash ^ (ulong)s.TileX) * FNV_PRIME;
            hash = (hash ^ (ulong)s.TileY) * FNV_PRIME;
        }
        return hash;
    }

    /// <summary>FNV-1a hash over all polyline points (rivers, then roads, then rails).</summary>
    public ulong HashPolylines()
    {
        const ulong FNV_PRIME  = 1099511628211UL;
        const ulong FNV_OFFSET = 14695981039346656037UL;
        ulong hash = FNV_OFFSET;
        foreach (var polylineList in new[] { Rivers, Roads, Rails })
        foreach (var p in polylineList.OrderBy(p => p.Id))
        foreach (var pt in p.Points)
        {
            hash = (hash ^ BitConverter.SingleToUInt32Bits(pt.X)) * FNV_PRIME;
            hash = (hash ^ BitConverter.SingleToUInt32Bits(pt.Y)) * FNV_PRIME;
        }
        return hash;
    }
}
