namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 12 — HabitabilityScore
/// Computes a per-tile habitability score used to place settlements.
/// formula: water_proximity*3 + flatness*2 + fertility*2 + trade_potential*1.5 + resource_proximity - elevation_extreme*2 - hazard_proximity*1.5
/// </summary>
public sealed class HabitabilityScoreStage : IWorldGenStage
{
    public string Name => "HabitabilityScore";

    private const int W = C.WORLD_WIDTH_TILES;
    private const int H = C.WORLD_HEIGHT_TILES;

    public void Run(WorldGenContext ctx)
    {
        var world   = ctx.World;
        var habitat = new float[W, H];

        ctx.ReportProgress(Name, 0f);

        // ── Water proximity via BFS wavefront ─────────────────────────────────
        var waterDist = BfsWaterDistance(world);
        ctx.ReportProgress(Name, 0.25f);

        // ── First-pass score (without trade_route_potential) ──────────────────
        Parallel.For(0, H, y =>
        {
            for (int x = 0; x < W; x++)
            {
                ref var tile = ref world.TileAt(x, y);
                if (tile.Biome == BiomeId.Ocean) continue;

                float e = tile.Elevation;
                float m = tile.Moisture;
                float t = tile.Temperature;

                float waterProx = 1f / (1f + waterDist[x, y]);
                float flatness  = 1f - LocalElevationVariance(world, x, y);
                float fertility = m * t;
                float resPx     = 1f / (1f + ResourceDistance(world, x, y));
                float elevEx    = Math.Max(0f, e - 0.70f) * 4f + Math.Max(0f, WorldState.SeaLevel + 0.03f - e) * 4f;

                habitat[x, y] = waterProx * 3f
                               + flatness  * 2f
                               + fertility * 2f
                               + resPx     * 1f
                               - elevEx    * 2f;
            }
        });

        ctx.ReportProgress(Name, 0.60f);

        // ── Second pass: add trade_route_potential ────────────────────────────
        // Use the median of first-pass scores as "high habitability" threshold
        var landScores = new List<float>(W * H / 4);
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            if (world.Tiles[x, y].Biome != BiomeId.Ocean && habitat[x, y] > 0)
                landScores.Add(habitat[x, y]);
        landScores.Sort();
        float median = landScores.Count > 0 ? landScores[landScores.Count / 2] : 1f;

        // Compute trade potential into a SEPARATE array to avoid a read-write race:
        // TradePotential reads habitat[nx,ny] values from neighboring tiles which
        // could be simultaneously written by other threads if we add directly.
        var tradeBonus = new float[W, H];
        Parallel.For(0, H, y =>
        {
            for (int x = 0; x < W; x++)
            {
                if (world.Tiles[x, y].Biome == BiomeId.Ocean) continue;
                tradeBonus[x, y] = TradePotential(habitat, x, y, median);
            }
        });
        // Single-threaded merge to avoid any write ordering ambiguity
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            habitat[x, y] += tradeBonus[x, y] * 1.5f;

        ctx.ReportProgress(Name, 0.85f);

        // ── Normalize to [0,1] ────────────────────────────────────────────────
        float minH = float.MaxValue, maxH = float.MinValue;
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
        {
            if (world.Tiles[x, y].Biome == BiomeId.Ocean) continue;
            minH = Math.Min(minH, habitat[x, y]);
            maxH = Math.Max(maxH, habitat[x, y]);
        }
        float range = maxH - minH > 1e-6f ? maxH - minH : 1f;
        Parallel.For(0, H, y =>
        {
            for (int x = 0; x < W; x++)
            {
                if (world.Tiles[x, y].Biome == BiomeId.Ocean) { habitat[x, y] = 0f; continue; }
                habitat[x, y] = (habitat[x, y] - minH) / range;
            }
        });

        world.Habitability = habitat;
        ctx.LogMessage("[HabitabilityScore] Computed habitability map.");
        ctx.ReportProgress(Name, 1f);
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    private static int[,] BfsWaterDistance(WorldState world)
    {
        var dist = new int[W, H];
        const int INF = int.MaxValue / 2;
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            dist[x, y] = INF;

        var queue = new Queue<(int x, int y)>();

        // Seed from all water tiles (ocean + HasRiver)
        for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
        {
            ref var t = ref world.TileAt(x, y);
            if (t.Biome == BiomeId.Ocean || (t.Features & FeatureFlags.HasRiver) != 0)
            {
                dist[x, y] = 0;
                queue.Enqueue((x, y));
            }
        }

        (int dx, int dy)[] dirs4 = { (0,-1),(1,0),(0,1),(-1,0) };
        while (queue.Count > 0)
        {
            var (cx, cy) = queue.Dequeue();
            foreach (var (ddx, ddy) in dirs4)
            {
                int nx = cx + ddx, ny = cy + ddy;
                if ((uint)nx >= W || (uint)ny >= H) continue;
                if (dist[nx, ny] != INF) continue;
                dist[nx, ny] = dist[cx, cy] + 1;
                queue.Enqueue((nx, ny));
            }
        }

        return dist;
    }

    private static float LocalElevationVariance(WorldState world, int x, int y, int radius = 2)
    {
        float sum = 0, sumSq = 0, count = 0;
        for (int dy = -radius; dy <= radius; dy++)
        for (int dx = -radius; dx <= radius; dx++)
        {
            int nx = x + dx, ny = y + dy;
            if ((uint)nx >= W || (uint)ny >= H) continue;
            float e = world.Tiles[nx, ny].Elevation;
            sum += e; sumSq += e * e; count++;
        }
        if (count < 2) return 0f;
        float mean = sum / count;
        float variance = sumSq / count - mean * mean;
        return Math.Min(1f, MathF.Sqrt(Math.Max(0f, variance)) * 10f);
    }

    private static int ResourceDistance(WorldState world, int x, int y)
    {
        // Returns approximate distance to nearest mountain, forest, or coast tile
        int best = 30;
        for (int r = 1; r <= 15; r++)
        {
            for (int dy = -r; dy <= r; dy++)
            for (int dx = -r; dx <= r; dx++)
            {
                if (Math.Abs(dx) != r && Math.Abs(dy) != r) continue;
                int nx = x + dx, ny = y + dy;
                if ((uint)nx >= W || (uint)ny >= H) continue;
                var b = world.Tiles[nx, ny].Biome;
                if (b is BiomeId.MountainAlpine or BiomeId.MountainForested
                    or BiomeId.TemperateDeciduous or BiomeId.Boreal or BiomeId.Coastal
                    or BiomeId.Coastal or BiomeId.SubtropicalForest)
                {
                    best = Math.Min(best, r);
                    goto nextTile;
                }
            }
            nextTile:;
            if (best < r) break;
        }
        return best;
    }

    private static float TradePotential(float[,] habitat, int x, int y, float median)
    {
        // Count high-habitability tiles reachable within a radius (centrality proxy)
        int count = 0;
        const int radius = 12;
        for (int dy = -radius; dy <= radius; dy++)
        for (int dx = -radius; dx <= radius; dx++)
        {
            int nx = x + dx, ny = y + dy;
            if ((uint)nx >= W || (uint)ny >= H) continue;
            if (habitat[nx, ny] > median) count++;
        }
        float diameter = 2 * radius + 1;
        return count / (diameter * diameter);
    }
}
