// BoardView (P4.3) — render the seeded match-3 board with real gem sprites.
//
// Modeled on game/chess/board_view.sx: a `View` that lays out an 8×8 grid and
// draws tiles/sprites through RenderContext.add_image / add_image_uv, sampling
// the gem sprite sheet by UV column. The background image fills the whole view;
// the grid is a centered square inside the safe-area inset.
#import "modules/std.sx";
#import "modules/math";
#import "modules/opengl.sx";
#import "modules/stb.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
#import "modules/ui/types.sx";
#import "modules/ui/render.sx";
#import "modules/ui/events.sx";
#import "modules/ui/view.sx";
#import "modules/ui/font.sx";
#import "board.sx";
#import "board_layout.sx";
#import "board_anim.sx";
#import "swipe.sx";

// Fraction of a cell each gem occupies; the remainder is margin so a gem sits
// inside its cell tile rather than touching the tile's edges.
GEM_FILL_FRAC :f32: 0.84;

// Selection overlay: a translucent warm fill plus a bright opaque rim around the
// chosen cell. `add_stroked_rect` draws the rim in its FILL color (the renderer
// ignores the separate stroke color), so SELECT_RIM is passed as the fill.
SELECT_FILL :: Color.{ r = 255, g = 240, b = 120, a = 70 };
SELECT_RIM  :: Color.{ r = 255, g = 228, b = 60,  a = 255 };

// HUD: a translucent card with the score and remaining moves, in the loaded Lato
// font. Placed in the empty band above the centered grid (inside the safe area).
HUD_FONT      :f32: 34.0;
HUD_PAD       :f32: 14.0;
HUD_LINE_GAP  :f32: 6.0;
HUD_TEXT  :: Color.{ r = 255, g = 255, b = 255, a = 255 };
HUD_PANEL :: Color.{ r = 12,  g = 14,  b = 22,  a = 185 };

// UV sub-rect of one gem column, spanning the sheet's full height.
GemUV :: struct {
    uv_min: Point;
    uv_max: Point;
}

// Loads and holds the three board textures (background, cell tile, gem sheet)
// and maps a gem index to its column UV. Modeled on chess's ChessPieces.
BoardAssets :: struct {
    bg_tex: u32;
    cell_tex: u32;
    gems_tex: u32;
    cell_u: f32;
    loaded: bool;

    init :: (self: *BoardAssets) {
        self.bg_tex = 0;
        self.cell_tex = 0;
        self.gems_tex = 0;
        // gems.png is GEM_COUNT columns wide and one row tall, so a gem's UV
        // column IS its gem index (0=red … 5=purple); cell_u is one column wide.
        self.cell_u = 1.0 / cast(f32) GEM_COUNT;
        self.loaded = false;
    }

    load :: (self: *BoardAssets, gpu: ?GPU) {
        self.bg_tex = load_texture("assets/board/background.png", gpu);
        self.cell_tex = load_texture("assets/board/cell.png", gpu);
        self.gems_tex = load_texture("assets/gems/gems.png", gpu);
        self.loaded = self.bg_tex != 0 and self.cell_tex != 0 and self.gems_tex != 0;
    }

    gem_uv :: (self: *BoardAssets, index: s64) -> GemUV {
        u0 : f32 = xx index * self.cell_u;
        GemUV.{
            uv_min = Point.{ x = u0, y = 0.0 },
            uv_max = Point.{ x = u0 + self.cell_u, y = 1.0 }
        }
    }
}

// Decode an RGBA image and upload it as a texture, returning the handle (0 on
// failure). When a GPU backend is bound (iOS Metal) it owns the upload; the
// desktop GL path falls back to a plain GL_TEXTURE_2D.
load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
    w : s32 = 0;
    h : s32 = 0;
    ch : s32 = 0;
    pixels := stbi_load(path, @w, @h, @ch, 4);
    if pixels == null {
        out("WARNING: could not load texture: ");
        out(path);
        out("\n");
        return 0;
    }

    tex : u32 = 0;
    if gpu != null {
        tex = gpu.create_texture(w, h, .rgba8, xx pixels);
    } else {
        glGenTextures(1, @tex);
        glBindTexture(GL_TEXTURE_2D, tex);
        glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
    }

    stbi_image_free(pixels);
    return tex;
}

