using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐ THE BASIN-GRAPH PLATE (rivers/04) — the taste gate on the foundation. Presentation only; reads
/// the graph and the height, writes pixels. Nothing here touches data.
///
/// What the eye is meant to check, per the task:
/// • is each SPILL (yellow ring, labelled) at the true low rim where water would actually overtop?
/// • which basins hold REAL LAKES (blue tint, the lake cells brighter) vs DRY sinks (amber tint)?
/// • does "who drains to whom" (the arrows: cyan → ocean, white → another basin, red = closed)
/// look like a physically sane network?
///
/// Label at each spill: "23M/7" = spill 23 m above the sea datum, 7 m climb from the basin
/// floor (the number the cap is judged against). Both on the RENDER surface.
///
public static class BasinGraphRenderer
{
private static readonly Color LakeTint = new(0.250f, 0.520f, 1.000f);
private static readonly Color LakeWater = new(0.180f, 0.420f, 0.980f);
private static readonly Color DryTint = new(0.980f, 0.660f, 0.250f);
private static readonly Color OutlineL = new(0.100f, 0.250f, 0.650f);
private static readonly Color OutlineD = new(0.600f, 0.330f, 0.060f);
private static readonly Color Spill = new(1.000f, 0.930f, 0.350f);
private static readonly Color EdgeOcean = new(0.250f, 0.900f, 1.000f);
private static readonly Color EdgeBasin = new(1.000f, 1.000f, 1.000f);
private static readonly Color EdgeNone = new(1.000f, 0.250f, 0.250f);
private static readonly Color Floor = new(0.050f, 0.050f, 0.050f);
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
private static readonly Color Shadow = new(0.000f, 0.000f, 0.000f);
private static readonly Color SeabedOutline = new(0.180f, 0.300f, 0.520f);
public static Image Plate(BasinGraph g, DrainageAnalysis.Plan plan, Image img, int n,
bool[] isOcean, bool[] isClassifyWater, string title, string subtitle, string third)
{
int[] basinId = plan.BasinId;
int total = n * n;
// 1. tint every basin cell by lake/dry; lake cells inside a basin drawn as water.
var lakeOf = new Dictionary();
var seabed = new HashSet();
foreach (var b in g.Nodes) { lakeOf[b.Id] = b.IsLake; if (b.IsSeabed) seabed.Add(b.Id); }
for (int i = 0; i < total; i++)
{
int id = basinId[i];
if (id == 0 || seabed.Contains(id)) continue;
int x = i / n, y = i % n;
bool isLakeBasin = lakeOf.TryGetValue(id, out bool l) && l;
if (isClassifyWater[i] && !isOcean[i])
{
img.SetPixel(x, y, isLakeBasin ? LakeWater : img.GetPixel(x, y).Lerp(LakeWater, 0.55f));
continue;
}
img.SetPixel(x, y, img.GetPixel(x, y).Lerp(isLakeBasin ? LakeTint : DryTint, 0.38f));
}
// 2. outline: a basin cell with a 4-neighbour of a different id.
for (int i = 0; i < total; i++)
{
int id = basinId[i];
if (id == 0) continue;
int x = i / n, y = i % n;
bool edge = (x > 0 && basinId[i - n] != id) || (x < n - 1 && basinId[i + n] != id)
|| (y > 0 && basinId[i - 1] != id) || (y < n - 1 && basinId[i + 1] != id);
if (!edge) continue;
if (seabed.Contains(id)) { img.SetPixel(x, y, SeabedOutline); continue; } // the seam: outline only
bool isLakeBasin = lakeOf.TryGetValue(id, out bool l) && l;
img.SetPixel(x, y, isLakeBasin ? OutlineL : OutlineD);
}
int thin = n >= 4096 ? 3 : 2, ring = n >= 4096 ? 16 : 9, ringW = n >= 4096 ? 4 : 3;
int floorR = n >= 4096 ? 6 : 3, head = n >= 4096 ? 22 : 12;
int scale = n >= 4096 ? 3 : 2;
// 3. the edges — the spill walk, arrowhead at the downstream end.
foreach (var b in g.LandNodes)
{
Color c = b.Downstream switch
{
DownstreamKind.Ocean => EdgeOcean,
DownstreamKind.Basin => EdgeBasin,
_ => EdgeNone,
};
var path = b.SpillPath;
if (path.Count >= 2)
{
for (int i = 1; i < path.Count; i++)
DrainageRenderer.Line(img, path[i - 1] / n, path[i - 1] % n, path[i] / n, path[i] % n, n, c, thin);
Arrowhead(img, path, n, c, head, thin);
}
else
{
// A spill that is itself the terminus (the rim cell is ocean) — or stuck on the spot.
DrainageRenderer.Disc(img, b.SpillCell / n, b.SpillCell % n, ring / 2, n, c);
}
}
// 4. spills, floors, labels.
foreach (var b in g.LandNodes)
{
int sx = b.SpillCell / n, sy = b.SpillCell % n;
DrainageRenderer.Ring(img, sx, sy, ring, n, Spill, ringW);
DrainageRenderer.Disc(img, b.FloorCell / n, b.FloorCell % n, floorR, n, Floor);
string lbl = $"{b.SpillAboveSeaM:F0}M/{b.SpillClimbM:F0}"; // TinyFont has no '+' or '^': "spill m above sea / climb m from floor"
Label(img, lbl, sx + ring + 4, sy - TinyFont.Height(scale) / 2, scale, n);
Label(img, $"#{b.Id}", b.FloorCell / n + floorR + 3, b.FloorCell % n - TinyFont.Height(scale) / 2, scale, n);
}
// 5. the legend.
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
TinyFont.Draw(img, title, 12, 12, s, Ink);
TinyFont.Draw(img, subtitle, 12, 12 + lh, s, Ink);
TinyFont.Draw(img, third, 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, "BLUE TINT = LAKE BASIN (SIGNIFICANT CLASSIFY WATER INSIDE) AMBER TINT = DRY SINK BLACK DOT = BASIN FLOOR (#ID)", 12, 12 + lh * 3, s, Ink);
TinyFont.Draw(img, "YELLOW RING = SPILL CELL. LABEL 12M/4 = SPILL 12 M ABOVE SEA / 4 M CLIMB FROM THE BASIN FLOOR TO OVERTOP (RENDER SURFACE)", 12, 12 + lh * 4, s, Ink);
TinyFont.Draw(img, "ARROW = WHERE THE SPILL DRAINS: CYAN TO OCEAN, WHITE INTO ANOTHER BASIN, RED = CLOSED. DATA LAYER ONLY - NOTHING FILLED, NOTHING CARVED", 12, 12 + lh * 5, s, Ink);
TinyFont.Draw(img, "FAINT BLUE OUTLINE, NO MARKS = SEABED PIT (A RENDER DEPRESSION UNDER THE CLASSIFY SEA - THE D-046 SEAM, INERT, EXCLUDED FROM THE GRAPH)", 12, 12 + lh * 6, s, Ink);
return img;
}
private static void Arrowhead(Image img, List path, int n, Color c, int len, int thick)
{
int end = path[^1];
int from = path[Math.Max(0, path.Count - 1 - 24)];
float ex = end / n, ey = end % n, fx = from / n, fy = from % n;
float dx = ex - fx, dy = ey - fy;
float L = MathF.Sqrt(dx * dx + dy * dy);
if (L < 1f) return;
dx /= L; dy /= L;
// two barbs, 30° either side of the reversed direction
const float a = 0.5236f;
float cs = MathF.Cos(a), sn = MathF.Sin(a);
float bx1 = -dx * cs - (-dy) * sn, by1 = -dx * sn + (-dy) * cs;
float bx2 = -dx * cs + (-dy) * sn, by2 = -(-dx) * sn + (-dy) * cs;
DrainageRenderer.Line(img, (int)ex, (int)ey, (int)(ex + bx1 * len), (int)(ey + by1 * len), n, c, thick);
DrainageRenderer.Line(img, (int)ex, (int)ey, (int)(ex + bx2 * len), (int)(ey + by2 * len), n, c, thick);
}
/// Ink over a one-px black shadow, clamped inside the image so a rim label near the edge is still readable.
private static void Label(Image img, string text, int x, int y, int scale, int n)
{
int w = TinyFont.Width(text, scale), h = TinyFont.Height(scale);
x = Math.Clamp(x, 0, Math.Max(0, n - w - 1));
y = Math.Clamp(y, 0, Math.Max(0, n - h - 1));
TinyFont.Draw(img, text, x + 1, y + 1, scale, Shadow);
TinyFont.Draw(img, text, x, y, scale, Ink);
}
}
}