use ratatui::{
    style::{Color, Style},
    text::{Line, Span},
};

// ── Pixel buffer ──────────────────────────────────────────────────────────────

type PixBuf = Vec<Vec<Option<Color>>>;

fn new_buf(pw: usize, ph: usize) -> PixBuf {
    vec![vec![None; pw]; ph]
}

fn px(buf: &PixBuf, x: usize, y: usize) -> Option<Color> {
    buf.get(y).and_then(|r| r.get(x)).copied().flatten()
}

fn lum(c: Option<Color>) -> f32 {
    match c {
        Some(Color::Rgb(r, g, b)) => 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32,
        _ => 0.0,
    }
}

// ── Render mode ───────────────────────────────────────────────────────────────

#[derive(Clone, Copy, PartialEq)]
pub enum RenderMode { Half, Quarter, Braille }

impl RenderMode {
    fn next(self) -> Self {
        match self { Self::Half => Self::Quarter, Self::Quarter => Self::Braille, Self::Braille => Self::Half }
    }
    fn prev(self) -> Self {
        match self { Self::Half => Self::Braille, Self::Quarter => Self::Half, Self::Braille => Self::Quarter }
    }
    // Pixel dimensions for a char_w × char_h terminal area
    fn pw(self, char_w: usize) -> usize {
        match self { Self::Half => char_w, Self::Quarter | Self::Braille => char_w * 2 }
    }
    fn ph(self, char_h: usize) -> usize {
        match self { Self::Half | Self::Quarter => char_h * 2, Self::Braille => char_h * 4 }
    }
}

// ── Half-block renderer (1×2 pixels per char) ─────────────────────────────────

fn render_half(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
    to_lines(char_w, char_h, |col, row| {
        let t = px(buf, col, row * 2);
        let b = px(buf, col, row * 2 + 1);
        match (t, b) {
            (None,    None   ) => (' ', Style::new()),
            (Some(c), None   ) => ('▀', Style::new().fg(c)),
            (None,    Some(c)) => ('▄', Style::new().fg(c)),
            (Some(a), Some(b)) if a == b => ('█', Style::new().fg(a)),
            (Some(a), Some(b)) => ('▀', Style::new().fg(a).bg(b)),
        }
    })
}

// ── Quarter-block renderer (2×2 pixels per char) ──────────────────────────────

// Bit order: [3]=TL [2]=TR [1]=BL [0]=BR
const Q: [char; 16] = [
    ' ', '▗', '▖', '▄', '▝', '▐', '▞', '▟',
    '▘', '▚', '▌', '▙', '▀', '▜', '▛', '█',
];

fn render_quarter(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
    to_lines(char_w, char_h, |col, row| {
        let tl = px(buf, col*2,   row*2);
        let tr = px(buf, col*2+1, row*2);
        let bl = px(buf, col*2,   row*2+1);
        let br = px(buf, col*2+1, row*2+1);
        let pixels = [tl, tr, bl, br];
        let ls = pixels.map(lum);

        // Median split: 2 brightest → fg, 2 darkest → bg
        let mut s = ls;
        s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        let thresh = (s[1] + s[2]) / 2.0;
        let on = ls.map(|l| l > thresh || (l == thresh && l > 0.0));

        let idx = (on[0] as usize)<<3 | (on[1] as usize)<<2 | (on[2] as usize)<<1 | on[3] as usize;
        let ch = Q[idx];

        let (mut fr, mut fg, mut fb, mut fn_) = (0u32, 0, 0, 0);
        let (mut br_, mut bg, mut bb, mut bn) = (0u32, 0, 0, 0);
        for (i, c) in pixels.iter().enumerate() {
            if let Some(Color::Rgb(r, g, b)) = c {
                if on[i] { fr += *r as u32; fg += *g as u32; fb += *b as u32; fn_ += 1; }
                else      { br_ += *r as u32; bg += *g as u32; bb += *b as u32; bn += 1; }
            }
        }
        let style = match (fn_ > 0, bn > 0) {
            (true,  true ) => Style::new()
                .fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
                .bg(Color::Rgb((br_/bn) as u8, (bg/bn)  as u8, (bb/bn)  as u8)),
            (true,  false) => Style::new()
                .fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8)),
            (false, true ) => Style::new()
                .bg(Color::Rgb((br_/bn) as u8, (bg/bn)  as u8, (bb/bn)  as u8)),
            (false, false) => Style::new(),
        };
        (ch, style)
    })
}

// ── Braille renderer (2×4 pixels per char) ────────────────────────────────────
//
// Braille dot layout (Unicode graphical ordering):
//   col0 col1
//    1    4     row 0
//    2    5     row 1
//    3    6     row 2
//    7    8     row 3
// Codepoint = U+2800 + bitmask

fn render_braille(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
    // bit positions for each of the 8 sub-pixels in a 2×4 block
    const BIT: [[u8; 2]; 4] = [
        [0x01, 0x08],  // row 0: dot1, dot4
        [0x02, 0x10],  // row 1: dot2, dot5
        [0x04, 0x20],  // row 2: dot3, dot6
        [0x40, 0x80],  // row 3: dot7, dot8
    ];

    to_lines(char_w, char_h, |col, row| {
        let mut mask: u8 = 0;
        let mut fr = 0u32; let mut fg = 0u32; let mut fb = 0u32; let mut fn_ = 0u32;
        let mut dr = 0u32; let mut dg = 0u32; let mut db = 0u32; // avg of all pixels for bg hint
        let mut dn = 0u32;

        let mut lums = [0f32; 8];
        let mut cols = [None::<Color>; 8];
        let mut k = 0;
        for dy in 0..4usize {
            for dx in 0..2usize {
                let c = px(buf, col*2+dx, row*4+dy);
                lums[k] = lum(c);
                cols[k] = c;
                k += 1;
            }
        }

        // Threshold = median of 8 luminances
        let mut s = lums;
        s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        let thresh = (s[3] + s[4]) / 2.0;

        for dy in 0..4usize {
            for dx in 0..2usize {
                let i = dy * 2 + dx;
                let c = cols[i];
                if let Some(Color::Rgb(r, g, b)) = c {
                    dr += r as u32; dg += g as u32; db += b as u32; dn += 1;
                }
                if lums[i] > thresh || (lums[i] == thresh && lums[i] > 0.0) {
                    mask |= BIT[dy][dx];
                    if let Some(Color::Rgb(r, g, b)) = c {
                        fr += r as u32; fg += g as u32; fb += b as u32; fn_ += 1;
                    }
                }
            }
        }

        let ch = char::from_u32(0x2800 + mask as u32).unwrap_or(' ');

        // Fg = avg of lit pixels; bg = avg of all pixels (darker, fills gaps)
        let style = if fn_ > 0 && dn > 0 {
            let fg_c = Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8);
            let bg_c = Color::Rgb(
                ((dr/dn) as u16).saturating_sub(30) as u8,
                ((dg/dn) as u16).saturating_sub(30) as u8,
                ((db/dn) as u16).saturating_sub(30) as u8,
            );
            Style::new().fg(fg_c).bg(bg_c)
        } else if fn_ > 0 {
            Style::new().fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
        } else {
            Style::new()
        };

        (ch, style)
    })
}

// ── Shared line builder ───────────────────────────────────────────────────────

fn to_lines(
    char_w: usize,
    char_h: usize,
    mut cell: impl FnMut(usize, usize) -> (char, Style),
) -> Vec<Line<'static>> {
    (0..char_h).map(|row| {
        let mut spans: Vec<Span<'static>> = Vec::new();
        let mut run = String::new();
        let mut cur = Style::new();
        for col in 0..char_w {
            let (ch, sty) = cell(col, row);
            if sty == cur { run.push(ch); }
            else {
                if !run.is_empty() { spans.push(Span::styled(run.clone(), cur)); run.clear(); }
                cur = sty;
                run.push(ch);
            }
        }
        if !run.is_empty() { spans.push(Span::styled(run, cur)); }
        Line::from(spans)
    }).collect()
}

// ── Drawing helpers ───────────────────────────────────────────────────────────

