using System;
using System.Collections.Generic;
namespace IslaApocalypse.Tools
{
/// One connected island of tagged offshore land, as the analysis sees it.
public sealed class IslandComponent
{
public int Id;
public long Cells;
public double CentroidX, CentroidY;
public int MinX, MinY, MaxX, MaxY;
/// Hemisphere by CENTROID (an island straddling the midline is counted once, where its mass is).
public byte Hemisphere;
///
/// ⚠ True if any cell of this island is 8-adjacent to land that is NOT tagged offshore —
/// i.e. the island touches the mainland. The moat exists to make this impossible; this is
/// the check that it did.
///
public bool BridgedToMainland;
}
///
/// ⭐ THE OFFSHORE ANALYSIS — counts islands, reads their hemisphere, and catches a land bridge.
/// Engine-free; used by the pass (to prove its own floor) and by the oracle (to prove it again,
/// independently, on the finished field).
///
/// ═══ THE HEMISPHERE CONVENTION — read from the code, not invented ═══
///
/// Pass 1's latitude scalar is y / MapSize (+ a ±0.1 wobble). The spine fades out where
/// that scalar exceeds 0.65 — "the southern fade" — and the "southern sinker" bites in the
/// BOTTOM 25 % of rows. So in this codebase, and in the lore it encodes (snow-town north,
/// shipwreck south): y increases SOUTHWARD. North is the top half of the image.
///
/// NORTH y ∈ [0, MapSize/2)
/// SOUTH y ∈ [MapSize/2, MapSize)
///
/// ⚠ The tag uses the clean row midline, NOT the wobbled latitude field. A hemisphere tag keyed
/// to a field that wanders ±10 % of the map would put the same island in different hemispheres
/// on different seeds for no geographic reason. The field's ORIENTATION is what is borrowed; its
/// wobble is not.
///
public static class OffshoreAnalysis
{
public const byte HemiNone = 0;
public const byte HemiNorth = 1;
public const byte HemiSouth = 2;
/// The convention, in one place. Every consumer of the tag reads hemisphere through this.
public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth;
public static string HemisphereName(byte h) => h switch
{
HemiNorth => "north", HemiSouth => "south", _ => "none",
};
// 8-connectivity, fixed order.
private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };
private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 };
///
/// Label the 8-connected components of tagged offshore land, and for each, whether it
/// touches untagged land (a bridge). +
/// define "land"; defines "offshore". Both are needed: the bridge test
/// is "tagged cell next to a land cell that is not tagged".
///
public static List Components(bool[,] tag, float[,] height, float sea, int mapSize)
=> Components(tag, height, sea, mapSize, out _);
///
/// As above, also returning the per-cell component id map (x * mapSize + y; 0 = not
/// tagged) — the debris guard needs membership, not just the list.
///
public static List Components(bool[,] tag, float[,] height, float sea, int mapSize,
out int[] idMap)
{
var comps = new List();
int n = mapSize;
var id = new int[n * n]; // 0 = unvisited / not tagged
idMap = id;
if (tag == null) return comps;
var stack = new Stack();
int next = 0;
for (int sx = 0; sx < n; sx++)
{
for (int sy = 0; sy < n; sy++)
{
if (!tag[sx, sy] || id[sx * n + sy] != 0) continue;
var c = new IslandComponent
{
Id = ++next, MinX = sx, MaxX = sx, MinY = sy, MaxY = sy,
};
double sumX = 0, sumY = 0;
id[sx * n + sy] = c.Id;
stack.Push(sx * n + sy);
while (stack.Count > 0)
{
int cur = stack.Pop();
int cx = cur / n, cy = cur % n;
c.Cells++; sumX += cx; sumY += cy;
if (cx < c.MinX) c.MinX = cx; if (cx > c.MaxX) c.MaxX = cx;
if (cy < c.MinY) c.MinY = cy; if (cy > c.MaxY) c.MaxY = cy;
for (int k = 0; k < 8; k++)
{
int nx = cx + DX[k], ny = cy + DY[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if (tag[nx, ny])
{
int ni = nx * n + ny;
if (id[ni] != 0) continue;
id[ni] = c.Id;
stack.Push(ni);
}
else if (height[nx, ny] >= sea)
{
// Land, not tagged offshore ⇒ mainland (or a lake-shore) touching
// this island. The moat should have made this impossible.
c.BridgedToMainland = true;
}
}
}
c.CentroidX = sumX / c.Cells;
c.CentroidY = sumY / c.Cells;
c.Hemisphere = HemisphereOfRow((int)Math.Round(c.CentroidY), mapSize);
comps.Add(c);
}
}
return comps;
}
/// Island counts per hemisphere, by component centroid.
public static (int north, int south) CountByHemisphere(List comps)
{
int nN = 0, nS = 0;
foreach (var c in comps)
{
if (c.Hemisphere == HemiNorth) nN++;
else if (c.Hemisphere == HemiSouth) nS++;
}
return (nN, nS);
}
///
/// Island SIZE statistics — the thing a count alone hides. 189 islands averaging 66 cells is
/// noise debris, not an archipelago; 12 islands averaging 900 cells is what the developer
/// asked for. Cells are map cells (1 column = 1 m at the target scale).
///
public static (long min, long median, double mean, long max, int belowThreshold)
SizeSummary(List comps, long threshold)
{
if (comps.Count == 0) return (0, 0, 0.0, 0, 0);
var sizes = new List(comps.Count);
double sum = 0; int below = 0;
foreach (var c in comps) { sizes.Add(c.Cells); sum += c.Cells; if (c.Cells < threshold) below++; }
sizes.Sort();
return (sizes[0], sizes[sizes.Count / 2], sum / sizes.Count, sizes[sizes.Count - 1], below);
}
/// How many components touch the mainland. Zero is the only acceptable answer.
public static int BridgedCount(List comps)
{
int b = 0;
foreach (var c in comps) if (c.BridgedToMainland) b++;
return b;
}
}
}