// Which board cell the player has currently selected, if any. Lives behind a
// pointer (heap-allocated in main) because BoardView is a value rebuilt every
// frame from `build_ui`, so the view itself cannot carry state across frames.
// A tap toggles this highlight; a swipe commits a swap (see DragInput) and
// clears it.
BoardSelection :: struct {
    active: bool;
    cell: Cell;

    init :: (self: *BoardSelection) {
        self.active = false;
        self.cell = Cell.{ col = 0, row = 0 };
    }

    clear :: (self: *BoardSelection) {
        self.active = false;
    }

    // Tapping a cell selects it; tapping the cell already selected clears the
    // selection, so a tap toggles its own cell and moves it to any other.
    toggle :: (self: *BoardSelection, c: Cell) {
        if self.active and self.cell.col == c.col and self.cell.row == c.row {
            self.active = false;
        } else {
            self.active = true;
            self.cell = c;
        }
    }
}

// Tracks an in-progress touch drag between its press and release so a swipe can
// be resolved on lift: the press records the start point, and release maps
// start→end through `swipe_intent` to an adjacent-swap intent. Heap-allocated
// (like BoardSelection) so it survives BoardView's per-frame rebuild between the
// down (touchesBegan → mouse_down) and up (touchesEnded → mouse_up) events.
DragInput :: struct {
    active: bool;
    start: Point;

    init :: (self: *DragInput) {
        self.active = false;
        self.start = Point.{ x = 0.0, y = 0.0 };
    }

    begin :: (self: *DragInput, p: Point) {
        self.active = true;
        self.start = p;
    }

    clear :: (self: *DragInput) {
        self.active = false;
    }
}

