using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐⭐ THE CANDIDATE SET — one implementation, shared by every task that ranks rivers.
///
/// Extracted from `RiverPromotionTool` at rivers/03, unchanged in behaviour, because routing needs
/// exactly the same promoted set the count gate was judged on. **Two copies of this enumeration
/// would be two answers to "which rivers does the island have", and the epic rests on there being
/// one.** `RiverPromotionTool` now delegates here; its plates are byte-identical across the change.
///
/// ═══ WHAT IT DOES, AND WHAT IT DELIBERATELY DOES NOT ═══
///
/// It derives the COMPLETE candidate set from the arrays `DrainageAnalysis.Plan` exposes —
/// `Dir` / `Acc` / `BasinId` / `BasinInflow` / `FullFilled` — rather than from `Plan.Trunks` /
/// `Plan.Giants`, which are already truncated by the analysis's lean reporting caps. Ranking over
/// the truncated lists would measure the caps rather than the terrain.
///
/// ⚠ **`DrainageAnalysis` is reused, never rebuilt.** Every derived quantity here is a
/// reconstruction of a value the analysis computed internally, from state it exposes. Nothing the
/// analysis owns is reimplemented — least of all stem tracing, which is BOUND from the analysis's
/// own `Trunk` / `Giant` (see ).
///
public static class RiverCandidates
{
/// The enumeration's full result: the ranking, plus what the separation rule cost.
public sealed class Enumeration
{
/// Separated and above the floor, descending by DrainagePx, `Rank` assigned.
public List Ranked;
public long LandCells, SeaReachingCells, EndorheicCells, UnroutedCells;
public int TerminalBasins, SeaOutletsAll;
/// Sea outlets dropped by the separation rule — ALL of them, incl. one-cell trickles.
public int SuppressedCount;
public long SuppressedPx;
/// ⭐ The two that matter: only outlets clearing the floor could ever have been promoted.
public int SuppressedAboveFloor;
public long SuppressedAboveFloorPx;
/// Of those, suppressed by an outlet on a DIFFERENT landmass — not a delta mouth by any definition.
public int SuppressedCrossLandmass;
public long SuppressedCrossLandmassPx;
public List SuppressedAboveFloorAccs = new();
public int SeaCount { get { int s = 0; foreach (var c in Ranked) if (c.IsSea) s++; return s; } }
}
///
/// Enumerate every candidate major drainage, cap-free.
///
/// SEA every cell with Dir == D_SEA, carrying Acc there, then the
/// analysis's own greedy MinOutletSeparationPx rule so three mouths of one
/// delta are not three rivers.
/// ENDORHEIC every terminal basin in BasinId, carrying BasinInflow[id], with
/// terminal cell / area / depth re-derived from the exposed surfaces.
///
/// ⚠⚠ Throws unless the metric-comparability identity holds exactly — see below.
///
public static Enumeration Enumerate(DrainageAnalysis.Plan plan, float[,] height, int n,
long floorPx, int separationPx, RegionLabels regions)
{
int total = n * n;
var r = new Enumeration
{
LandCells = plan.LandCells, SeaReachingCells = plan.SeaReachingCells,
EndorheicCells = plan.EndorheicCells, UnroutedCells = plan.UnroutedCells,
TerminalBasins = plan.TerminalBasinCount,
};
// ---- SEA: every outlet, then the separation rule ----
var outlets = new List<(int cell, long acc)>();
long seaSum = 0;
for (int i = 0; i < total; i++)
if (plan.Dir[i] == DrainageAnalysis.D_SEA) { outlets.Add((i, plan.Acc[i])); seaSum += plan.Acc[i]; }
outlets.Sort((a, b) => b.acc.CompareTo(a.acc));
r.SeaOutletsAll = outlets.Count;
var sea = new List();
var kept = new List();
foreach (var (cell, acc) in outlets)
{
int cx = cell / n, cy = cell % n;
bool far = true; int suppressor = -1;
foreach (int pcell in kept)
{
float ddx = cx - pcell / n, ddy = cy - pcell % n;
if (ddx * ddx + ddy * ddy < (float)separationPx * separationPx) { far = false; suppressor = pcell; break; }
}
var c = new RiverCandidate { IsSea = true, Cell = cell, X = cx, Y = cy, TermX = cx, TermY = cy, DrainagePx = acc, SuppressedBySeparation = !far };
if (far) kept.Add(cell);
else
{
r.SuppressedCount++; r.SuppressedPx += acc;
if (acc >= floorPx)
{
r.SuppressedAboveFloor++; r.SuppressedAboveFloorPx += acc; r.SuppressedAboveFloorAccs.Add(acc);
// ⚠⚠ IS THE SUPPRESSOR EVEN ON THE SAME LANDMASS? The separation rule is a plain
// Euclidean distance test — it has no idea what land a coastline belongs to. On
// this deliberately fragmented archipelago (→ D-063) an ISLAND's only river can
// be suppressed by a mainland mouth 400 px away ACROSS WATER. Measured, not
// argued; the rule itself is NOT changed (it belongs to the analysis).
if (regions != null && suppressor >= 0)
{
int a = regions.Id[cell], b = regions.Id[suppressor];
if (a != 0 && b != 0 && a != b) { r.SuppressedCrossLandmass++; r.SuppressedCrossLandmassPx += acc; }
}
}
}
if (acc >= floorPx) sea.Add(c);
}
// ---- ENDORHEIC: every terminal basin, metrics re-derived ----
// After the analysis's reversion, BasinId is non-zero ONLY on terminal-basin cells, and
// Filled == the original height there — so FullFilled − height IS the fill depth, and the
// basin minimum is the argmin of height over the basin's cells. Both reconstruct exactly
// what the analysis computed internally as basinMinCell / basinDepthM / basinAreaPx.
int maxId = 0;
for (int i = 0; i < total; i++) if (plan.BasinId[i] > maxId) maxId = plan.BasinId[i];
var area = new long[maxId + 1];
var minCell = new int[maxId + 1];
var minH = new float[maxId + 1];
var depth = new float[maxId + 1];
for (int id = 0; id <= maxId; id++) { minCell[id] = -1; minH[id] = float.MaxValue; }
for (int i = 0; i < total; i++)
{
int id = plan.BasinId[i];
if (id == 0) continue;
area[id]++;
float h = height[i / n, i % n];
if (h < minH[id]) { minH[id] = h; minCell[id] = i; }
float d = WorldScale.MetresFromRaw(plan.FullFilled[i] - h);
if (d > depth[id]) depth[id] = d;
}
var endo = new List();
long endoSum = 0;
for (int id = 1; id <= maxId; id++)
{
if (minCell[id] < 0) continue;
long inflow = id < plan.BasinInflow.Length ? plan.BasinInflow[id] : 0;
endoSum += inflow;
if (inflow < floorPx) continue;
endo.Add(new RiverCandidate
{
IsSea = false, Cell = minCell[id], X = minCell[id] / n, Y = minCell[id] % n,
TermX = minCell[id] / n, TermY = minCell[id] % n, // replaced at bind time by the stem's pooling point
DrainagePx = inflow, BasinId = id, BasinAreaPx = area[id], BasinDepthM = depth[id],
});
}
// ═══ ⚠⚠ THE COMPARABILITY ASSERTION — the whole unified ranking rests on this ═══
//
// Both metrics are counts of contributing LAND CELLS on the same D8 field, and every land
// cell has exactly one destination — so the two populations partition the land exactly.
// If this identity ever fails, the two numbers are not the same unit and ranking them in
// one list is meaningless. It is asserted per seed rather than argued in a comment.
long partition = seaSum + endoSum + plan.UnroutedCells;
if (seaSum != plan.SeaReachingCells || endoSum != plan.EndorheicCells || partition != plan.LandCells)
throw new InvalidOperationException(
"[RiverCandidates] METRIC COMPARABILITY VIOLATION — the unified ranking is not sound on this field.\n" +
$" Σ Acc over sea outlets = {seaSum:N0}, expected SeaReachingCells = {plan.SeaReachingCells:N0}\n" +
$" Σ BasinInflow = {endoSum:N0}, expected EndorheicCells = {plan.EndorheicCells:N0}\n" +
$" sum + unrouted = {partition:N0}, expected LandCells = {plan.LandCells:N0}\n" +
"Sea-outlet drainage area and endorheic credited inflow must be the same unit over the same " +
"population for one ranking to mean anything. Refusing to rank. (rivers/02 Part 0 §2.)");
GD.Print($" ✅ comparability: Σ sea Acc {seaSum:N0} + Σ BasinInflow {endoSum:N0} + unrouted {plan.UnroutedCells:N0} == land {plan.LandCells:N0} — same unit, exact partition");
// ---- the unified ranking: one list, both termini, descending by contributing cells ----
var ranked = new List();
foreach (var c in sea) if (!c.SuppressedBySeparation) ranked.Add(c);
ranked.AddRange(endo);
ranked.Sort((a, b) => b.DrainagePx.CompareTo(a.DrainagePx));
for (int i = 0; i < ranked.Count; i++) ranked[i].Rank = i + 1;
r.Ranked = ranked;
return r;
}
///
/// Bind each candidate in to the Trunk / Giant the
/// analysis already traced, so plates draw REAL upland stems rather than anything reimplemented
/// here. Sea binds by outlet cell (identical greedy pick, identical order); endorheic binds by
/// BASIN ID — not by terminal coordinates, because a flat basin floor can have several cells at
/// the minimum height and the analysis's DFS tie-break need not match a row-major scan.
///
/// ⚠ Also transfers the analysis's own and
/// for endorheic candidates — rivers/03
/// needs the reference's routed/lake-ender verdict to compare against its own.
///
public static void BindCourses(DrainageAnalysis.Plan plan, int n, List need, string what)
{
var byOutlet = new Dictionary();
foreach (var t in plan.Trunks) byOutlet[(int)t.Outlet.x * n + (int)t.Outlet.y] = t;
var byBasin = new Dictionary();
foreach (var g in plan.Giants)
{
int cell = (int)g.Terminal.x * n + (int)g.Terminal.y;
int id = plan.BasinId[cell];
if (id > 0 && !byBasin.ContainsKey(id)) byBasin[id] = g;
}
int missing = 0;
foreach (var c in need)
{
if (c.Course != null) continue;
if (c.IsSea)
{
if (byOutlet.TryGetValue(c.Cell, out var t)) { c.Course = t.Course; c.TermX = (int)t.Outlet.x; c.TermY = (int)t.Outlet.y; }
}
else
{
// ⚠ Take the RIVER's terminus from the Giant, not the basin minimum this candidate
// is keyed on — see RiverCandidate.TermX. They differ on a flat basin floor, and
// marking the wrong one draws every endorheic stem detached from its own endpoint.
if (byBasin.TryGetValue(c.BasinId, out var g))
{
c.Course = g.Course; c.TermX = (int)g.Terminal.x; c.TermY = (int)g.Terminal.y;
c.AnalysisKind = g.Kind;
c.TerminalInClassifyWater = g.TerminalInClassifyWater;
}
}
if (c.Course == null) missing++;
}
if (missing > 0)
throw new InvalidOperationException(
$"[RiverCandidates] {missing} of {need.Count} candidates in {what} have no traced stem. The analysis's " +
"reporting caps are what produce the courses, so they must cover every candidate being drawn — " +
"raise ISLA_PROMOTE_MAX. Refusing to render a plate with rivers drawn as bare markers.");
}
}
}