fn set_px(buf: &mut PixBuf, x: isize, y: isize, c: Color) {
    if y >= 0 && x >= 0 {
        if let Some(row) = buf.get_mut(y as usize) {
            if let Some(cell) = row.get_mut(x as usize) {
                *cell = Some(c);
            }
        }
    }
}

fn fill_r(buf: &mut PixBuf, x: isize, y: isize, w: usize, h: usize, c: Color) {
    for dy in 0..h as isize {
        for dx in 0..w as isize {
            set_px(buf, x + dx, y + dy, c);
        }
    }
}

// ── Hash helper ───────────────────────────────────────────────────────────────

fn hash(mut x: u64) -> u64 {
    x ^= x >> 30;
    x = x.wrapping_mul(0xbf58476d1ce4e5b9);
    x ^= x >> 27;
    x = x.wrapping_mul(0x94d049bb133111eb);
    x ^ (x >> 31)
}

fn dispatch(mode: RenderMode, buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
    match mode {
        RenderMode::Half    => render_half(buf, char_w, char_h),
        RenderMode::Quarter => render_quarter(buf, char_w, char_h),
        RenderMode::Braille => render_braille(buf, char_w, char_h),
    }
}

// ── Scene element drawing functions ──────────────────────────────────────────

fn draw_mountains_and_buildings(buf: &mut PixBuf, pw: usize, ph: usize) {
    let ground_y = ph * 88 / 100;
    let sky_col = |py: usize| {
        let frac = py as f64 / ground_y as f64;
        Color::Rgb((8.0 + frac * 6.0) as u8, (12.0 + frac * 8.0) as u8, (22.0 + frac * 12.0) as u8)
    };

    // Mountains
    let m_base = ph * 72 / 100;
    for ppx in 0..pw {
        let xf = ppx as f64 / pw as f64;
        let r1 = ((xf * std::f64::consts::PI * 2.8).sin() * 0.5 + 0.5) * ph as f64 * 0.28;
        let r2 = ((xf * std::f64::consts::PI * 1.5 + 1.0).sin() * 0.5 + 0.5) * ph as f64 * 0.14;
        let ridge = (r1 + r2) as usize;
        let top = m_base.saturating_sub(ridge);
        for py in top..m_base.min(ground_y) {
            let d = (py - top) as f64 / ridge.max(1) as f64;
            buf[py][ppx] = Some(Color::Rgb(
                (38.0 + d * 12.0) as u8,
                (45.0 + d * 14.0) as u8,
                (72.0 + d * 20.0) as u8,
            ));
        }
    }

    let win     = Color::Rgb(210, 175, 85);
    let win_dim = Color::Rgb(125, 103, 42);
    let beam    = Color::Rgb(48,  32,  16);   // dark oak
    let mortar  = Color::Rgb(88,  82,  74);   // stone mortar
    let spire_c = (82u8, 77u8, 70u8);   // dark old stone
    let plant   = Color::Rgb(42,  95,  35);
    let pot     = Color::Rgb(140, 72,  32);

    // (x_frac, w_frac, h_frac, style)  0=tower, 1=hall, 2=spire
    let bldgs: &[(f64, f64, f64, u8)] = &[
        (0.00, 0.07, 0.40, 0),
        (0.06, 0.13, 0.26, 1),
        (0.17, 0.06, 0.55, 2),
        (0.22, 0.14, 0.30, 1),
        (0.35, 0.07, 0.46, 0),
        (0.41, 0.16, 0.23, 1),
        (0.57, 0.06, 0.44, 2),
        (0.62, 0.11, 0.32, 1),
        (0.72, 0.07, 0.48, 0),
        (0.78, 0.13, 0.27, 1),
        (0.90, 0.10, 0.42, 0),
    ];

    let b_base = ph * 78 / 100;
    for &(xf, wf, hf, style) in bldgs {
        let bx = (xf * pw as f64) as isize;
        let bw = ((wf * pw as f64) as usize).max(3);
        let bh = ((hf * ph as f64) as usize).max(4);
        let by = b_base.saturating_sub(bh) as isize;

        // Draw walls from building top all the way to ground
        let wall_bottom = ground_y.min(ph);
        match style {
            0 => {
                // Tower — light stone with coursed masonry
                for py in (by as usize)..wall_bottom {
                    for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
                        let v   = hash(ppx as u64 * 3 + py as u64 * 7 + 101);
                        let row = py.wrapping_sub(by as usize);
                        let is_mortar = row % 3 == 0 || ppx % 4 == (if (row / 3) % 2 == 0 { 0 } else { 2 });
                        buf[py][ppx] = Some(if is_mortar {
                            mortar
                        } else {
                            Color::Rgb(
                                158u8.saturating_add((v % 18) as u8),
                                150u8.saturating_add(((v >> 4) % 14) as u8),
                                136u8.saturating_add(((v >> 8) % 11) as u8),
                            )
                        });
                    }
                }
                // Battlements
                let merlon_w = (bw / 4).max(1);
                let crenel_h = (bh / 5).max(1);
                let mut toggle = true;
                let mut cx = bx;
                while cx < bx + bw as isize {
                    let seg_end = (cx + merlon_w as isize).min(bx + bw as isize);
                    if !toggle {
                        for py in (by - crenel_h as isize)..by {
                            for ppx in cx..seg_end {
                                if py >= 0 && ppx >= 0 && (ppx as usize) < pw && (py as usize) < ph {
                                    buf[py as usize][ppx as usize] = Some(sky_col(py as usize));
                                }
                            }
                        }
                    }
                    toggle = !toggle;
                    cx = seg_end;
                }
                if bw >= 4 && bh >= 6 {
                    let wx = bx + bw as isize / 2;
                    let wy = by + bh as isize / 3;
                    set_px(buf, wx, wy,     win);
                    set_px(buf, wx, wy + 1, win_dim);
                }
            }
            2 => {
                // Spire — dark old stone tower with needle
                for py in (by as usize)..wall_bottom {
                    for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
                        let v = hash(ppx as u64 * 5 + py as u64 * 11 + 303);
                        buf[py][ppx] = Some(Color::Rgb(
                            spire_c.0.saturating_add((v % 10) as u8),
                            spire_c.1.saturating_add(((v >> 4) % 8) as u8),
                            spire_c.2.saturating_add(((v >> 8) % 7) as u8),
                        ));
                    }
                }
                let spire_h = (bh * 3 / 4) as isize;
                let half_bw = (bw as isize / 2).max(1);
                let cx = bx + bw as isize / 2;
                for dy in 0..spire_h {
                    let hw = (half_bw as f64 * (spire_h - dy) as f64 / spire_h as f64 * 0.6) as isize;
                    for dx in -hw..=hw {
                        let v = hash((cx + dx).unsigned_abs() as u64 * 7 + (by - spire_h + dy).unsigned_abs() as u64 * 13 + 404);
                        set_px(buf, cx + dx, by - spire_h + dy, Color::Rgb(
                            spire_c.0.saturating_add((v % 8) as u8),
                            spire_c.1.saturating_add(((v >> 4) % 6) as u8),
                            spire_c.2.saturating_add(((v >> 8) % 5) as u8),
                        ));
                    }
                }
                if bw >= 3 && bh >= 8 {
                    set_px(buf, bx + bw as isize / 2, by + bh as isize * 2 / 5, win);
                }
            }
            _ => {
                // Hall — half-timbered: cream plaster + dark oak beams
                // Per-building colour variation seeded from position
                let bseed = hash(bx.unsigned_abs() as u64 * 31 + 999);
                let br_off = (bseed % 30) as u8;
                let bg_off = ((bseed >> 4) % 22) as u8;
                let bb_off = ((bseed >> 8) % 18) as u8;
                for py in (by as usize)..wall_bottom {
                    for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
                        let v = hash(ppx as u64 * 5 + py as u64 * 9 + 202);
                        buf[py][ppx] = Some(Color::Rgb(
                            (175u8.saturating_add(br_off)).saturating_add((v % 12) as u8),
                            (165u8.saturating_add(bg_off)).saturating_add(((v >> 4) % 10) as u8),
                            (148u8.saturating_add(bb_off)).saturating_add(((v >> 8) % 8) as u8),
                        ));
                    }
                }
                // Vertical studs: corners + thirds
                for &dx in &[0isize, bw as isize / 3, bw as isize * 2 / 3, bw as isize - 1] {
                    for py in (by as usize)..wall_bottom {
                        set_px(buf, bx + dx, py as isize, beam);
                    }
                }
                // Horizontal rails: top, thirds
                for &dy in &[0isize, bh as isize / 3, bh as isize * 2 / 3] {
                    for ppx in (bx as usize)..(bx as usize + bw).min(pw) {
                        set_px(buf, ppx as isize, by + dy, beam);
                    }
                }
                // Pitched roof — wide at building top, peak at top
                let roof_h = (bh / 3).max(2) as isize;
                let cx_r = bx + bw as isize / 2;
                for dy in 0..roof_h {
                    let hw = (bw as f64 * 0.5 * (roof_h - dy) as f64 / roof_h as f64) as isize;
                    for dx in -hw..=hw {
                        let v = hash((cx_r + dx).unsigned_abs() as u64 * 3 + (by - dy).unsigned_abs() as u64 * 7 + 505);
                        let edge = dx.abs() == hw;
                        set_px(buf, cx_r + dx, by - dy, if edge {
                            beam
                        } else {
                            Color::Rgb(
                                62u8.saturating_add((v % 12) as u8),
                                45u8.saturating_add(((v >> 4) % 9) as u8),
                                30u8.saturating_add(((v >> 8) % 7) as u8),
                            )
                        });
                    }
                }
                // Ridge beam at peak
                set_px(buf, cx_r, by - roof_h, beam);
                // Windows + plant pots
                if bw >= 5 && bh >= 5 {
                    let wy   = by + bh as isize / 3 + 1;
                    let step = (bw / 3).max(2) as isize;
                    let mut wx = bx + step / 2;
                    let mut pot_toggle = false;
                    while wx < bx + bw as isize - 1 {
                        set_px(buf, wx, wy,     win);
                        set_px(buf, wx, wy + 1, win_dim);
                        if pot_toggle {
                            set_px(buf, wx, wy - 1, plant);
                            set_px(buf, wx, wy + 2, pot);
                        }
                        pot_toggle = !pot_toggle;
                        wx += step;
                    }
                }
            }
        }

        // Door — all building styles, centered at base
        let door_w = (bw / 4).max(2);
        let door_h = (bh / 4).max(3);
        let door_x = bx + bw as isize / 2 - door_w as isize / 2;
        let door_top = ground_y as isize - door_h as isize;
        let door_col = Color::Rgb(14, 10, 6);
        for py in door_top..ground_y as isize {
            let row_from_top = (py - door_top) as usize;
            for dx in 0..door_w as isize {
                // arch: round top corners on first row
                let is_corner = row_from_top == 0 && (dx == 0 || dx == door_w as isize - 1);
                if !is_corner {
                    set_px(buf, door_x + dx, py, door_col);
                }
            }
        }
    }
}

