// Collapse golden: run gravity over several HAND-CRAFTED boards and snapshot the
// post-collapse board. Unlike the clear/match goldens these boards need not be
// run-free — `collapse` never inspects matches, it only moves gems past holes —
// so each column is painted to exercise a distinct case (holes in the middle, at
// the bottom, a full column of holes, a column with none, a lone gem, an
// alternating stack). Distinct gem letters are stacked vertically so the
// top-to-bottom order is observable in the dump.
//
// For each scene the before/after boards are printed, and two facts are asserted
// independently of the dump: every column ends with its original gems (same
// top-to-bottom order) packed at the BOTTOM and all holes contiguous above, and
// the returned `moved` flag is exact.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";

// Inverse of `gem_char`: map a board character back to its Gem. The hole glyph
// maps to `.empty`, so a board can be hand-written with holes in any position.
char_to_gem :: (c: u8) -> Gem {
    if c == EMPTY_CHAR { return .empty; }
    for 0..GEM_COUNT: (i) {
        if GEM_CHARS[i] == c { return cast(Gem) i; }
    }
    .red
}

// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
load_board :: (rows: []string) -> Board {
    b : Board = ---;
    for 0..BOARD_ROWS: (row) {
        line := rows[row];
        for 0..BOARD_COLS: (col) {
            b.set(col, row, char_to_gem(line[col]));
        }
    }
    b
}

// The collapse invariant, checked column by column against the original board:
// each column's gems (its non-hole cells, in top-to-bottom order) must reappear
// packed contiguously at the BOTTOM in that same order, with every cell above
// them a hole. This single check covers holes-bubble-to-top, gems-settle-to-
// bottom, order-preservation, and the all-holes / no-holes edge columns at once.
check_collapsed :: (orig: *Board, b: *Board) -> bool {
    for 0..BOARD_COLS: (col) {
        gems : [BOARD_ROWS]Gem = ---;
        n := 0;
        for 0..BOARD_ROWS: (row) {
            g := orig.at(col, row);
            if g != .empty { gems[n] = g; n += 1; }
        }
        boundary := BOARD_ROWS - n;            // first row that must hold a gem
        for 0..BOARD_ROWS: (row) {
            if row < boundary {
                if b.at(col, row) != .empty { return false; }
            } else {
                if b.at(col, row) != gems[row - boundary] { return false; }
            }
        }
    }
    true
}

// Collapse one scene, snapshot before/after, and assert the collapse invariant
// plus the exact `moved` flag.
scene :: (name: string, rows: []string, want_moved: bool) {
    b := load_board(rows);
    orig := load_board(rows);              // pristine copy for the invariant check

    moved := collapse(@b);

    print("== {} ==\n", name);
    out("before:\n");
    out(board_dump(@orig));
    out("after:\n");
    out(board_dump(@b));

    t.expect(check_collapsed(@orig, @b), concat(name, ": gems packed bottom, holes top, order preserved"));
    t.expect(moved == want_moved, concat(name, ": moved flag exact"));
}

main :: () -> s32 {
    print("== collapse (gravity) ==\n");

    // Eight independent columns, one case each (top-to-bottom):
    //   col0  holes in the MIDDLE: R O Y G B straddle three holes -> all fall.
    //   col1  holes at the BOTTOM: R O Y sit on top -> fall to the floor.
    //   col2  a FULL column of holes -> stays all holes.
    //   col3  NO holes (eight gems) -> unchanged.
    //   col4  already settled (holes already at the top) -> unchanged.
    //   col5  a LONE gem at the very top -> drops to the floor.
    //   col6  an ALTERNATING gem/hole stack -> gems pack, order preserved.
    //   col7  three gems already resting on the floor -> unchanged.
    scene("varied", .[
        "RR.R.RR.",
        ".O.O....",
        ".Y.Y..O.",
        "O..GR...",
        "Y..BO.Y.",
        "...RY..R",
        "G..OG.GO",
        "B..YB..Y",
    ], true);

    // No holes anywhere: gravity has nothing to do, board is left byte-identical.
    scene("no-holes", .[
        "ROYGBPRO",
        "YGBPROYG",
        "BPROYGBP",
        "ROYGBPRO",
        "YGBPROYG",
        "BPROYGBP",
        "ROYGBPRO",
        "YGBPROYG",
    ], false);

    // Every cell a hole: an empty board collapses to itself, nothing moves.
    scene("all-holes", .[
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
        "........",
    ], false);

    // Already settled: every column has its holes contiguous at the top and its
    // gems contiguous at the bottom (this IS the post-collapse form of "varied").
    // Re-collapsing must move nothing and leave the board unchanged — the
    // idempotency the P2.4 cascade relies on to detect that gravity has stopped.
    scene("settled", .[
        "...R....",
        "...O....",
        "...Y....",
        "R..GR...",
        "O..BO.R.",
        "YR.RY.OR",
        "GO.OG.YO",
        "BY.YBRGY",
    ], false);

    print("ok: collapse over hand-crafted boards\n");
    return 0;
}