BoardView :: struct {
    board: *Board;
    assets: *BoardAssets;
    sel: *BoardSelection;
    drag: *DragInput;
    anim: *BoardAnim;
    safe: EdgeInsets;

    // Where the grid sits + the touch↔cell mapping. Recomputed each render /
    // event from the current frame so the hit-test matches what was drawn.
    layout: BoardLayout;

    compute_layout :: (self: *BoardView, frame: Frame) {
        self.layout.compute(frame, self.safe);
    }

    // Draw gem `gem_index`'s sprite-sheet column into `gf`.
    draw_gem :: (self: *BoardView, ctx: *RenderContext, gf: Frame, gem_index: s64) {
        uv := self.assets.gem_uv(gem_index);
        ctx.add_image_uv(gf, self.assets.gems_tex, uv.uv_min, uv.uv_max);
    }

    // Frame for a gem at a (possibly fractional) board position, inset inside its
    // cell. Fractional col/row is how the swap-slide and fall animations place a
    // gem partway between cells.
    gem_frame :: (self: *BoardView, fcol: f32, frow: f32, inset: f32, dim: f32) -> Frame {
        Frame.make(
            self.layout.origin.x + fcol * self.layout.cell_size + inset,
            self.layout.origin.y + frow * self.layout.cell_size + inset,
            dim,
            dim
        )
    }

    // Frame for a gem shrunk by `scale` about its cell centre — the clear
    // scale-out. At scale 0 the gem is a zero-size frame (gone).
    gem_frame_scaled :: (self: *BoardView, col: s64, row: s64, dim: f32, scale: f32) -> Frame {
        cs := self.layout.cell_size;
        cx := self.layout.origin.x + cast(f32) col * cs + cs * 0.5;
        cy := self.layout.origin.y + cast(f32) row * cs + cs * 0.5;
        d := dim * scale;
        Frame.make(cx - d * 0.5, cy - d * 0.5, d, d)
    }

    // Settled-board gems: one sprite per non-empty cell at its cell frame. Used
    // whenever no move is animating.
    render_gems :: (self: *BoardView, ctx: *RenderContext, inset: f32, dim: f32) {
        for 0..BOARD_ROWS: (row) {
            for 0..BOARD_COLS: (col) {
                g := self.board.at(col, row);
                if g != .empty {
                    gf := self.gem_frame(cast(f32) col, cast(f32) row, inset, dim);
                    self.draw_gem(ctx, gf, cast(s64) g);
                }
            }
        }
    }

    // Play the active slice of the move timeline. Gem motion is clipped to the
    // grid so refilled gems slide in from behind the top edge rather than
    // overlapping the HUD band above the board.
    render_anim :: (self: *BoardView, ctx: *RenderContext, inset: f32, dim: f32) {
        ph := self.anim.phase();
        cs := self.layout.cell_size;
        grid := Frame.make(
            self.layout.origin.x, self.layout.origin.y,
            cs * cast(f32) BOARD_COLS, cs * cast(f32) BOARD_ROWS
        );
        ctx.push_clip(grid);

        mv := @self.anim.move;
        if ph.kind == .swap {
            self.render_swap(ctx, mv, inset, dim, ph.t);
        } else if ph.kind == .clear {
            rd := @mv.rounds.items[ph.round];
            self.render_clear(ctx, rd, inset, dim, ph.t);
        } else if ph.kind == .fall {
            rd := @mv.rounds.items[ph.round];
            self.render_fall(ctx, rd, inset, dim, ph.t);
        } else {
            // Settled tail of the timeline — draw the final (model) board. tick()
            // normally clears `active` before this is reached, so it is the seam
            // safety net rather than a frame the player typically sees.
            for 0..BOARD_CELLS: (i) {
                g := mv.final[i];
                if g != .empty {
                    gf := self.gem_frame(cast(f32) (i % BOARD_COLS), cast(f32) (i / BOARD_COLS), inset, dim);
                    self.draw_gem(ctx, gf, cast(s64) g);
                }
            }
        }

        ctx.pop_clip();
    }

    // Swap segment: the board sits still (pre-swap) except the two swapped gems,
    // which slide between their cells. A legal swap slides fully (a→b, b→a); an
    // illegal one pings out to the neighbour and back, ending where it started.
    render_swap :: (self: *BoardView, ctx: *RenderContext, mv: *AnimMove, inset: f32, dim: f32, t: f32) {
        ai := Board.idx(mv.a.col, mv.a.row);
        bi := Board.idx(mv.b.col, mv.b.row);

        for 0..BOARD_CELLS: (i) {
            if i == ai or i == bi { continue; }
            g := mv.pre[i];
            if g != .empty {
                gf := self.gem_frame(cast(f32) (i % BOARD_COLS), cast(f32) (i / BOARD_COLS), inset, dim);
                self.draw_gem(ctx, gf, cast(s64) g);
            }
        }

        p : f32 = ---;
        if mv.legal {
            p = ease_out_cubic(t);
        } else if t < 0.5 {
            p = ease_out_cubic(t * 2.0);
        } else {
            p = ease_out_cubic((1.0 - t) * 2.0);
        }

        afc := cast(f32) mv.a.col; afr := cast(f32) mv.a.row;
        bfc := cast(f32) mv.b.col; bfr := cast(f32) mv.b.row;

        ga := mv.pre[ai];
        if ga != .empty {
            gf := self.gem_frame(afc + (bfc - afc) * p, afr + (bfr - afr) * p, inset, dim);
            self.draw_gem(ctx, gf, cast(s64) ga);
        }
        gb := mv.pre[bi];
        if gb != .empty {
            gf := self.gem_frame(bfc + (afc - bfc) * p, bfr + (afr - bfr) * p, inset, dim);
            self.draw_gem(ctx, gf, cast(s64) gb);
        }
    }

    // Clear segment: matched gems shrink toward nothing; the rest hold position.
    render_clear :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, inset: f32, dim: f32, t: f32) {
        shrink := 1.0 - ease_in_quad(t);
        if shrink < 0.0 { shrink = 0.0; }
        for 0..BOARD_CELLS: (i) {
            g := rd.before[i];
            if g == .empty { continue; }
            col := i % BOARD_COLS;
            row := i / BOARD_COLS;
            if rd.matched.cells[i] {
                gf := self.gem_frame_scaled(col, row, dim, shrink);
                self.draw_gem(ctx, gf, cast(s64) g);
            } else {
                gf := self.gem_frame(cast(f32) col, cast(f32) row, inset, dim);
                self.draw_gem(ctx, gf, cast(s64) g);
            }
        }
    }

    // Fall segment: every gem of the round's settled board slides from its source
    // row (above the board for refills) down to its destination cell.
    render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, inset: f32, dim: f32, t: f32) {
        te := ease_out_cubic(t);
        for 0..BOARD_CELLS: (i) {
            g := rd.after[i];
            if g == .empty { continue; }
            col := i % BOARD_COLS;
            drow := i / BOARD_COLS;
            src := rd.src[i];
            cur_row := cast(f32) src + (cast(f32) drow - cast(f32) src) * te;
            gf := self.gem_frame(cast(f32) col, cur_row, inset, dim);
            self.draw_gem(ctx, gf, cast(s64) g);
        }
    }
}

impl View for BoardView {
    size_that_fits :: (self: *BoardView, proposal: ProposedSize) -> Size {
        Size.{ width = proposal.width ?? 0.0, height = proposal.height ?? 0.0 }
    }

    layout :: (self: *BoardView, bounds: Frame) {
        self.compute_layout(bounds);
    }