fn trunk_half_w(t: f64, pw: usize) -> isize {
    // Uniform trunk — very slight taper, mostly cylindrical
    ((0.11 + t * 0.04) * pw as f64) as isize
}

fn draw_background_trees(buf: &mut PixBuf, pw: usize, ph: usize) {
    let ground_y = ph * 85 / 100;
    let trunk_col = Color::Rgb(32, 18, 7);

    // (x_frac, trunk_h_frac, canopy_r_frac, palette)
    let trees: &[(f64, f64, f64, usize)] = &[
        (0.07, 0.52, 0.11, 0),
        (0.17, 0.44, 0.09, 1),
        (0.27, 0.62, 0.13, 2),
        (0.73, 0.48, 0.10, 2),
        (0.83, 0.58, 0.12, 1),
        (0.93, 0.40, 0.08, 0),
    ];

    // Three autumn palettes: red, orange-gold, warm amber
    let palettes: &[[Color; 3]] = &[
        [Color::Rgb(180, 48, 8),  Color::Rgb(200, 70, 12),  Color::Rgb(155, 35, 5) ],
        [Color::Rgb(195, 120, 0), Color::Rgb(215, 148, 8),  Color::Rgb(170, 98,  0)],
        [Color::Rgb(162, 72, 15), Color::Rgb(185, 92,  20), Color::Rgb(140, 55, 10)],
    ];

    for &(xf, hf, rf, pal) in trees {
        let tx      = (xf * pw as f64) as isize;
        let trunk_h = (hf * ph as f64) as usize;
        let top_y   = ground_y.saturating_sub(trunk_h) as isize;
        let cr      = ((rf * pw as f64).max(3.0)) as isize;
        let cy      = top_y - cr / 2;
        let cols    = &palettes[pal % palettes.len()];

        // Trunk — width scales with tree size
        let tw = (cr / 3).max(2);
        for py in top_y..ground_y as isize {
            for dx in -tw..=tw {
                set_px(buf, tx + dx, py, trunk_col);
            }
        }

        // Canopy — slightly elliptical blob with density variation
        for dy in -cr..=cr {
            for dx in -cr * 2..=cr * 2 {
                // Ellipse: squash x
                let ex = dx / 2;
                if ex * ex + dy * dy <= cr * cr {
                    let v = hash((tx as u64).wrapping_add(500).wrapping_add(((cy + dy) as u64).wrapping_mul(83)).wrapping_add((dx as u64).wrapping_mul(17)));
                    if v % 8 > 1 {
                        let c = cols[(v % 3) as usize];
                        set_px(buf, tx + dx, cy + dy, c);
                    }
                }
            }
        }
    }
}

fn draw_moss(buf: &mut PixBuf, pw: usize, ph: usize) {
    let ground_y = ph * 85 / 100;
    let cx       = pw as isize / 2;
    let moss_dk  = Color::Rgb(20, 42, 16);
    let moss_mid = Color::Rgb(30, 58, 23);
    let moss_lt  = Color::Rgb(42, 75, 32);

    for py in ground_y..ph {
        let t      = py as f64 / ph as f64;
        let hw     = trunk_half_w(t, pw) as f64;
        for ppx in 0..pw {
            let dist   = (ppx as isize - cx).abs() as f64;
            let beyond = dist - hw;
            if beyond > 0.0 && beyond < pw as f64 * 0.28 {
                let proximity = 1.0 - (beyond / (pw as f64 * 0.28)).min(1.0);
                let v = hash((ppx as u64 * 7).wrapping_add(py as u64 * 11).wrapping_add(888));
                if ((v % 10) as f64) < 2.5 + proximity * 5.5 {
                    buf[py][ppx] = Some(match v % 3 {
                        0 => moss_dk,
                        1 => moss_mid,
                        _ => moss_lt,
                    });
                }
            }
        }
    }
}

