import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers

// MARK: - FacetsGenerator

/// **The faceted background, rendered** — the Swift port of `board-backgrounds-faceted.html`'s
/// `genFacets` (DESIGN/explorations/board-backgrounds.md ▸ Faceted gallery, reviewed 2026-08-07).
///
/// A jittered point scatter over a frame slightly larger than the picture, Delaunay-triangulated,
/// every triangle filled with its own colour off the recipe's narrow lightness band.
///
/// ### Pure, and deliberately nothing else
///
/// No SwiftUI, no `NSColor`, no actor, no filesystem: recipe in, pixels out. That is what lets the
/// picker render a dozen previews off the main actor and the write path render one at 3072 px in the
/// same call, from the same code, with no risk that the preview and the file are different pictures.
///
/// ### Normalized space, so one seed means one composition at every size
///
/// The gallery works in a 480×300 SVG viewBox; this works in a 16:10 **unit** frame — x 0…1,
/// y 0…0.625 — with every constant divided by 480. A seed therefore names a composition rather than
/// a composition-at-a-size: the 240 px preview in the picker and the 3072 px file written to the
/// board folder are the same mesh, scaled.
///
/// ### The scatter runs past the frame on purpose
///
/// Points are laid over a region inset **outward** on all four sides by a margin the density
/// chooses (`FacetsRecipe.Density.scatterMargin`, which carries the whole reasoning). Delaunay
/// triangulation only covers the convex hull of its points, so a scatter that stopped at the frame
/// edge would leave the ground colour showing in a ragged border. The margin is sized so that even
/// the worst jitter draw puts every boundary-cell point at or beyond the frame edge — the picture is
/// therefore entirely interior to the hull, and the ragged hull is cropped away.
public enum FacetsGenerator: Sendable {

    /// **The name a generated background is written under** (`BoardStore.applyGeneratedBackground`).
    ///
    /// One fixed name rather than a minted one, because regenerating is the overwhelmingly common
    /// gesture — the user rerolls until they like it — and a fresh UUID per roll would leave a board
    /// folder full of abandoned PNGs the app never offers to clean up. The collision ladder handles
    /// the one case a fixed name cannot: somebody else's `facets.png` already sitting there.
    public static let fileName = "facets.png"

    /// The normalized frame: 1 wide, 10/16 tall.
    static let frameHeight = 0.625

    /// **The fraction of a cell a jittered point can be pushed toward the cell's far corner** — the
    /// upper end of the 0.08…0.92 placement band, and therefore the number the boundary ring has to
    /// beat (`FacetsRecipe.Density.scatterMargin`).
    static let jitterReach = 0.92

    /// **The anti-seam stroke**, 0.7 px at the gallery's 480-wide scale. Each triangle is stroked in
    /// its *own* fill colour, which is the whole trick: adjacent antialiased edges otherwise leave a
    /// hairline of the ground colour between every pair of faces, and a mesh full of those reads as a
    /// wireframe rather than a surface.
    static let strokeWidth = 0.7 / 480

    // MARK: Rendering

    /// The pixel height that goes with `width` — the 16:10 frame, rounded.
    public static func pixelHeight(forWidth width: Int) -> Int {
        max(1, Int((Double(width) * frameHeight).rounded()))
    }

    /// This recipe's mesh, drawn at `pixelWidth` — opaque sRGB, no alpha to composite and none to
    /// store.
    ///
    /// `nil` only when CoreGraphics declines to make the bitmap at all (an allocation failure at an
    /// absurd size); every recipe renders. Callers treat it the way `BoardBackdrop.decode` is
    /// treated — no image, no banner, nothing written.
    public static func render(recipe: FacetsRecipe, pixelWidth: Int) -> CGImage? {
        let width = max(1, pixelWidth)
        let height = pixelHeight(forWidth: width)
        guard let space = CGColorSpace(name: CGColorSpace.sRGB),
              let context = CGContext(
                  data: nil,
                  width: width,
                  height: height,
                  bitsPerComponent: 8,
                  bytesPerRow: 0,
                  space: space,
                  bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
              )
        else { return nil }

        let mesh = facets(recipe: recipe)

        // The ground, painted in device pixels before the transform goes on: the frame's rounded
        // height and `frameHeight × width` differ by up to half a pixel, and a rect stated in
        // normalized units could leave that sliver unpainted along one edge.
        if let ground = mesh.ground.cgColor(in: space) {
            context.setFillColor(ground)
            context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
        }

        // **SVG's y-down frame, kept.** The mesh is a scatter, so a flip would cost nothing visually
        // — but keeping the axis the gallery drew in means a swatch reviewed there and a file written
        // here are the same picture rather than its mirror, which is the only claim the port makes.
        context.translateBy(x: 0, y: CGFloat(height))
        context.scaleBy(x: CGFloat(width), y: -CGFloat(width))
        context.setLineWidth(strokeWidth)

        for face in mesh.faces {
            guard let color = face.color.cgColor(in: space) else { continue }
            context.setFillColor(color)
            context.setStrokeColor(color)
            context.beginPath()
            context.move(to: face.a)
            context.addLine(to: face.b)
            context.addLine(to: face.c)
            context.closePath()
            context.drawPath(using: .fillStroke)
        }

        return context.makeImage()
    }