    render :: (self: *BoardView, ctx: *RenderContext, frame: Frame) {
        self.compute_layout(frame);

        // 1. Background image fills the whole view, behind the grid.
        if self.assets.bg_tex != 0 {
            ctx.add_image(frame, self.assets.bg_tex);
        }

        // 2. One cell tile per board cell — the static grid, never animated.
        gem_inset := self.layout.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
        gem_dim := self.layout.cell_size * GEM_FILL_FRAC;
        if self.assets.cell_tex != 0 {
            for 0..BOARD_ROWS: (row) {
                for 0..BOARD_COLS: (col) {
                    ctx.add_image(self.layout.cell_frame(col, row), self.assets.cell_tex);
                }
            }
        }

        // 2b. Gems: while a move is animating, play its swap/clear/fall timeline;
        //     otherwise draw the settled model board. The timeline ends exactly on
        //     the model state, so the seam back to the static path is invisible.
        if self.assets.gems_tex != 0 {
            if self.anim != null and self.anim.active {
                self.render_anim(ctx, gem_inset, gem_dim);
            } else {
                self.render_gems(ctx, gem_inset, gem_dim);
            }
        }

        // 3. Selection overlay on the chosen cell: a translucent fill under a
        //    bright rim, drawn over the whole grid so it reads as a highlight.
        if self.sel != null and self.sel.active {
            cf := self.layout.cell_frame(self.sel.cell.col, self.sel.cell.row);
            ctx.add_rect(cf, SELECT_FILL);
            rim_w := max(2.0, self.layout.cell_size * 0.06);
            ctx.add_stroked_rect(cf, SELECT_RIM, SELECT_RIM, rim_w, self.layout.cell_size * 0.14);
        }

        // 4. HUD card with score + remaining moves, in the band above the grid.
        avail := frame.inset(self.safe);
        render_hud(ctx, self.board, avail);
    }

    // Touch input. A press records the drag start; the release resolves the
    // gesture against the SAME layout it was drawn with. A swipe (start→end maps
    // to an adjacent-swap intent) is fed straight into `commit_swap`: a legal
    // swap applies, cascades, scores and spends a move, an illegal one reverts —
    // either way the next frame re-renders the board + HUD from the model. A
    // sub-threshold / off-board drag carries no intent and falls back to the tap
    // behaviour: toggle the selection on the pressed cell, or clear it off-board.
    handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
        self.compute_layout(frame);
        if event.* == {
            case .mouse_down: (d) {
                // Gate input at gesture START: while a move animation is in
                // flight the board ignores new gestures for the WHOLE in-flight
                // window, so a press begun mid-animation never latches a drag and
                // so can't commit when the animation later ends. The press is
                // still consumed; input resumes once the timeline settles.
                if !accepts_input(self.anim) { return true; }
                self.drag.begin(d.position);
                return true;
            }
            case .mouse_up: (d) {
                if !self.drag.active { return false; }
                start := self.drag.start;
                self.drag.clear();
                if intent := swipe_intent(@self.layout, start, d.position) {
                    mv := plan_and_commit(self.board, intent.a, intent.b);
                    if self.anim != null { self.anim.begin(mv); }
                    self.sel.clear();
                } else {
                    if hit := self.layout.point_to_cell(start) {
                        self.sel.toggle(hit);
                    } else {
                        self.sel.clear();
                    }
                }
                return true;
            }
        }
        false
    }
}

// Draw the HUD card — current score and remaining moves (out of the move limit)
// — centered horizontally in the top of `avail`, the safe-area-inset region the
// grid is centered in. Reads live model state, so it tracks score/moves as the
// game progresses. A translucent panel sits behind the text for legibility over
// the board art.
render_hud :: (ctx: *RenderContext, board: *Board, avail: Frame) {
    score_str := format("SCORE  {}", board.score);
    moves_str := format("MOVES  {}/{}", board.moves_remaining(), board.move_limit);

    score_sz := measure_text(score_str, HUD_FONT);
    moves_sz := measure_text(moves_str, HUD_FONT);
    text_w := max(score_sz.width, moves_sz.width);

    panel_w := text_w + HUD_PAD * 2.0;
    panel_h := score_sz.height + HUD_LINE_GAP + moves_sz.height + HUD_PAD * 2.0;
    panel_x := avail.origin.x + (avail.size.width - panel_w) * 0.5;
    panel_y := avail.origin.y + HUD_PAD;
    ctx.add_rounded_rect(Frame.make(panel_x, panel_y, panel_w, panel_h), HUD_PANEL, 12.0);

    tx := panel_x + HUD_PAD;
    ty := panel_y + HUD_PAD;
    ctx.add_text(Frame.make(tx, ty, score_sz.width, score_sz.height), score_str, HUD_FONT, HUD_TEXT);
    ty += score_sz.height + HUD_LINE_GAP;
    ctx.add_text(Frame.make(tx, ty, moves_sz.width, moves_sz.height), moves_str, HUD_FONT, HUD_TEXT);
}