fn draw_maple_tree(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let cx = pw as isize / 2;

    let very_dark = Color::Rgb(25, 13, 5);
    let dark      = Color::Rgb(42, 24, 9);
    let mid       = Color::Rgb(58, 34, 14);
    let light     = Color::Rgb(74, 46, 20);

    for py in 0..ph {
        let t     = py as f64 / ph as f64;
        let hw    = trunk_half_w(t, pw);
        let lj    = (hash(py as u64 * 7 + 13) % 3) as isize - 1;
        let rj    = (hash(py as u64 * 11 + 29) % 3) as isize - 1;
        let left  = cx - hw + lj;
        let right = cx + hw + rj;
        for ppx in left..right {
            let edge_d = (ppx - left).min(right - ppx);
            let stripe = hash((ppx as u64).wrapping_mul(3).wrapping_add(py as u64 / 4 * 7) + 42) % 10;
            let c = if edge_d <= 1 { very_dark }
                    else if stripe == 0 { very_dark }
                    else if stripe <= 3 { dark }
                    else if stripe <= 6 { mid }
                    else { light };
            set_px(buf, ppx, py as isize, c);
        }
    }

    // Knots
    for k in 0..3usize {
        let ky = (ph * (k + 1) / 4) as isize;
        let kx = cx + [-4isize, 5, -3][k];
        fill_r(buf, kx - 2, ky - 1, 5, 3, very_dark);
        set_px(buf, kx, ky, Color::Rgb(15, 8, 3));
    }

    // Shimenawa
    let rope_y   = (ph * 30 / 100) as isize;
    let rope_hw  = trunk_half_w(0.30, pw) + 1;
    let rope_l   = cx - rope_hw;
    let rope_r   = cx + rope_hw;
    let rope_col = Color::Rgb(185, 155, 80);
    let rope_dk  = Color::Rgb(140, 115, 55);
    for ppx in rope_l..rope_r {
        let alt = (ppx % 3 == 0) as isize;
        set_px(buf, ppx, rope_y + alt,     rope_col);
        set_px(buf, ppx, rope_y + 2 + alt, rope_dk);
    }

    // Shide
    let shide_col = Color::Rgb(235, 230, 218);
    let shide_dk  = Color::Rgb(195, 190, 178);
    let shide_h   = (ph / 10).max(5);
    for i in 0..7usize {
        let sx     = rope_l + (rope_r - rope_l) * i as isize / 6;
        let sway   = ((tick as f64 * 0.015 + i as f64 * 1.1).sin() * 1.2) as isize;
        let base_x = sx + sway;
        for dy in 0..shide_h as isize {
            let ox = if (dy * 3 / shide_h as isize) % 2 == 0 { 0isize } else { 1 };
            let c  = if dy % 2 == 0 { shide_col } else { shide_dk };
            set_px(buf, base_x + ox,     rope_y + 4 + dy, c);
            set_px(buf, base_x + ox + 1, rope_y + 4 + dy, c);
        }
    }
}

fn draw_boat(buf: &mut PixBuf, pw: usize, ph: usize, t: f64) {
    let horizon    = ph * 3 / 10;
    let bcx        = pw / 3;
    // Boat rides waves but clamped well below horizon into the water body
    let w1         = (bcx as f64 * 0.35 + t).sin() * 2.5;
    let w2         = (bcx as f64 * 0.18 - t * 0.65).sin() * 1.5;
    let water_surf = horizon as f64 + w1 + w2;
    let water_y    = (water_surf + ph as f64 * 0.15).max(horizon as f64 + ph as f64 * 0.12);
    // Slope for slight tilt
    let w1r        = ((bcx + 4) as f64 * 0.35 + t).sin() * 2.5;
    let w2r        = ((bcx + 4) as f64 * 0.18 - t * 0.65).sin() * 1.5;
    let slope      = ((horizon as f64 + w1r + w2r) - water_surf) / 4.0;

    let half_deck  = (pw / 12).max(3) as isize;
    let hull_h     = (ph / 12).max(2) as isize;
    let hull_dark  = Color::Rgb(55, 35, 16);
    let hull_mid   = Color::Rgb(78, 52, 24);
    let deck_col   = Color::Rgb(105, 72, 36);
    let mast_col   = Color::Rgb(70, 46, 20);
    let sail_col   = Color::Rgb(218, 210, 192);

    // Hull rows: trapezoid, wide at deck, narrow at keel
    let half_keel  = half_deck / 2;
    for row in 0..hull_h {
        let frac     = row as f64 / hull_h as f64;
        let hw       = (half_deck as f64 - frac * (half_deck - half_keel) as f64) as isize;
        let tilt_off = (slope * (row as f64 - hull_h as f64 / 2.0)) as isize;
        let deck_y   = water_y as isize - hull_h + row;
        for dx in -hw..=hw {
            let c = if row == 0 { deck_col }
                    else if dx == -hw || dx == hw { hull_dark }
                    else { hull_mid };
            set_px(buf, bcx as isize + dx + tilt_off, deck_y, c);
        }
    }

    // Mast
    let mast_base = water_y as isize - hull_h;
    let mast_h    = hull_h * 3;
    for dy in 0..mast_h { set_px(buf, bcx as isize, mast_base - dy, mast_col); }

    // Triangular sail
    let sail_h = mast_h * 2 / 3;
    for dy in 0..sail_h {
        let sail_w = (half_deck as f64 * (sail_h - dy) as f64 / sail_h as f64) as isize;
        for dx in 1..sail_w { set_px(buf, bcx as isize + dx, mast_base - dy, sail_col); }
    }
}

fn draw_fireplace(buf: &mut PixBuf, pw: usize, ph: usize) {
    let side_w   = (pw * 19 / 100).max(4);
    let mantel_h = (ph * 11 / 100).max(2);
    let hearth_y = ph.saturating_sub((ph * 9 / 100).max(2));
    let fire_x0  = side_w;
    let fire_x1  = pw.saturating_sub(side_w);

    let mortar   = Color::Rgb(52, 47, 42);

    let brick_h = (ph / 18).max(2);
    let brick_w = (side_w / 3).max(3);

    // Brick columns
    for side in 0..2usize {
        let xs = if side == 0 { 0 } else { pw.saturating_sub(side_w) };
        for py in mantel_h..hearth_y {
            for ppx in xs..(xs + side_w).min(pw) {
                let row  = (py - mantel_h) / brick_h;
                let off  = if row % 2 == 0 { 0 } else { brick_w / 2 };
                let col_b = (ppx - xs + off) % brick_w;
                let row_b = (py - mantel_h) % brick_h;
                let v = hash(((row * 13 + (ppx - xs + off) / brick_w) as u64).wrapping_add(77));
                let c = if row_b == 0 || col_b == 0 {
                    mortar
                } else if row_b == 1 {
                    // shadow just below mortar joint
                    Color::Rgb(
                        90u8.saturating_add((v % 15) as u8),
                        45u8.saturating_add(((v >> 4) % 10) as u8),
                        30u8.saturating_add(((v >> 8) % 8) as u8),
                    )
                } else {
                    Color::Rgb(
                        135u8.saturating_add((v % 25) as u8),
                        72u8.saturating_add(((v >> 4) % 18) as u8),
                        52u8.saturating_add(((v >> 8) % 14) as u8),
                    )
                };
                if py < buf.len() && ppx < buf[0].len() { buf[py][ppx] = Some(c); }
            }
        }
    }

    // Mantelpiece — warm oak wood grain
    for py in 0..mantel_h {
        for ppx in 0..pw {
            if py >= buf.len() || ppx >= buf[0].len() { continue; }
            let grain = hash((ppx as u64 / 2).wrapping_add((py as u64 * 3).wrapping_mul(17)));
            let grain_line = (py * pw + ppx / 3) % 7 == 0;
            let (r, g, b) = if py == mantel_h - 1 {
                (52u8, 31u8, 12u8)  // dark underside shadow
            } else if grain_line {
                (72u8.saturating_add((grain % 10) as u8), 43u8.saturating_add((grain % 8) as u8), 16u8.saturating_add((grain % 5) as u8))
            } else {
                (88u8.saturating_add((grain % 18) as u8), 54u8.saturating_add((grain % 13) as u8), 22u8.saturating_add((grain % 9) as u8))
            };
            buf[py][ppx] = Some(Color::Rgb(r, g, b));
        }
    }

    // Hearth floor — stone flags outside, carpet in centre
    let carpet_x0 = fire_x0.saturating_sub(fire_x0 / 3);
    let carpet_x1 = (fire_x1 + (pw - fire_x1) / 3).min(pw);
    for py in hearth_y..ph {
        for ppx in 0..pw {
            if py >= buf.len() || ppx >= buf[0].len() { continue; }
            let on_carpet = ppx >= carpet_x0 && ppx < carpet_x1;
            if on_carpet {
                let cx = ppx - carpet_x0;
                let cy = py - hearth_y;
                let cw = carpet_x1 - carpet_x0;
                let ch = ph - hearth_y;
                let border = cx == 0 || cx == cw.saturating_sub(1) || cy == 0 || cy == ch.saturating_sub(1);
                let inner_border = cx == 2 || cx == cw.saturating_sub(3) || cy == 2 || cy == ch.saturating_sub(3);
                let v = hash(ppx as u64 * 7 + py as u64 * 13 + 8888);
                let noise = (v % 12) as u8;
                buf[py][ppx] = Some(if border || inner_border {
                    Color::Rgb(130u8.saturating_add(noise / 2), 40u8.saturating_add(noise / 4), 18u8.saturating_add(noise / 4))
                } else {
                    Color::Rgb(105u8.saturating_add(noise), 28u8.saturating_add(noise / 3), 18u8.saturating_add(noise / 3))
                });
            } else {
                let v = hash(((ppx / 5) as u64).wrapping_add((py * 53) as u64));
                buf[py][ppx] = Some(Color::Rgb(
                    78u8.saturating_add((v % 18) as u8),
                    76u8.saturating_add(((v >> 4) % 14) as u8),
                    72u8.saturating_add(((v >> 8) % 11) as u8),
                ));
            }
        }
    }

    // Logs — two overlapping logs sitting at the base of the fire
    let log_h  = (ph * 5 / 100).max(2);
    let log_y0 = hearth_y.saturating_sub(log_h + 1);
    let fire_w = fire_x1 - fire_x0;
    let log_hw = fire_w / 2;
    let logs: &[(isize, u8, u8, u8)] = &[
        (fire_x0 as isize + fire_w as isize / 3,     58, 32, 12),
        (fire_x0 as isize + fire_w as isize * 2 / 3, 48, 26, 10),
    ];
    for &(cx, lr, lg, lb) in logs {
        for dy in 0..log_h {
            let py = log_y0 + dy;
            if py >= ph { continue; }
            let ry = dy as f64 / log_h as f64;
            let half_w = (log_hw as f64 * (1.0 - (ry * 2.0 - 1.0).powi(2)).sqrt()) as isize;
            for dx in -half_w..=half_w {
                let px = cx + dx;
                if px < fire_x0 as isize || px as usize >= fire_x1 { continue; }
                let v = hash(px as u64 * 5 + py as u64 * 11 + 3030);
                let bark = (v % 3 == 0) as u8 * 12;
                let ember = if dy == 0 { 30u8 } else { 0u8 };
                buf[py][px as usize] = Some(Color::Rgb(
                    lr.saturating_add(bark).saturating_add(ember),
                    lg.saturating_add(bark / 2),
                    lb.saturating_add(bark / 3),
                ));
            }
        }
    }

    // Iron grate — vertical bars across lower portion of fire opening
    let grate_h  = (ph * 10 / 100).max(3);
    let grate_y0 = hearth_y.saturating_sub(grate_h);
    let bar_w    = 1usize;
    let gap_w    = (fire_x1 - fire_x0) / 10;
    let gap_w    = gap_w.max(2);
    let iron_hi  = Color::Rgb(48, 45, 42);
    let iron_dk  = Color::Rgb(22, 20, 18);
    // horizontal rail at top and bottom of grate
    for ppx in fire_x0..fire_x1 {
        if grate_y0 < buf.len() { buf[grate_y0][ppx] = Some(iron_hi); }
        if hearth_y < buf.len() && hearth_y < ph { buf[hearth_y.min(ph-1)][ppx] = Some(iron_dk); }
    }
    // vertical bars
    let mut bx = fire_x0 + gap_w / 2;
    while bx + bar_w <= fire_x1 {
        for py in grate_y0..hearth_y {
            if py >= buf.len() { continue; }
            for dx in 0..bar_w {
                let px = bx + dx;
                if px < buf[0].len() {
                    buf[py][px] = Some(if dx == 0 { iron_hi } else { iron_dk });
                }
            }
        }
        bx += gap_w;
    }
}