    /// The same render, encoded as PNG — the bytes `BoardWriter.writeBoardImage` puts in the board
    /// folder.
    ///
    /// **PNG rather than JPEG**, and not for the usual reason: a flat-shaded triangle mesh is exactly
    /// the content JPEG is worst at (hard edges become ringing, and the anti-seam stroke's whole
    /// point is that there is no gap at those edges), while it is exactly what PNG's filters
    /// compress well.
    public static func pngData(recipe: FacetsRecipe, pixelWidth: Int) -> Data? {
        guard let image = render(recipe: recipe, pixelWidth: pixelWidth) else { return nil }
        let data = NSMutableData()
        guard let destination = CGImageDestinationCreateWithData(
            data, UTType.png.identifier as CFString, 1, nil
        ) else { return nil }
        CGImageDestinationAddImage(destination, image, nil)
        guard CGImageDestinationFinalize(destination) else { return nil }
        return data as Data
    }

    // MARK: The mesh

    /// One triangle, in normalized coordinates, with the colour it is both filled and stroked in.
    struct Face: Sendable, Equatable {
        var a: CGPoint
        var b: CGPoint
        var c: CGPoint
        var color: FacetsColor

        /// The unsigned area of the triangle, in normalized units — what the coverage check adds up.
        var area: Double {
            let abx = Double(b.x - a.x), aby = Double(b.y - a.y)
            let acx = Double(c.x - a.x), acy = Double(c.y - a.y)
            return abs(abx * acy - acx * aby) / 2
        }
    }

    /// A whole composition: the ground and the faces over it, in draw order.
    struct Mesh: Sendable, Equatable {
        var ground: FacetsColor
        var faces: [Face]
    }

    /// **The mesh, and the whole of the random consumption order.**
    ///
    /// Fixed and documented because it is the only thing keeping a seed meaningful across versions:
    /// **every point first** — column-major, x before y within a point, matching the HTML's
    /// `c`-outer/`r`-inner loops — **then four draws per triangle in triangle order** (the weighted
    /// hue pick, the hue jitter, the saturation factor, the lightness offset). Triangulation itself
    /// consumes nothing. Inserting a draw anywhere in that sequence re-rolls every board that ever
    /// stored this seed.
    ///
    /// **Nothing stores one yet**, which is why the grid could be resized under it (the full-bleed
    /// ring, `FacetsRecipe.Density`): a board carries the rendered PNG, never the recipe that made
    /// it, so re-rolling every composition costs exactly nothing today. The moment a seed is written
    /// to disk — a `background.recipe` subkey, a preset library — that stops being true and this
    /// sequence, the density grid and the scatter margins all become format.
    static func facets(recipe: FacetsRecipe) -> Mesh {
        var random = FacetsRandom(seed: recipe.seed)
        let points = scatter(recipe.density, using: &random)
        let indices = triangulate(points)

        let hues = recipe.hues
        let level = recipe.level
        var faces: [Face] = []
        faces.reserveCapacity(indices.count)
        for index in indices {
            faces.append(Face(
                a: points[index.a],
                b: points[index.b],
                c: points[index.c],
                color: color(hues: hues, level: level, using: &random)
            ))
        }
        return Mesh(ground: recipe.primaryColor, faces: faces)
    }

    /// The grid-jittered scatter over the outset region. One point per cell, placed anywhere in the
    /// middle 84% of it — the 0.08…0.92 inset is what stops two points in neighbouring cells from
    /// landing on top of each other and producing a sliver triangle, and its upper end is what the
    /// density's margin is sized against (`FacetsRecipe.Density.scatterMargin`).
    static func scatter(_ density: FacetsRecipe.Density, using random: inout FacetsRandom) -> [CGPoint] {
        let margin = density.scatterMargin
        let x0 = -margin
        let x1 = 1 + margin
        let y0 = -margin
        let y1 = frameHeight + margin
        let columns = density.columns
        let rows = density.rows
        let cellWidth = (x1 - x0) / Double(columns)
        let cellHeight = (y1 - y0) / Double(rows)

        var points: [CGPoint] = []
        points.reserveCapacity(columns * rows)
        for column in 0..<columns {
            for row in 0..<rows {
                // Two draws, x then y — the order the JS array literal evaluates in.
                let x = x0 + (Double(column) + random.uniform(0.08, 0.92)) * cellWidth
                let y = y0 + (Double(row) + random.uniform(0.08, 0.92)) * cellHeight
                points.append(CGPoint(x: x, y: y))
            }
        }
        return points
    }

