//! Walkable-mask construction. Stamps every obstacle (walls, desks,
//! sofas, plants, decor) into a `WalkableMask` so A* knows where
//! characters can route. The padding constant `OBSTACLE_PAD_PX` adds a
//! clearance band around each obstacle so walkers don't scrape along
//! edges.

use super::{
    anchored_top_left, furniture_def, z_sort_row, Anchor, Furniture, PlantItem, PodDecorItem,
    Point, Size, WallDecorItem, WallSegment, Waypoint, WaypointKind, OBSTACLE_PAD_PX,
    PANTRY_FOOTPRINT_DEPTH, WALL_BAND_TO_TOP_MARGIN,
};
use crate::walkable::WalkableMask;

/// Stamp a `(w, h)` furniture footprint into the mask, positioned by its
/// placement `anchor` (the ONE source for footprint origin — shared with the
/// renderer's sprite origin via `anchored_top_left`, so blocked ground and the
/// painted sprite can't drift). The `pad` clearance band is added on every side.
fn stamp_anchored(mask: &mut WalkableMask, anchor: Anchor, pos: Point, w: u16, h: u16, pad: u16) {
    let tl = anchored_top_left(anchor, pos, w, h);
    mask.mark_blocked(tl.x, tl.y, w, h, pad);
}

/// Stamp ONLY the south (ground-contact) `depth` rows of an ELEVATED furniture
/// whose `sprite`-sized visual overhangs its floor base — a plant canopy over
/// its pot, a booth column over its door, the whiteboard panel over its wheels
/// (invariant #6). The strip's south edge is the sprite's south base
/// (`z_sort_row`, the same row the renderer y-sorts by), so the block hugs the
/// floor: a walker parks DEEP behind the piece and is occluded by the overhang's
/// own y-sort (no synthetic cap). `w` is the GROUND width. Horizontally the
/// strip follows the SPRITE, not the bare footprint: a `TopLeft` anchor places
/// the `sprite.w`-wide visual at `pos`, so the narrower ground strip is
/// CENTERED under it (`pos.x + (sprite.w - w) / 2` — the wall-decor
/// whiteboard's 10px wheel span sits at sprite cols 2-11, not 0-9). `Center`
/// anchors keep their legacy `pos.x - w / 2` (footprint and sprite are both
/// centered on `pos` there, byte-identical to the pre-centering stamp).
fn stamp_south_strip(
    mask: &mut WalkableMask,
    anchor: Anchor,
    pos: Point,
    w: u16,
    sprite: Size,
    depth: u16,
    pad: u16,
) {
    let sprite_h = sprite.h;
    let left = match anchor {
        Anchor::TopLeft => pos.x + sprite.w.saturating_sub(w) / 2,
        Anchor::Center => anchored_top_left(anchor, pos, w, sprite_h).x,
    };
    let south = z_sort_row(anchor, pos, sprite_h);
    let depth = depth.min(sprite_h);
    // Saturating to match the inline pantry stamp's style: `south + 1` can't
    // realistically overflow (sprite_h is small, pos.y a fraction of buf_h) and
    // `south + 1 >= sprite_h >= depth` so the sub can't underflow — but keep the
    // arithmetic robust so a future large-sprite call site near the bottom edge
    // of a huge buffer can't wrap.
    let top = south.saturating_add(1).saturating_sub(depth);
    mask.mark_blocked(left, top, w, depth, pad);
}

/// Stamp a furniture footprint, picking the anchoring by whether the sprite
/// OVERHANGS its ground base. `visual_h > footprint_h` ⇒ the sprite is taller
/// than its floor contact (plant canopy, booth column, TV monitor, whiteboard
/// panel): south-anchor the shallow ground strip to the sprite base so a walker
/// parks DEEP behind it and the sprite's own y-sort occludes them (invariant #6,
/// the back-cap's replacement). `visual_h == footprint_h` ⇒ a flat box (vending,
/// printer, tables, couch): a plain anchored stamp is correct. Only the H axis is
/// modeled — a piece that overhangs SIDEWAYS (`visual.w > footprint.w` while
/// `visual.h == footprint.h`) would fall through to the centered stamp and occlude
/// nothing; none exists today, but stamp such a kind explicitly if added.
fn stamp_overhang_aware(
    mask: &mut WalkableMask,
    anchor: Anchor,
    pos: Point,
    w: u16,
    footprint_h: u16,
    visual: Size,
    pad: u16,
) {
    if visual.h > footprint_h {
        stamp_south_strip(mask, anchor, pos, w, visual, footprint_h, pad);
    } else {
        stamp_anchored(mask, anchor, pos, w, footprint_h, pad);
    }
}