// ── Landscape fill functions ──────────────────────────────────────────────────

fn fill_waves(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let t = tick as f64 * 0.12;
    let horizon = ph * 3 / 10;
    for py in 0..ph {
        for ppx in 0..pw {
            buf[py][ppx] = Some(if py < horizon {
                let frac = py as f64 / horizon as f64;
                Color::Rgb((8.0 + frac*18.0) as u8, (18.0 + frac*38.0) as u8, (55.0 + frac*75.0) as u8)
            } else {
                let wy  = py - horizon;
                let wh  = ph - horizon;
                let wyf = wy as f64;
                let whf = wh as f64;
                let depth = wyf / whf;

                // Wave trains — surface and three sub-surface layers
                let w1 = (ppx as f64 * 0.35 + t).sin() * 2.5;
                let w2 = (ppx as f64 * 0.18 - t * 0.65).sin() * 1.5;
                let surf = (wyf - (w1 + w2)).abs();

                let w3   = (ppx as f64 * 0.52 + t * 1.2).sin() * 1.8;
                let w4   = (ppx as f64 * 0.27 - t * 0.85).sin() * 1.3;
                let d1   = whf * 0.11;
                let surf2 = (wyf - d1 - (w3 + w4)).abs();

                let w5   = (ppx as f64 * 0.66 - t * 1.45).sin() * 1.4;
                let w6   = (ppx as f64 * 0.15 + t * 0.42).sin() * 1.9;
                let d2   = whf * 0.23;
                let surf3 = (wyf - d2 - (w5 + w6 * 0.6)).abs();

                let w7   = (ppx as f64 * 0.80 + t * 1.75).sin() * 1.1;
                let w8   = (ppx as f64 * 0.38 - t * 0.55).sin() * 1.6;
                let d3   = whf * 0.38;
                let surf4 = (wyf - d3 - (w7 + w8 * 0.7)).abs();

                // Cross-chop — short diagonal ripples across the body
                let chop1 = (ppx as f64 * 0.9 + wyf * 0.4 + t * 2.1).sin();
                let chop2 = (ppx as f64 * 0.7 - wyf * 0.35 - t * 1.8).sin();
                let chop  = (chop1 * chop2).abs();

                // Subtle specular glints
                let refl  = (ppx as f64 * 0.13 - t * 0.28).sin()
                           * (ppx as f64 * 0.08 + t * 0.19).sin();

                let base_g = (90.0 - depth * 50.0).max(0.0);
                let base_b = (200.0 - depth * 80.0).max(0.0);

                if surf < 1.3 {
                    Color::Rgb(205, 232, 255)
                } else if surf < 2.6 {
                    Color::Rgb(125, 188, 232)
                } else if surf2 < 1.0 {
                    Color::Rgb(80, 158, 218)
                } else if surf2 < 2.0 {
                    Color::Rgb(30, 122, 200)
                } else if surf3 < 0.9 {
                    Color::Rgb(18, 108, 185)
                } else if surf3 < 1.8 {
                    Color::Rgb(8, 95, 172)
                } else if surf4 < 0.85 && depth < 0.72 {
                    Color::Rgb(12, 102, 168)
                } else if chop > 0.82 && depth < 0.55 {
                    // cross-chop glitter
                    Color::Rgb((12.0 + chop * 28.0) as u8, (base_g + chop * 22.0).min(255.0) as u8, (base_b + chop * 14.0).min(255.0) as u8)
                } else if refl > 0.68 && depth < 0.42 {
                    // specular glint near surface
                    Color::Rgb((18.0 + refl * 45.0) as u8, (base_g + refl * 32.0).min(255.0) as u8, (base_b + refl * 18.0).min(255.0) as u8)
                } else {
                    Color::Rgb(0, base_g as u8, base_b as u8)
                }
            });
        }
    }
    let mx = pw * 4 / 5;
    let my = ph / 8;
    let mr = (pw.min(ph) / 10).max(2) as f64;
    for py in 0..ph {
        for ppx in 0..pw {
            let dx = ppx as f64 - mx as f64;
            let dy = (py as f64 - my as f64) * 1.6;
            if dx*dx + dy*dy < mr*mr { buf[py][ppx] = Some(Color::Rgb(255, 240, 180)); }
        }
    }
    draw_boat(buf, pw, ph, t);
}

