using System;
using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// The region pass's numbers, carried on Pass1Result for the report.
public sealed class RegionLedger
{
public bool RevertOn;
public long ThresholdCells;
public int PreIslands, PreNorth, PreSouth; // before the speck revert
public int PostIslands, PostNorth, PostSouth; // after
public int RevertedComponents; public long RevertedCells;
public bool CentreWasLand = true, CentreWasLandPre = true;
public long PreMin, PreMedian, PreMax, PostMin, PostMedian, PostMax; public double PreMean, PostMean;
public int[] PreHistogram, PostHistogram;
public List<(int id, long cells, byte hemi, float newHeight)> Reverted = new();
}
///
/// ⭐ PASS 1c — REGION LABELING + THE SPECK REVERT + THE ISLAND TAG (chat2/07). Runs over the
/// finished pass-1/1b classify field, IN PLACE, after the shelf/islets and before HMaxSeed
/// is retaken and anything classifies.
///
/// ═══ WHAT IT DOES, IN ORDER ═══
///
/// 1. LABEL over the classify field — the general layer.
/// Pure analysis: no height changes. (The pre-revert table is kept for the instrument.)
/// 2. REVERT (config-gated: TerrainGenConfig.SpeckRevert; threshold
/// MinLandComponentFrac × map area) — every NON-MAINLAND component below the
/// threshold is lowered to seabed. ORIGIN-BLIND: it judges components by size, not by
/// who made them — a small natural nub goes the same way as an offshore-pass dot.
/// Fewer, bigger. Two guards, ASSERTED per component, hard failure on violation:
/// LOWER-ONLY — every touched cell goes DOWN (land → below sea), never up;
/// COMPONENT-ONLY — only cells of the sub-threshold component are touched, never a
/// neighbour (the submerged skirt an offshore island leaves behind
/// is NOT this component and stays — a shoal, by the rule).
/// Together they make it impossible for "revert" to move the mainland coast.
/// MAINLAND IS NEVER A CANDIDATE (asserted), however small a pathological seed made it.
/// The seabed a cell is lowered to is the MEAN height of the component's adjacent sea
/// cells (its ring), held strictly below sea by BitDecrement — a flat shoal at the
/// local depth, not a pit and not a reef.
/// 3. RELABEL after a revert the layer is run again, so the exposed table and ids are those of
/// the finished field.
/// 4. TAG BY CONSTRUCTION: every cell of every non-mainland component is an island cell,
/// hemisphere from its component's centroid. The big organic detached masses are
/// tagged the same as an offshore-pass dot. Nothing here knows which pass made a cell.
///
/// The classify field IS the pass-1 array; pass 2 derives the render field from it and the curve is
/// identity at and below sea, so a reverted cell is seabed in both — asserted downstream by oracle (k).
///
public static class RegionPass
{
public sealed class Result
{
public RegionLabels LabelsPre; // before the revert (== Labels when the revert is off or reverted nothing)
public RegionLabels Labels; // the finished field's labeling
public bool[,] IsIsland; // the tag, by construction
public byte[,] IslandHemisphere;
public RegionLedger Ledger = new();
public List Notes = new();
}
/// The three thresholds of the chat2/07 batch, fractions of the map's area; Mid is the config default.
public const float ThresholdLowFrac = 1e-5f; // 168 cells at 4096 — only the smallest natural specks
public const float ThresholdMidFrac = 3e-5f; // 503 cells at 4096 — the offshore pass's own speck guard, applied to all land
public const float ThresholdHighFrac = 1e-4f; // 1,678 cells at 4096 — "fewer, bigger": takes small offshore islands too
public static Result Apply(float[,] height, int mapSize, float sea, TerrainGenConfig cfg)
{
var r = new Result();
var pre = RegionLabeling.Label(height, mapSize, sea);
r.LabelsPre = pre;
var led = r.Ledger;
led.CentreWasLandPre = pre.CentreWasLand;
FillPre(led, pre);
r.Notes.Add($"[Regions] labeled {pre.Regions.Count} land components ({pre.LandCells:N0} land cells): mainland id {pre.MainlandId} " +
$"({(pre.Mainland == null ? 0 : pre.Mainland.SizeCells):N0} cells, centre {(pre.CentreWasLand ? "is land" : "⚠ NOT LAND — fell back to the largest component")}), " +
$"{pre.IslandCount} islands (N {led.PreNorth} / S {led.PreSouth}); island cells min {led.PreMin} median {led.PreMedian} mean {led.PreMean:F0} max {led.PreMax}.");
RegionLabels final = pre;
led.RevertOn = cfg.SpeckRevert;
if (cfg.SpeckRevert)
{
long threshold = Math.Max(1L, (long)Math.Round(cfg.MinLandComponentFrac * (double)mapSize * mapSize));
led.ThresholdCells = threshold;
float strictlyBelowSea = MathF.BitDecrement(sea);
int n = mapSize;
// Which components go: non-mainland, below the threshold. Mainland is never a candidate.
var revert = new Dictionary();
foreach (var c in pre.Regions)
if (!c.IsMainland && c.SizeCells < threshold) revert[c.Id] = c;
if (pre.Mainland != null && revert.ContainsKey(pre.MainlandId))
throw new InvalidOperationException("[RegionPass] the mainland was selected for revert. Refusing.");
if (revert.Count > 0)
{
// The ring: the mean height of each doomed component's adjacent SEA cells.
var ringSum = new Dictionary();
var ringCnt = new Dictionary();
foreach (int id in revert.Keys) { ringSum[id] = 0; ringCnt[id] = 0; }
for (int x = 0; x < n; x++)
{
for (int y = 0; y < n; y++)
{
int id = pre.Id[x * n + y];
if (id == 0 || !revert.ContainsKey(id)) continue;
for (int dx = -1; dx <= 1; dx++)
{
int nx = x + dx; if (nx < 0 || nx >= n) continue;
for (int dy = -1; dy <= 1; dy++)
{
int ny = y + dy; if (ny < 0 || ny >= n || (dx == 0 && dy == 0)) continue;
if (pre.Id[nx * n + ny] != 0) continue; // land (this or another component)
ringSum[id] += height[nx, ny]; ringCnt[id]++;
}
}
}
}
var target = new Dictionary();
foreach (var (id, c) in revert)
{
float t = ringCnt[id] > 0 ? (float)(ringSum[id] / ringCnt[id]) : strictlyBelowSea;
target[id] = MathF.Min(t, strictlyBelowSea); // strictly below sea, whatever the ring says
}
// The revert, with both guards asserted cell by cell.
var touched = new Dictionary();
foreach (int id in revert.Keys) touched[id] = 0;
long cells = 0;
for (int x = 0; x < n; x++)
{
for (int y = 0; y < n; y++)
{
int id = pre.Id[x * n + y];
if (id == 0 || !target.TryGetValue(id, out float t)) continue; // COMPONENT-ONLY: nothing else is ever touched
float h = height[x, y];
if (h < sea)
throw new InvalidOperationException($"[RegionPass] component {id} cell ({x},{y}) is not land (h {h:G9} < sea {sea:G9}) — the labeling and the field disagree. Refusing.");
if (t >= h)
throw new InvalidOperationException($"[RegionPass] LOWER-ONLY violated at ({x},{y}): {h:G9} → {t:G9}. Refusing.");
height[x, y] = t;
touched[id]++; cells++;
}
}
foreach (var (id, c) in revert)
{
if (touched[id] != c.SizeCells)
throw new InvalidOperationException($"[RegionPass] COMPONENT-ONLY violated: component {id} has {c.SizeCells} cells, {touched[id]} touched. Refusing.");
led.Reverted.Add((id, c.SizeCells, c.Hemisphere, target[id]));
}
led.RevertedComponents = revert.Count;
led.RevertedCells = cells;
final = RegionLabeling.Label(height, mapSize, sea);
if (final.Mainland == null || pre.Mainland == null || final.Mainland.SizeCells != pre.Mainland.SizeCells)
throw new InvalidOperationException("[RegionPass] the mainland's size changed across the revert. Refusing.");
}
r.Notes.Add($"[Regions] speck revert ON (threshold {threshold:N0} cells = {cfg.MinLandComponentFrac:G2} of the map): " +
$"{led.RevertedComponents} sub-threshold non-mainland components ({led.RevertedCells:N0} cells) lowered to their ring's mean seabed; " +
$"lower-only and component-only asserted; mainland untouched by definition.");
}
else r.Notes.Add("[Regions] speck revert OFF.");
r.Labels = final;
led.CentreWasLand = final.CentreWasLand;
FillPost(led, final);
// ═══ THE TAG, BY CONSTRUCTION ═══
var tag = new bool[mapSize, mapSize];
var hemi = new byte[mapSize, mapSize];
for (int x = 0; x < mapSize; x++)
for (int y = 0; y < mapSize; y++)
{
int id = final.Id[x * mapSize + y];
if (id == 0 || id == final.MainlandId) continue;
tag[x, y] = true;
hemi[x, y] = final.Regions[id - 1].Hemisphere;
}
r.IsIsland = tag; r.IslandHemisphere = hemi;
r.Notes.Add($"[Regions] tag by construction: {final.IslandCount} islands (N {led.PostNorth} / S {led.PostSouth}), " +
$"{final.LandCells - (final.Mainland?.SizeCells ?? 0):N0} island cells tagged; island cells min {led.PostMin} median {led.PostMedian} mean {led.PostMean:F0} max {led.PostMax}.");
return r;
}
private static void FillPre(RegionLedger l, RegionLabels lab)
{
(l.PreNorth, l.PreSouth) = RegionLabeling.IslandsByHemisphere(lab);
var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab);
l.PreIslands = c; l.PreMin = mn; l.PreMedian = med; l.PreMean = mean; l.PreMax = mx; l.PreHistogram = h;
}
private static void FillPost(RegionLedger l, RegionLabels lab)
{
(l.PostNorth, l.PostSouth) = RegionLabeling.IslandsByHemisphere(lab);
var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab);
l.PostIslands = c; l.PostMin = mn; l.PostMedian = med; l.PostMean = mean; l.PostMax = mx; l.PostHistogram = h;
}
}
}