    /// One triangle's colour: a weighted hue pick, then the three jitters.
    ///
    /// The gallery rounds each channel to one decimal on its way into a CSS string; that is a
    /// serialization artefact of emitting text, not part of the recipe, so nothing rounds here.
    static func color(
        hues: [FacetsRecipe.WeightedHue],
        level: FacetsRecipe.Level,
        using random: inout FacetsRandom
    ) -> FacetsColor {
        let picked = pickHue(hues, using: &random)
        let hue = picked + random.uniform(-3, 3)
        let saturation = level.saturation * random.uniform(0.85, 1.15)
        let lightness = level.lightness
            + random.uniform(-FacetsRecipe.lightnessJitter, FacetsRecipe.lightnessJitter)
        return FacetsColor(hue: hue, saturation: saturation, lightness: lightness)
    }

    /// The weighted pick, off **one** draw: walk the list subtracting weights until the draw falls
    /// inside one. The trailing return is the floating-point safety net for a draw that survives every
    /// subtraction — the weights sum to 1, but they sum to 1 in binary.
    static func pickHue(_ hues: [FacetsRecipe.WeightedHue], using random: inout FacetsRandom) -> Double {
        var x = random.next()
        for entry in hues {
            if x < entry.weight { return entry.degrees }
            x -= entry.weight
        }
        return hues.last?.degrees ?? 0
    }

    // MARK: Triangulation

    /// Three indices into the point array.
    struct IndexedTriangle: Sendable, Equatable {
        var a: Int
        var b: Int
        var c: Int
    }

    /// **Bowyer–Watson**, exactly as the gallery does it: a super-triangle enclosing everything,
    /// points inserted one at a time, the triangles whose circumcircle contains the new point
    /// removed, and the cavity they leave retriangulated against its own boundary — the edges that
    /// were not shared by two of the removed triangles.
    ///
    /// Triangles touching the super-triangle are dropped at the end, which is what leaves a
    /// triangulation of the input points alone.
    ///
    /// ### The one deliberate departure from the gallery
    ///
    /// The super-triangle is the HTML's shape at **100× its size**. Bowyer–Watson only produces a
    /// true triangulation when the scaffold is large enough that no real point's circumcircle can
    /// reach past it; the gallery's is about 11× the point cloud, which is not, and the cost is a
    /// handful of sliver triangles quietly missing near the hull — the count comes out 1–4 short of
    /// the 2n − 2 − h every triangulation must satisfy.
    ///
    /// It is a departure from the *scaffold*, not from the picture: the scaffold is deleted before
    /// anything is drawn, and the two versions were measured against each other over 36 meshes — total
    /// covered area differs by 0.2%, entirely in slivers outside the visible frame. What is bought is
    /// an invariant a test can hold the port to exactly, instead of a tolerance around a defect.
    static func triangulate(_ points: [CGPoint]) -> [IndexedTriangle] {
        let count = points.count
        guard count >= 3 else { return [] }

        var vertices = points
        vertices.append(CGPoint(x: -300_000.0 / 480, y: -300_000.0 / 480))
        vertices.append(CGPoint(x: 350_000.0 / 480, y: -300_000.0 / 480))
        vertices.append(CGPoint(x: 240.0 / 480, y: 360_000.0 / 480))

        struct Working {
            var triangle: IndexedTriangle
            var circle: Circumcircle?
        }

        var working = [Working(
            triangle: IndexedTriangle(a: count, b: count + 1, c: count + 2),
            circle: circumcircle(vertices[count], vertices[count + 1], vertices[count + 2])
        )]

        for index in 0..<count {
            let point = vertices[index]

            var bad: [IndexedTriangle] = []
            var kept: [Working] = []
            kept.reserveCapacity(working.count)
            for entry in working {
                if let circle = entry.circle, circle.contains(point) {
                    bad.append(entry.triangle)
                } else {
                    kept.append(entry)
                }
            }
            working = kept

            // The cavity's boundary: an edge shared by two removed triangles is interior and dies
            // with them; one held by a single triangle is the hole's rim and gets a new face.
            //
            // The rim is walked in the removed triangles' own vertex order — **not** the map's, and
            // not a normalized one. Insertion order here is the order the faces come out in, and the
            // face order is the order the colour draws are consumed in, so a tidier walk would be a
            // different picture from the same seed.
            var edgeCounts: [Edge: Int] = [:]
            for triangle in bad {
                for edge in triangle.orderedEdges { edgeCounts[Edge(edge.from, edge.to), default: 0] += 1 }
            }
            for triangle in bad {
                for edge in triangle.orderedEdges where edgeCounts[Edge(edge.from, edge.to)] == 1 {
                    guard let circle = circumcircle(vertices[edge.from], vertices[edge.to], point) else {
                        continue
                    }
                    working.append(Working(
                        triangle: IndexedTriangle(a: edge.from, b: edge.to, c: index),
                        circle: circle
                    ))
                }
            }
        }

        return working.map(\.triangle).filter { $0.a < count && $0.b < count && $0.c < count }
    }