fn fill_rain(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let ground_y = ph * 88 / 100;

    // Sky
    for py in 0..ground_y {
        let frac = py as f64 / ground_y as f64;
        for ppx in 0..pw {
            buf[py][ppx] = Some(Color::Rgb((8.0 + frac*6.0) as u8, (12.0 + frac*8.0) as u8, (22.0 + frac*12.0) as u8));
        }
    }

    // Cobblestone ground
    let cobble_h = (ph / 22).max(2);
    let cobble_w = (pw / 14).max(3);
    for py in ground_y..ph {
        for ppx in 0..pw {
            let row     = (py - ground_y) / cobble_h;
            let offset  = if row % 2 == 0 { 0 } else { cobble_w / 2 };
            let col_b   = (ppx + offset) % cobble_w;
            let row_b   = (py - ground_y) % cobble_h;
            buf[py][ppx] = Some(if row_b == 0 || col_b == 0 {
                Color::Rgb(14, 16, 14) // mortar
            } else {
                let v = hash(((row * 89 + (ppx + offset) / cobble_w) as u64).wrapping_add(301));
                Color::Rgb(
                    42u8.saturating_add((v % 22) as u8),
                    46u8.saturating_add(((v >> 4) % 20) as u8),
                    40u8.saturating_add(((v >> 8) % 16) as u8),
                )
            });
        }
    }

    draw_mountains_and_buildings(buf, pw, ph);

    let speed = 3usize;
    let spacing = 12usize;
    for ppx in 0..pw {
        let col_offset = (hash(ppx as u64 + 7) % spacing as u64) as usize;
        let streak = 3 + (hash(ppx as u64 + 99) % 4) as usize;
        let base = (tick as usize * speed + col_offset * ph / spacing) % ph;
        for s in 0..streak {
            let py = (base + s) % ph;
            if py < ground_y {
                let alpha = 1.0 - s as f64 / streak as f64;
                buf[py][ppx] = Some(Color::Rgb((80.0*alpha) as u8, (130.0*alpha) as u8, (255.0*alpha) as u8));
            }
        }
    }

    // Puddle ripples on cobblestones
    for py in ground_y..ph {
        for ppx in 0..pw {
            let ripple = (ppx as f64 * 0.4 + tick as f64 * 0.15).sin();
            if ripple > 0.7 { buf[py][ppx] = Some(Color::Rgb(28, 52, 75)); }
        }
    }
}

fn fill_leaves(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let ground_y = ph * 85 / 100;
    for py in 0..ph {
        for ppx in 0..pw {
            buf[py][ppx] = Some(if py < ground_y {
                let f = py as f64 / ground_y as f64;
                Color::Rgb((35.0 + f*12.0) as u8, (28.0 + f*10.0) as u8, (20.0 + f*6.0) as u8)
            } else {
                let f = (py - ground_y) as f64 / (ph - ground_y) as f64;
                Color::Rgb((45.0 + f*15.0) as u8, (30.0 + f*10.0) as u8, (15.0 + f*5.0) as u8)
            });
        }
    }
    draw_background_trees(buf, pw, ph);
    draw_moss(buf, pw, ph);
    draw_maple_tree(buf, pw, ph, tick);

    let leaf_colors = [
        Color::Rgb(210, 65, 10), Color::Rgb(195, 130, 0),
        Color::Rgb(170, 75, 20), Color::Rgb(145, 95, 30), Color::Rgb(220, 160, 0),
    ];
    // Falling leaves — full height, no ground_y cap
    let n_leaves = (pw * ph / 40).max(12).min(80);
    for i in 0..n_leaves {
        let h1 = hash(i as u64 + 1);
        let h2 = hash(i as u64 + 1000);
        let x_base = (h1 % pw as u64) as usize;
        let y_start = (h2 % ph as u64) as usize;
        let speed = 1 + (h1 >> 20) % 2;
        let sway_a = 3.0 + (h2 >> 10 & 0xf) as f64 * 0.3;
        let sway_f = 0.04 + (h1 >> 15 & 0x7) as f64 * 0.005;
        let phase_s = (h2 >> 8 & 0x3f) as f64;
        let x = (x_base as f64 + (tick as f64 * sway_f + phase_s).sin() * sway_a) as isize;
        let y = ((y_start + tick as usize * speed as usize) % ph) as isize;
        let col = leaf_colors[(h1 as usize) % leaf_colors.len()];
        for dy in 0..2isize {
            for dx in 0..2isize {
                set_px(buf, x + dx, y + dy, col);
            }
        }
    }

    // Scattered leaves on floor — static positions, slightly faded
    let n_floor = (pw * (ph - ground_y) / 8).max(6).min(40);
    for i in 0..n_floor {
        let h = hash(i as u64 * 3 + 7777);
        let fx = (h % pw as u64) as isize;
        let fy = (ground_y + (h >> 10) as usize % (ph - ground_y)) as isize;
        let col = leaf_colors[(h as usize) % leaf_colors.len()];
        // Single pixel leaves on floor — no alpha fade needed, moss/ground visible around them
        set_px(buf, fx, fy, col);
        if (h >> 20) % 3 != 0 { set_px(buf, fx + 1, fy, col); }
    }
}

fn draw_ufo(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let period  = 380u64;
    let visible = 100u64;
    let phase   = tick % period;
    if phase >= visible { return; }

    let crossing  = tick / period;
    let progress  = phase as f64 / visible as f64;
    // alternate direction each crossing
    let cx = if crossing % 2 == 0 {
        (progress * (pw as f64 + 24.0)) as isize - 12
    } else {
        (pw as f64 + 12.0 - progress * (pw as f64 + 24.0)) as isize
    };
    // vary y position between crossings
    let base_y  = (ph as f64 * (0.30 + (hash(crossing * 31 + 7) % 20) as f64 * 0.01)) as isize;
    let bob     = ((tick as f64 * 0.18).sin() * 2.5) as isize;
    let cy      = base_y + bob;

    let body_hi = Color::Rgb(168, 170, 182);
    let body_dk = Color::Rgb(108, 110, 124);
    let dome_hi = Color::Rgb(85, 195, 230);
    let dome_dk = Color::Rgb(48, 142, 178);
    let rim_col = Color::Rgb(200, 200, 210);

    // Saucer body — wide ellipse 2px tall
    for dx in -5isize..=5 {
        let inner = dx.abs() <= 2;
        set_px(buf, cx + dx, cy,     if inner { body_hi } else { body_dk });
        set_px(buf, cx + dx, cy + 1, body_dk);
    }
    // Rim highlight
    set_px(buf, cx - 5, cy, rim_col);
    set_px(buf, cx + 5, cy, rim_col);

    // Dome
    for dx in -2isize..=2 { set_px(buf, cx + dx, cy - 1, dome_dk); }
    for dx in -1isize..=1 { set_px(buf, cx + dx, cy - 2, dome_hi); }
    set_px(buf, cx, cy - 3, dome_dk);

    // Dim pulsing lights on underside
    let pulse = (tick as f64 * 0.12).sin() * 0.5 + 0.5;
    let v = (140.0 + pulse * 50.0) as u8;
    for lx in [-3isize, 0, 3] {
        set_px(buf, cx + lx, cy + 2, Color::Rgb(v, v, v));
    }

    // Tractor beam — fades in at start of crossing
    if phase < 35 {
        let alpha = 1.0 - phase as f64 / 35.0;
        let beam_h = (ph as f64 * 0.18 * alpha) as isize;
        for dy in 3..3 + beam_h {
            let spread = (dy / 5).min(3);
            for dx in -spread..=spread {
                let v = hash((cx + dx).unsigned_abs() as u64 * 7 + (cy + dy).unsigned_abs() as u64 * 13 + tick);
                if v % 3 != 0 {
                    set_px(buf, cx + dx, cy + dy, Color::Rgb(
                        (180.0 * alpha) as u8,
                        (210.0 * alpha) as u8,
                        (255.0 * alpha) as u8,
                    ));
                }
            }
        }
    }
}