/// Walkable footprint (and render face height) of a horizontal (E-W) interior
/// wall, in px. The renderer derives `WALL_THICK_H_PX` from this so the visible
/// glass face and the blocked ground footprint can never drift apart.
pub const WALL_THICK_H: u16 = 6;
/// Walkable footprint of a vertical (N-S) interior wall — seen edge-on, so 1px
/// (the renderer draws it 3px wide, visual-wider-than-footprint per the
/// top-down ground-projection rule). Single source: mask + the placement test
/// read this rather than re-typing `1`.
pub const WALL_THICK_V: u16 = 1;

#[allow(clippy::too_many_arguments)]
pub(super) fn build_walkable_mask(
    buf_w: u16,
    buf_h: u16,
    top_margin: u16,
    door: Option<Point>,
    home_desks: &[Point],
    meeting_sofas: &[Point],
    meeting_tables: &[Point],
    pantry_table: Option<Point>,
    pantry_chairs: &[Point],
    waypoints: &[Waypoint],
    plants: &[PlantItem],
    floor_lamp: Option<Point>,
    lounge_side_table: Option<Point>,
    wall_decor: &[WallDecorItem],
    pod_decor: &[PodDecorItem],
    room_walls: &[WallSegment],
    pantry_counter_size: Size,
) -> WalkableMask {
    let mut mask = WalkableMask::new_open(buf_w, buf_h);

    // Block the north wall band down to the WALL VISUAL bottom (top_wall_h),
    // not the full top_margin — the rows between are carpet apron under the
    // windows, so blocking them put the walkable boundary ~4px south of the
    // visible wall base. Mask = ground projection (invariant #6).
    mask.mark_blocked(
        0,
        0,
        buf_w,
        top_margin.saturating_sub(WALL_BAND_TO_TOP_MARGIN),
        0,
    );
    if let Some(d) = door {
        let cut_x = d.x.saturating_sub(2);
        let cut_h = top_margin.saturating_add(OBSTACLE_PAD_PX);
        mask.mark_walkable(cut_x, 0, 8, cut_h);
    }

    let baseboard_top = buf_h.saturating_sub(3);
    mask.mark_blocked(0, baseboard_top, buf_w, 3, 0);

    // Interior walls. Stardew-style fake-3D perspective:
    //   • horizontal walls (E-W) show their FACE — WALL_THICK_H px tall so the
    //     wall reads as having real mass/height when viewed from the north
    //     room (clearly thicker than the edge-on vertical).
    //   • vertical walls (N-S) are seen EDGE-ON — WALL_THICK_V px thin footprint
    //     (the renderer draws it 3 px wide; visual-wider-than-footprint per the
    //     top-down ground-projection rule).
    // Wall padding is ASYMMETRIC by orientation — driven by the coarse 4×4
    // router grid (`pathfind::cell_walkable`: a cell is walkable when ≥8 of its
    // 16 px are open), NOT by clearance:
    //   • HORIZONTAL (E-W) walls are WALL_THICK_H=6 px tall. 6 contiguous blocked
    //     px already fill a routing cell, so the wall is impassable with pad=0 —
    //     and you stand FLUSH against its south face, so any pad is pure red bloat
    //     (a 6px wall read as 10px). pad=0.
    //   • VERTICAL (N-S) walls are WALL_THICK_V=1 px edge-on (top-down ground
    //     projection, invariant #6). A 1px-wide blocked strip is INVISIBLE to the
    //     coarse grid — every straddling cell keeps ≥12/16 px walkable, so A*
    //     routes STRAIGHT THROUGH the wall. It needs OBSTACLE_PAD_PX (→5px blocked)
    //     to drive the wall's whole cell-column under the threshold. This is the
    //     original design: DOOR_GAP_V=14 is sized for "≥10px effective gap after
    //     [this] padding" (see compute_room_walls). The 1px FOOTPRINT is unchanged
    //     (characters still stand right next to the 3px visual); the pad is a
    //     routing-only clearance band, not a wider wall.
    for &WallSegment { start, end } in room_walls {
        if start.x == end.x {
            let seg_top = start.y.min(end.y);
            let seg_bot = start.y.max(end.y);
            // Mirror the renderer's stitch_vertical_wall: a segment whose top
            // is at top_margin plugs into the north window band — but the band
            // mask now ends WALL_BAND_TO_TOP_MARGIN px higher (the freed carpet
            // apron). Raise the wall's top to meet it, or a walkable slot opens
            // at the wall's top and A* threads between the rooms there (the wall
            // is DRAWN connecting to the band but the mask wouldn't block it).
            // Regression: vertical_wall_is_impassable_except_through_the_door.
            let seg_top = if seg_top == top_margin {
                top_margin.saturating_sub(WALL_BAND_TO_TOP_MARGIN)
            } else {
                seg_top
            };
            mask.mark_blocked(
                start.x,
                seg_top,
                WALL_THICK_V,
                seg_bot - seg_top + 1,
                OBSTACLE_PAD_PX,
            );
        } else {
            mask.mark_blocked(
                start.x.min(end.x),
                start.y,
                start.x.abs_diff(end.x) + 1,
                WALL_THICK_H,
                0,
            );
        }
    }

    for desk in home_desks {
        // Block ONLY the desk surface — not the 8-px-above seated-character
        // zone. In a top-down 3/4 view a walker passing "behind" a desk row
        // is fine: the seated character paints in Pass 1, the walker also
        // paints in Pass 1 (occasional sprite overlap is acceptable), and
        // the desk paints in Pass 2 on top of both. Routes become much
        // shorter — walkers can cut diagonally between desk rows instead
        // of weaving around each one.
        // Desk footprint comes from the shared FurnitureDef (always Some for
        // the desk); stamped TOP-LEFT at the desk Point (not centred like
        // visited furniture).
        // Stamped TOP-LEFT at the desk Point (not centred like visited
        // furniture); the desk pos IS its NW corner.
        if let Some(Size { w, h }) = super::decor::desk_furniture_def().footprint {
            stamp_anchored(&mut mask, Anchor::TopLeft, *desk, w, h, OBSTACLE_PAD_PX);
        }
    }

    for sofa in meeting_sofas {
        // Sofa BODY footprint from the table (16 ON PURPOSE: 16 + 2·pad = the
        // 20px sprite X footprint, with the pad giving vertical sit clearance —
        // see the furniture_def row). Top-down rule: walk up to its sides.
        if let Some(Size { w, h }) = furniture_def(Furniture::MeetingSofaBody).footprint {
            stamp_anchored(&mut mask, Anchor::Center, *sofa, w, h, OBSTACLE_PAD_PX);
        }
    }

    for t in meeting_tables {
        if let Some(Size { w, h }) = furniture_def(Furniture::MeetingTable).footprint {
            stamp_anchored(&mut mask, Anchor::Center, *t, w, h, OBSTACLE_PAD_PX);
        }
    }

    if let Some(t) = pantry_table {
        if let Some(Size { w, h }) = furniture_def(Furniture::PantryTable).footprint {
            stamp_anchored(&mut mask, Anchor::Center, t, w, h, OBSTACLE_PAD_PX);
        }
    }
    for chair in pantry_chairs {
        // Small stool, stamped CENTERED on its pos like the other centered
        // furniture — was left/top-biased (offset 2), which blocked floor 1px
        // north & west of the 2×2 the painter actually draws.
        if let Some(Size { w, h }) = furniture_def(Furniture::PantryChair).footprint {
            stamp_anchored(&mut mask, Anchor::Center, *chair, w, h, 1);
        }
    }

    for wp in waypoints {
        // Footprint sizes live in `approach::obstacle_footprint` (single source
        // of truth shared with `stand_point`). `None` = meeting slots, which
        // sit/stand on sofa/table furniture already stamped above — no obstacle.
        let Some(Size { w, h }) = super::approach::obstacle_footprint(wp.kind, pantry_counter_size)
        else {
            continue;
        };
        // Pad=1 (not OBSTACLE_PAD_PX=2) — waypoint furniture paints in
        // Pass 1.5 (after characters) so a visitor's body is occluded
        // by the sprite. We don't need extra clearance around the
        // sprite footprint; the render order handles overlap correctly.
        if matches!(wp.kind, WaypointKind::Pantry) {
            // The counter sprite (h px tall) is centered on pos, but only its
            // SOUTH base sits on the floor — the receding cabinet tops +
            // backsplash are elevation that overhangs (invariant #6). Block a
            // shallow PANTRY_FOOTPRINT_DEPTH-tall strip anchored to that base
            // (sprite bottom = pos.y + h/2 - 1) instead of the full height, so
            // the non-walkable area hugs the counter foot. A character routed
            // behind it is occluded by the counter's own y-sorted sprite,
            // couch-style. `stand_point` uses the FULL `visual` so the USER parks
            // clear of the whole counter, not inside the upper sprite.
            let depth = PANTRY_FOOTPRINT_DEPTH.min(h);
            let south = wp.pos.y + h / 2;
            mask.mark_blocked(
                wp.pos.x.saturating_sub(w / 2),
                south.saturating_sub(depth),
                w,
                depth,
                1,
            );
            continue;
        }
        // Booth/standing-desk are elevated (sprite overhangs their base) → the
        // helper south-anchors their shallow strip; vending/printer/couch are
        // flat → plain centered stamp.
        let visual = furniture_def(wp.kind.furniture()).visual;
        stamp_overhang_aware(&mut mask, Anchor::Center, wp.pos, w, h, visual, 1);
    }

    for &PlantItem { kind, pos } in plants {
        // GROUND footprint = a shallow pot strip; the canopy overhangs it, so
        // south-anchor it to the sprite base (the leaves then occlude a walker
        // parked north of the pot via their own y-sort; invariant #6).
        let def = furniture_def(kind.furniture());
        if let Some(Size { w, h }) = def.footprint {
            stamp_overhang_aware(&mut mask, Anchor::Center, pos, w, h, def.visual, 1);
        }
    }

    if let Some(lamp) = floor_lamp {
        // EXCEPTION: the lamp has visual.h > footprint.h yet stamps plain-centered
        // (NOT via `stamp_overhang_aware`). Its 7px footprint is already sized so a
        // CENTERED stamp + pad reaches the disc at the sprite south (see the
        // FloorLamp row in decor.rs); south-anchoring a short strip would lift the
        // block off the disc. The lamp sits in the open lounge, not a corridor, so
        // "walker behind it" occlusion isn't needed.
        if let Some(Size { w, h }) = furniture_def(Furniture::FloorLamp).footprint {
            stamp_anchored(&mut mask, Anchor::Center, lamp, w, h, 1);
        }
    }

    if let Some(t) = lounge_side_table {
        // Small footprint, pad=1: sits in the wide open lounge floor with
        // plenty of clearance.
        if let Some(Size { w, h }) = furniture_def(Furniture::LoungeSideTable).footprint {
            stamp_anchored(&mut mask, Anchor::Center, t, w, h, 1);
        }
    }

    // Wall decor is top-left anchored. Only kinds with a ground footprint in
    // the furniture table are obstacles (the rolling whiteboard + the floor-
    // standing bookshelf / meeting screen); the truly wall-HUNG kinds (bulletin
    // board, exit sign) are flush against the wall (footprint None) and stamp
    // nothing. Each footprint is the SHALLOW floor base of a sprite that
    // overhangs north (whiteboard panel / bookshelf shelves / TV monitor), so
    // the strip is SOUTH-anchored to the sprite base and the overhang occludes
    // a walker behind it (invariant #6).
    for &WallDecorItem { kind, pos } in wall_decor {
        // pad=1 (not OBSTACLE_PAD_PX=2): these elevated boards/cabinets overhang
        // nothing solid, so a 2px clearance band on every side just inflated the
        // blocked rect back to the full sprite width (hiding the footprint
        // shrink). Matches the pod-decor whiteboard's pad.
        if let Some(Size { w, h: depth }) = furniture_def(kind.furniture()).footprint {
            let visual = furniture_def(kind.furniture()).visual;
            stamp_south_strip(&mut mask, Anchor::TopLeft, pos, w, visual, depth, 1);
        }
    }

    // Pod-aisle decor is centred at `pos`. All variants are obstacles.
    // PhoneBooth + StandingDesk are also waypoints — those entries
    // appear above in `waypoints` and double-block the same area;
    // mark_blocked is idempotent. Use pad=1 (not OBSTACLE_PAD_PX=2)
    // because aisles are tight (14×16) and an extra pixel of pad on
    // each side disconnects the routing grid through the aisle.
    for &PodDecorItem { kind, pos } in pod_decor {
        // GROUND footprint (not the sprite size). Every overhanging aisle piece
        // (plant canopy, booth column, TV monitor, whiteboard panel) has a
        // shallow base that `stamp_overhang_aware` south-anchors to the sprite
        // base, so the overhang occludes a walker behind it (invariant #6); flat
        // boxes fall through to a plain centered stamp.
        let def = furniture_def(kind.furniture());
        let Some(Size { w, h }) = def.footprint else {
            continue;
        };
        stamp_overhang_aware(&mut mask, Anchor::Center, pos, w, h, def.visual, 1);
    }

    mask
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn overhang_footprint_south_anchored_leaves_the_overhang_walkable() {
        // A 6-wide piece whose sprite is 12 tall but whose ground base is only 3
        // (a phone booth): `stamp_overhang_aware` must block ONLY the 3-row south
        // strip at the sprite base, leaving the tall overhang north of it walkable
        // — that's where a walker parks deep so the sprite's y-sort occludes them.
        // This is the core invariant the cap-deletion relies on; pin it directly.
        let mut mask = WalkableMask::new_open(40, 40);
        let pos = Point { x: 20, y: 20 };
        stamp_overhang_aware(
            &mut mask,
            Anchor::Center,
            pos,
            6,
            3,
            Size { w: 6, h: 12 },
            0,
        );
        let south = z_sort_row(Anchor::Center, pos, 12); // sprite base row
        for dy in 0..3 {
            assert!(
                !mask.is_walkable(pos.x, south - dy),
                "base row {} must be blocked",
                south - dy
            );
        }
        assert!(
            mask.is_walkable(pos.x, south - 4),
            "overhang region north of the base must stay walkable (walker parks here)"
        );
        assert!(
            mask.is_walkable(pos.x, pos.y.saturating_sub(5)),
            "sprite-top region must stay walkable"
        );
    }

    #[test]
    fn wall_decor_whiteboard_footprint_centers_under_the_wider_sprite() {
        // The wall-decor whiteboard is TopLeft-anchored: the renderer blits its
        // 14px-wide sprite at `pos`, with the wheels at sprite cols 2 and 11.
        // Its 10px ground footprint is the wheel span — so the south strip must
        // be CENTERED under the visual (left = pos.x + 2), not hug the sprite's
        // west edge: left-aligned, the east wheel column stayed walkable while
        // 2px of bare floor west of the board was blocked.
        use crate::layout::WallDecor;
        let pos = Point { x: 40, y: 30 };
        let wall_decor = vec![WallDecorItem {
            kind: WallDecor::Whiteboard,
            pos,
        }];
        let mask = build_walkable_mask(
            120,
            96,
            20,
            None,
            &[],
            &[],
            &[],
            None,
            &[],
            &[],
            &[],
            None,
            None,
            &wall_decor,
            &[],
            &[],
            Size { w: 20, h: 8 },
        );
        let def = furniture_def(Furniture::Whiteboard);
        let sprite_h = def.visual.h; // 11
        let base = pos.y + sprite_h - 1; // TopLeft south row
                                         // Both wheel columns blocked (pad=1 widens beyond them, so probe the
                                         // wheels themselves).
        assert!(
            !mask.is_walkable(pos.x + 2, base),
            "west wheel column must be blocked"
        );
        assert!(
            !mask.is_walkable(pos.x + 11, base),
            "east wheel column must be blocked"
        );
        // Bare floor west of the wheel span (beyond the 1px pad) stays open.
        assert!(
            mask.is_walkable(pos.x, base),
            "bare floor west of the wheels must stay walkable"
        );
        // And the sprite's own east edge column (beyond footprint+pad) too.
        assert!(
            mask.is_walkable(pos.x + 13, base),
            "floor east of the wheels+pad must stay walkable"
        );
    }

    #[test]
    fn flat_box_footprint_is_centered_not_south_anchored() {
        // visual.h == footprint.h ⇒ flat box (vending/printer/table): a centered
        // stamp, NOT a south strip — the block straddles `pos`, so the row NORTH
        // of center is blocked (a south strip would leave it open).
        let mut mask = WalkableMask::new_open(40, 40);
        let pos = Point { x: 20, y: 20 };
        stamp_overhang_aware(&mut mask, Anchor::Center, pos, 4, 6, Size { w: 4, h: 6 }, 0);
        assert!(
            !mask.is_walkable(pos.x, pos.y),
            "centered block: center blocked"
        );
        assert!(
            !mask.is_walkable(pos.x, pos.y - 2),
            "centered block: north-of-center blocked (not a south strip)"
        );
    }
}