    /// An undirected edge, keyed the way the HTML keys its `edgeCount` map: low index first, so the
    /// same edge seen from either of its triangles is one key.
    struct Edge: Hashable {
        var low: Int
        var high: Int

        init(_ first: Int, _ second: Int) {
            low = min(first, second)
            high = max(first, second)
        }
    }

    struct Circumcircle {
        var x: Double
        var y: Double
        var radiusSquared: Double

        func contains(_ point: CGPoint) -> Bool {
            let dx = Double(point.x) - x
            let dy = Double(point.y) - y
            return dx * dx + dy * dy < radiusSquared
        }
    }

    /// The circle through three points, or `nil` when they are collinear.
    ///
    /// The guard is `1e-12` **in normalized units**, which is the HTML's `1e-9` at 480 scale carried
    /// across with room to spare: the determinant is quadratic in the coordinates, so the same
    /// degeneracy reads about 2×10⁵ times smaller here. Three jittered grid points are never actually
    /// collinear; this exists so a hand-built or pathological point set degrades to a missing face
    /// instead of an infinity.
    static func circumcircle(_ a: CGPoint, _ b: CGPoint, _ c: CGPoint) -> Circumcircle? {
        let ax = Double(a.x), ay = Double(a.y)
        let bx = Double(b.x), by = Double(b.y)
        let cx = Double(c.x), cy = Double(c.y)

        let d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
        guard abs(d) >= 1e-12 else { return nil }

        let a2 = ax * ax + ay * ay
        let b2 = bx * bx + by * by
        let c2 = cx * cx + cy * cy
        let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d
        let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d
        let dx = ax - ux
        let dy = ay - uy
        return Circumcircle(x: ux, y: uy, radiusSquared: dx * dx + dy * dy)
    }
}

extension FacetsGenerator.IndexedTriangle {
    /// The three edges as **directed** pairs, in the HTML's own `e`/`(e+1)%3` order — see the walk in
    /// `triangulate` for why the direction is kept.
    var orderedEdges: [(from: Int, to: Int)] {
        [(a, b), (b, c), (c, a)]
    }
}

// MARK: - FacetsRandom

/// **mulberry32**, ported bit-for-bit from the gallery's own PRNG.
///
/// Not `SystemRandomNumberGenerator` and not `SeededRandomNumberGenerator`-of-the-week: the seed's
/// entire job is to be a *stable name for a picture*, so the sequence has to be reproducible across
/// machines, OS versions and Swift releases. A 32-bit LCG-ish mixer with wrapping arithmetic is
/// reproducible by definition; the standard library's generators explicitly are not.
///
/// Swift's `&*`/`&+` on `UInt32` are exactly JavaScript's `Math.imul` and its `| 0` truncation, so
/// this produces the same doubles in the same order as the reviewed gallery does.
struct FacetsRandom: Sendable {

    private var state: UInt32

    /// The 64-bit seed folded to the generator's 32-bit state through **xmur3's finalizer** — the
    /// avalanche half of the gallery's string hash. Folding rather than truncating matters: seeds
    /// minted from a counter differ only in their low bits, and a raw truncation would hand
    /// neighbouring seeds neighbouring first draws.
    init(seed: UInt64) {
        var h = UInt32(truncatingIfNeeded: seed ^ (seed >> 32))
        h ^= h >> 16
        h = h &* 2_246_822_507
        h ^= h >> 13
        h = h &* 3_266_489_909
        h ^= h >> 16
        state = h
    }

    /// The next draw in 0..<1.
    mutating func next() -> Double {
        state = state &+ 0x6D2B_79F5
        var t = state
        t = (t ^ (t >> 15)) &* (t | 1)
        t ^= t &+ ((t ^ (t >> 7)) &* (t | 61))
        return Double(t ^ (t >> 14)) / 4_294_967_296
    }

    /// A draw scaled into `lower..<upper`.
    mutating func uniform(_ lower: Double, _ upper: Double) -> Double {
        lower + next() * (upper - lower)
    }
}