fn fill_stars(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    for py in 0..ph { for ppx in 0..pw { buf[py][ppx] = Some(Color::Rgb(0, 0, 8)); } }
    let layers: &[(usize, u8, u8, bool)] = &[
        (1, 90,  110, false),
        (2, 170, 190, false),
        (4, 240, 255, true),
    ];
    let n = (pw * ph / 80).max(8);
    for (depth, (speed, dim, bright, trail)) in layers.iter().enumerate() {
        for i in 0..n {
            let seed = hash(i as u64 * 3 + depth as u64 * 997);
            let y  = (seed % ph as u64) as usize;
            let x0 = (seed >> 10) as usize % pw;
            let x  = (x0 + pw - (tick as usize * speed / 10) % pw) % pw;
            let c  = Color::Rgb(*bright, *bright, *bright);
            buf[y][x] = Some(c);
            if *trail {
                for t in 1..4usize {
                    let tx = (x + t) % pw;
                    let fade = dim.saturating_sub(t as u8 * 25);
                    buf[y][tx] = Some(Color::Rgb(fade, fade, fade + 15));
                }
            }
        }
    }
    for i in 0..(pw * ph / 400).max(2) {
        let seed = hash(i as u64 * 7919 + 42);
        let bx = (seed % pw as u64) as usize;
        let by = (seed >> 10) as usize % ph;
        let tw = ((tick as f64 * 0.07 + i as f64).sin() * 0.5 + 0.5) * 255.0;
        let v  = tw as u8;
        let c  = Color::Rgb(v, v, (v as u16 + 40).min(255) as u8);
        buf[by][bx] = Some(c);
        if bx + 1 < pw { buf[by][bx+1] = Some(c); }
        if by + 1 < ph { buf[by+1][bx] = Some(c); }
    }
    draw_ufo(buf, pw, ph, tick);
}

fn fill_fire(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let side_w   = (pw * 19 / 100).max(4);
    let mantel_h = (ph * 11 / 100).max(2);
    let hearth_y = ph.saturating_sub((ph * 9 / 100).max(2));
    let fire_x0  = side_w;
    let fire_x1  = pw.saturating_sub(side_w);
    let fire_pw  = fire_x1.saturating_sub(fire_x0);
    let fire_ph  = hearth_y.saturating_sub(mantel_h);

    // Dark room background
    for py in 0..ph { for ppx in 0..pw { buf[py][ppx] = Some(Color::Rgb(5, 4, 3)); } }

    // Back wall of fireplace — soot-stained, darker than room to push it back
    for py in mantel_h..hearth_y {
        for ppx in fire_x0..fire_x1 {
            let v = hash(ppx as u64 * 3 + py as u64 * 7 + 999);
            buf[py][ppx] = Some(Color::Rgb(
                11u8.saturating_add((v % 4) as u8),
                9u8.saturating_add(((v >> 4) % 3) as u8),
                8u8.saturating_add(((v >> 8) % 3) as u8),
            ));
        }
    }

    // Fire — hash-based crackle: each column flickers independently, no sine sweep
    let crackle_t = tick / 4;
    let blend_f   = (tick % 4) as f64 / 4.0;
    for ppx in fire_x0..fire_x1 {
        let local_x = ppx - fire_x0;
        let cx      = (local_x as f64 - fire_pw as f64 / 2.0).abs() / (fire_pw as f64 / 2.0);
        let arch    = (1.0 - cx * 0.65).max(0.0);

        // Current and next crackle state blended — independent per column
        let ha = hash(ppx as u64 * 11 + crackle_t * 37 + 101);
        let hb = hash(ppx as u64 * 11 + (crackle_t + 1) * 37 + 101);
        let ca = (ha % 100) as f64 / 100.0 * 0.30 - 0.09;
        let cb = (hb % 100) as f64 / 100.0 * 0.30 - 0.09;
        // Slight neighbour influence so adjacent columns aren't totally unrelated
        let hn = hash((ppx as u64 + 1) * 11 + crackle_t * 37 + 101);
        let cn = (hn % 100) as f64 / 100.0 * 0.30 - 0.09;
        let crackle = (ca * (1.0 - blend_f) + cb * blend_f) * 0.75 + cn * 0.25;

        let height = ((arch + crackle).clamp(0.1, 1.0) * 0.58 * fire_ph as f64) as usize;
        let top    = hearth_y.saturating_sub(height);

        for py in top..hearth_y {
            let f = (py - top) as f64 / height.max(1) as f64;
            buf[py][ppx] = Some(
                if f < 0.20 { let v = f/0.20; Color::Rgb(255, (210.0*v+45.0) as u8, (90.0*(1.0-v)) as u8) }
                else if f < 0.60 { let v = (f-0.20)/0.40; Color::Rgb(255, (45.0*(1.0-v)) as u8, 0) }
                else { Color::Rgb(((1.0-(f-0.60)/0.40)*240.0).max(0.0) as u8, 0, 0) }
            );
        }
    }

    // Sparks — brief bright pixels near flame tips
    let n_sparks = (fire_pw / 2).max(3);
    for i in 0..n_sparks {
        let hs = hash(i as u64 * 17 + tick / 5 * 31 + 555);
        if hs % 4 != 0 { continue; }
        let sx = fire_x0 + (hs >> 10) as usize % fire_pw;
        let sy = mantel_h + (hs >> 20) as usize % (fire_ph * 2 / 5);
        set_px(buf, sx as isize, sy as isize, match (hs >> 5) % 3 {
            0 => Color::Rgb(255, 210, 60),
            1 => Color::Rgb(255, 150, 25),
            _ => Color::Rgb(255, 255, 190),
        });
    }

    // Depth — shadow gradient at inner edges of opening, simulating column thickness
    let shadow_w = (fire_pw / 6).max(3);
    for py in mantel_h..hearth_y {
        for dx in 0..shadow_w {
            let alpha = (1.0 - dx as f64 / shadow_w as f64) * 0.78;
            for &px in &[fire_x0 + dx, fire_x1.saturating_sub(1 + dx)] {
                if px < buf[0].len() {
                    if let Some(Color::Rgb(r, g, b)) = buf[py][px] {
                        buf[py][px] = Some(Color::Rgb(
                            (r as f64 * (1.0 - alpha)) as u8,
                            (g as f64 * (1.0 - alpha)) as u8,
                            (b as f64 * (1.0 - alpha)) as u8,
                        ));
                    }
                }
            }
        }
    }

    // Fireplace structure drawn on top
    draw_fireplace(buf, pw, ph);
}

fn draw_snow_landscape(buf: &mut PixBuf, pw: usize, ph: usize) {
    let base_ground = ph * 84 / 100;
    let pi = std::f64::consts::PI;

    // Per-column ground height — gentle rolling hills
    let ground_at = |ppx: usize| -> usize {
        let xf = ppx as f64 / pw as f64;
        let h1 = ((xf * pi * 2.3).sin() * 0.5 + 0.5) * ph as f64 * 0.035;
        let h2 = ((xf * pi * 4.7 + 0.8).sin() * 0.5 + 0.5) * ph as f64 * 0.018;
        let noise = (hash(ppx as u64 * 17 + 333) % 4) as f64;
        ((base_ground as f64) - h1 - h2 - noise).max(0.0) as usize
    };

    // Helper: draw a mountain range with gradient base
    let draw_range = |buf: &mut PixBuf, base: usize, amp1: f64, freq1: f64, phase1: f64,
                                                       amp2: f64, freq2: f64, phase2: f64,
                      rock: (u8,u8,u8), snow_frac: f64| {
        for ppx in 0..pw {
            let xf  = ppx as f64 / pw as f64;
            let h1  = ((xf * pi * freq1 + phase1).sin() * 0.5 + 0.5) * ph as f64 * amp1;
            let h2  = ((xf * pi * freq2 + phase2).sin() * 0.5 + 0.5) * ph as f64 * amp2;
            let ht  = (h1 + h2) as usize;
            if ht == 0 { continue; }

            // Per-column noise on the base — breaks up the hard horizontal edge
            let base_noise = (hash(ppx as u64 * 13 + base as u64 * 7 + 444) % 7) as usize;
            let base_y  = (base + base_noise).min(ph);
            let top     = base_y.saturating_sub(ht);
            let snow_ln = top + (ht as f64 * snow_frac) as usize;
            let fade_h  = ((ht / 5) + 3).min(ht);
            let fade_y  = base_y.saturating_sub(fade_h);

            for py in top..base_y {
                if py >= buf.len() || ppx >= buf[0].len() { continue; }
                let noise = (hash(ppx as u64 * 5 + py as u64 * 9 + 1234) % 6) as f64 * 0.5;
                buf[py][ppx] = Some(if py < snow_ln {
                    let b = (py - top) as f64 / (snow_ln - top).max(1) as f64;
                    Color::Rgb((220.0 - b*25.0 + noise) as u8, (232.0 - b*22.0 + noise) as u8, (248.0 - b*14.0) as u8)
                } else if py < fade_y {
                    Color::Rgb(
                        (rock.0 as f64 + noise) as u8,
                        (rock.1 as f64 + noise) as u8,
                        (rock.2 as f64 + noise) as u8,
                    )
                } else {
                    // Gradient: rock → snow colour, with per-pixel noise for rough edge
                    let f = (py - fade_y) as f64 / fade_h as f64;
                    let n2 = (hash(ppx as u64 * 3 + py as u64 * 17 + 5678) % 10) as f64;
                    Color::Rgb(
                        (rock.0 as f64 + f * (190.0 - rock.0 as f64) + n2) as u8,
                        (rock.1 as f64 + f * (205.0 - rock.1 as f64) + n2) as u8,
                        (rock.2 as f64 + f * (222.0 - rock.2 as f64) + n2) as u8,
                    )
                });
            }
        }
    };

    // Distant range — more snow, lighter rock
    draw_range(buf, ph * 76 / 100, 0.14, 2.2, 0.0, 0.06, 4.3, 0.8, (22, 28, 48), 0.52);
    // Close range — more snow, heavier mountains
    draw_range(buf, base_ground,   0.24, 1.5, 0.5, 0.10, 3.1, 1.3, (8, 11, 20),  0.40);

    // Snow ground — per-column hill height
    for ppx in 0..pw {
        let gy = ground_at(ppx);
        for py in gy..ph {
            if py >= buf.len() || ppx >= buf[0].len() { continue; }
            let depth    = (py - gy) as f64 / (ph - gy).max(1) as f64;
            let aurora_t = (1.0 - depth) * 16.0;
            let v        = hash((ppx as u64 * 5).wrapping_add(py as u64 * 13).wrapping_add(9999));
            let noise    = (v % 6) as f64 * 0.5;
            let bright   = 182.0 + depth * 22.0;
            buf[py][ppx] = Some(Color::Rgb(
                (bright + noise) as u8,
                (bright + aurora_t + noise) as u8,
                (bright + aurora_t * 1.6 + noise + 10.0).min(255.0) as u8,
            ));
        }
    }
}

fn fill_aurora(buf: &mut PixBuf, pw: usize, ph: usize, tick: u64) {
    let t = tick as f64 * 0.04;
    for py in 0..ph {
        for ppx in 0..pw {
            let frac = py as f64 / ph as f64;
            buf[py][ppx] = Some(Color::Rgb((3.0+frac*5.0) as u8, (5.0+frac*8.0) as u8, (15.0+frac*20.0) as u8));
        }
    }
    let aurora_h = ph * 7 / 10;
    let band_colors: [(u8,u8,u8); 4] = [(20,200,120),(40,180,220),(100,60,220),(20,220,160)];
    for ppx in 0..pw {
        let x = ppx as f64 / pw as f64;
        for band in 0..4u32 {
            let b = band as f64;
            let cx = (b * 0.27 + t * (0.3 + b * 0.1)).sin() * 0.5 + 0.5;
            let width = 0.12 + (t * 0.07 + b).sin() * 0.05;
            let dist = (x - cx).abs();
            if dist < width {
                let intensity = (1.0 - dist / width).powi(2);
                let band_h = (aurora_h as f64 * (0.4 + intensity * 0.5)) as usize;
                let bottom_f = aurora_h as f64
                    + (b * 1.7 + t * 0.05).sin()               * ph as f64 * 0.10
                    + (ppx as f64 * 0.03 + t * 0.18 + b * 2.1).sin() * ph as f64 * 0.07
                    + (ppx as f64 * 0.09 + t * 0.31 + b * 0.9).sin() * ph as f64 * 0.04
                    + (ppx as f64 * 0.23 + t * 0.14 + b * 3.3).sin() * ph as f64 * 0.02;
                let bottom = (bottom_f as isize).clamp(0, ph as isize) as usize;
                let top = bottom.saturating_sub(band_h);
                let (r, g, bl) = band_colors[band as usize % band_colors.len()];
                for py in top..bottom.min(ph) {
                    let depth = if band_h > 0 { (py - top) as f64 / band_h as f64 } else { 0.0 };
                    let alpha = intensity * (1.0 - depth * 0.7);
                    let existing = buf[py][ppx];
                    let (er, eg, eb) = match existing {
                        Some(Color::Rgb(a, b, c)) => (a as f64, b as f64, c as f64),
                        _ => (0.0, 0.0, 0.0),
                    };
                    buf[py][ppx] = Some(Color::Rgb(
                        (er + r as f64 * alpha).min(255.0) as u8,
                        (eg + g as f64 * alpha).min(255.0) as u8,
                        (eb + bl as f64 * alpha).min(255.0) as u8,
                    ));
                }
            }
        }
    }
    for i in 0..(pw * ph / 60).max(5) {
        let seed = hash(i as u64 + 5555);
        let sx = (seed % pw as u64) as usize;
        let sy = (seed >> 8) as usize % (ph / 2).max(1);
        let tw = ((tick as f64 * 0.1 + i as f64 * 1.3).sin() * 0.4 + 0.6) * 160.0;
        let v = tw as u8;
        if let Some(Color::Rgb(er, eg, eb)) = buf[sy][sx] {
            if (er as u16 + eg as u16 + eb as u16) < 150 {
                buf[sy][sx] = Some(Color::Rgb(v, v, v));
            }
        }
    }
    draw_snow_landscape(buf, pw, ph);
}

// ── Theme list ────────────────────────────────────────────────────────────────

type FillFn = fn(&mut PixBuf, usize, usize, u64);

struct Theme { fill: FillFn, color: Color }

const THEMES: &[Theme] = &[
    Theme { fill: fill_waves,  color: Color::Rgb(0,   120, 210) },
    Theme { fill: fill_rain,   color: Color::Rgb(80,  130, 210) },
    Theme { fill: fill_leaves, color: Color::Rgb(210, 95,  15)  },
    Theme { fill: fill_stars,  color: Color::Rgb(140, 140, 255) },
    Theme { fill: fill_fire,   color: Color::Rgb(255, 90,  0)   },
    Theme { fill: fill_aurora, color: Color::Rgb(20,  210, 150) },
];

// ── Public Animation struct ───────────────────────────────────────────────────

pub struct Animation {
    theme_idx: usize,
    pub render_mode: RenderMode,
    tick_count: u64,
}

impl Animation {
    pub fn new() -> Self {
        Self { theme_idx: 0, render_mode: RenderMode::Half, tick_count: 0 }
    }

    pub fn tick(&mut self) { self.tick_count += 1; }

    pub fn next_theme(&mut self) { self.theme_idx = (self.theme_idx + 1) % THEMES.len(); }
    pub fn prev_theme(&mut self) { self.theme_idx = (self.theme_idx + THEMES.len() - 1) % THEMES.len(); }
    pub fn next_mode(&mut self)  { self.render_mode = self.render_mode.next(); }
    pub fn prev_mode(&mut self)  { self.render_mode = self.render_mode.prev(); }

    pub fn theme_color(&self) -> Color { THEMES[self.theme_idx].color }

    pub fn render_lines(&self, _phase: &crate::timer::Phase, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
        if char_w == 0 || char_h == 0 { return vec![]; }
        let mode = self.render_mode;
        let theme = &THEMES[self.theme_idx];
        let (pw, ph) = (mode.pw(char_w), mode.ph(char_h));
        let mut buf = new_buf(pw, ph);
        (theme.fill)(&mut buf, pw, ph, self.tick_count);
        dispatch(mode, &buf, char_w, char_h)
    }
}
