use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use ratatui::Frame;

use crate::app::App;
use crate::engine::SequencerSnapshot;
use crate::model::{MAX_BANKS, MAX_PATTERNS};
use crate::state::PatternsColumn;
use crate::theme;
use crate::widgets::{render_scroll_indicators, IndicatorAlign};

const MIN_ROW_HEIGHT: u16 = 1;

/// Replaces the background color of spans beyond `filled_cols` with `unfilled_bg`.
fn apply_progress_bg(spans: Vec<Span<'_>>, filled_cols: usize, unfilled_bg: Color) -> Vec<Span<'_>> {
    let mut result = Vec::with_capacity(spans.len() + 1);
    let mut col = 0usize;
    for span in spans {
        let span_width = span.content.chars().count();
        if col + span_width <= filled_cols {
            result.push(span);
        } else if col >= filled_cols {
            result.push(Span::styled(span.content, span.style.bg(unfilled_bg)));
        } else {
            let split_at = filled_cols - col;
            let byte_offset = span.content.char_indices()
                .nth(split_at)
                .map_or(span.content.len(), |(i, _)| i);
            let (left, right) = span.content.split_at(byte_offset);
            result.push(Span::styled(left.to_string(), span.style));
            result.push(Span::styled(right.to_string(), span.style.bg(unfilled_bg)));
        }
        col += span_width;
    }
    result
}

pub fn layout(area: Rect) -> [Rect; 3] {
    let [top_area, _bottom_area] =
        Layout::vertical([Constraint::Fill(1), Constraint::Length(8)]).areas(area);
    let [banks_area, patterns_area] =
        Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(top_area);
    [banks_area, patterns_area, _bottom_area]
}

pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
    let theme = theme::get();

    let [banks_area, patterns_area, bottom_area] = layout(area);

    let [steps_area, props_area] =
        Layout::horizontal([Constraint::Fill(1), Constraint::Length(22)]).areas(bottom_area);

    render_banks(frame, app, snapshot, banks_area);
    render_patterns(frame, app, snapshot, patterns_area);

    let bank = app.patterns_nav.bank_cursor;
    let pattern_idx = app.patterns_nav.pattern_cursor;

    // Steps block
    let steps_block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border))
        .title(" Steps ")
        .title_style(Style::new().fg(theme.ui.unfocused));
    let steps_inner = steps_block.inner(steps_area);
    frame.render_widget(steps_block, steps_area);
    render_mini_tile_grid(frame, app, snapshot, steps_inner, bank, pattern_idx);

    // Properties block
    let props_block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border))
        .title(" Properties ")
        .title_style(Style::new().fg(theme.ui.unfocused));
    let props_inner = props_block.inner(props_area);
    frame.render_widget(props_block, props_area);
    render_properties(frame, app, props_inner, bank, pattern_idx);
}

fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
    let theme = theme::get();
    let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);

    let border_color = if is_focused { theme.ui.header } else { theme.ui.border };
    let title_style = if is_focused {
        Style::new().fg(theme.ui.header)
    } else {
        Style::new().fg(theme.ui.unfocused)
    };
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(border_color))
        .title(" Banks ")
        .title_style(title_style);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let banks_with_playback: Vec<usize> = snapshot
        .active_patterns
        .iter()
        .map(|p| p.bank)
        .collect();

    let banks_with_staged: Vec<usize> = app
        .playback
        .staged_changes
        .iter()
        .filter_map(|c| match &c.change {
            crate::engine::PatternChange::Start { bank, .. } => Some(*bank),
            _ => None,
        })
        .collect();

    let cursor = app.patterns_nav.bank_cursor;
    let max_visible = (inner.height / MIN_ROW_HEIGHT) as usize;
    let max_visible = max_visible.max(1);

    let scroll_offset = if MAX_BANKS <= max_visible {
        0
    } else {
        cursor
            .saturating_sub(max_visible / 2)
            .min(MAX_BANKS - max_visible)
    };

    let visible_count = MAX_BANKS.min(max_visible);
    let row_height = inner.height / visible_count as u16;
    let row_height = row_height.max(MIN_ROW_HEIGHT);

    for visible_idx in 0..visible_count {
        let idx = scroll_offset + visible_idx;
        let y = inner.y + (visible_idx as u16) * row_height;
        if y >= inner.y + inner.height {
            break;
        }

        let row_area = Rect {
            x: inner.x,
            y,
            width: inner.width,
            height: row_height.min(inner.y + inner.height - y),
        };

        let is_cursor = is_focused && idx == app.patterns_nav.bank_cursor;
        let is_selected = idx == app.patterns_nav.bank_cursor;
        let is_edit = idx == app.editor_ctx.bank;
        let is_playing = banks_with_playback.contains(&idx);
        let is_staged = banks_with_staged.contains(&idx);
        let is_in_range = is_focused
            && app
                .patterns_nav
                .bank_selection_range()
                .is_some_and(|r| r.contains(&idx));

        let has_muted = (0..MAX_PATTERNS).any(|p| app.mute.is_muted(idx, p));
        let has_soloed = (0..MAX_PATTERNS).any(|p| app.mute.is_soloed(idx, p));
        let has_staged_mute = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_mute(idx, p));
        let has_staged_solo = (0..MAX_PATTERNS).any(|p| app.playback.has_staged_solo(idx, p));
        let has_staged_mute_solo = has_staged_mute || has_staged_solo;

        let (bg, fg, prefix) = if is_cursor {
            (theme.selection.cursor, theme.selection.cursor_fg, "")
        } else if is_in_range {
            (theme.selection.in_range_bg, theme.selection.in_range_fg, "")
        } else if is_playing {
            if has_staged_mute_solo {
                (theme.list.staged_play_bg, theme.list.staged_play_fg, ">*")
            } else if has_soloed {
                (theme.list.soloed_bg, theme.list.soloed_fg, ">S")
            } else if has_muted {
                (theme.list.muted_bg, theme.list.muted_fg, ">M")
            } else {
                (theme.list.playing_bg, theme.list.playing_fg, "> ")
            }
        } else if is_staged {
            (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
        } else if has_staged_mute_solo {
            (theme.list.staged_play_bg, theme.list.staged_play_fg, " *")
        } else if has_soloed && is_selected {
            (theme.list.soloed_bg, theme.list.soloed_fg, " S")
        } else if has_muted && is_selected {
            (theme.list.muted_bg, theme.list.muted_fg, " M")
        } else if is_selected {
            (theme.list.hover_bg, theme.list.hover_fg, "")
        } else if is_edit {
            (theme.list.edit_bg, theme.list.edit_fg, "")
        } else {
            (theme.ui.bg, theme.ui.text_muted, "")
        };

        let bank_ref = &app.project_state.project.banks[idx];
        let name = bank_ref.name.as_deref().unwrap_or("");
        let content_count = bank_ref.content_pattern_count();

        let idx_part = format!("{}{:02}", prefix, idx + 1);
        let count_part = format!("{}", content_count);
        let available_for_name = (row_area.width as usize)
            .saturating_sub(idx_part.len() + 1 + count_part.len());
        let label = if name.is_empty() {
            let pad = " ".repeat(available_for_name);
            format!("{idx_part}{pad}{count_part}")
        } else {
            let name_display: String = name.chars().take(available_for_name.saturating_sub(1)).collect();
            let used = name_display.chars().count() + 1;
            let pad = " ".repeat(available_for_name.saturating_sub(used));
            format!("{idx_part} {name_display}{pad}{count_part}")
        };

        let style = Style::new().bg(bg).fg(fg);
        let style = if is_playing || is_staged {
            style.add_modifier(Modifier::BOLD)
        } else {
            style
        };

        let bg_block = Block::default().style(Style::new().bg(bg));
        frame.render_widget(bg_block, row_area);

        let text_y = if row_height > 1 {
            row_area.y + (row_height - 1) / 2
        } else {
            row_area.y
        };
        let text_area = Rect {
            x: row_area.x,
            y: text_y,
            width: row_area.width,
            height: 1,
        };
        let para = Paragraph::new(label).style(style);
        frame.render_widget(para, text_area);
    }

    render_scroll_indicators(
        frame,
        inner,
        scroll_offset,
        visible_count,
        MAX_BANKS,
        theme.ui.text_muted,
        IndicatorAlign::Center,
    );
}

fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
    use crate::model::PatternSpeed;

    let theme = theme::get();
    let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);

    let bank = app.patterns_nav.bank_cursor;
    let bank_name = app.project_state.project.banks[bank].name.as_deref();
    let title_text = match bank_name {
        Some(name) => format!(" Patterns ({name}) "),
        None => format!(" Patterns (Bank {:02}) ", bank + 1),
    };

    let border_color = if is_focused { theme.ui.header } else { theme.ui.border };
    let title_style = if is_focused {
        Style::new().fg(theme.ui.header)
    } else {
        Style::new().fg(theme.ui.unfocused)
    };
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(border_color))
        .title(title_text)
        .title_style(title_style);
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let playing_patterns: Vec<usize> = snapshot
        .active_patterns
        .iter()
        .filter(|p| p.bank == bank)
        .map(|p| p.pattern)
        .collect();

    let staged_to_play: Vec<usize> = app
        .playback
        .staged_changes
        .iter()
        .filter_map(|c| match &c.change {
            crate::engine::PatternChange::Start {
                bank: b, pattern, ..
            } if *b == bank => Some(*pattern),
            _ => None,
        })
        .collect();

    let staged_to_stop: Vec<usize> = app
        .playback
        .staged_changes
        .iter()
        .filter_map(|c| match &c.change {
            crate::engine::PatternChange::Stop {
                bank: b,
                pattern,
            } if *b == bank => Some(*pattern),
            _ => None,
        })
        .collect();

    let edit_pattern = if app.editor_ctx.bank == bank {
        Some(app.editor_ctx.pattern)
    } else {
        None
    };

    let cursor = app.patterns_nav.pattern_cursor;
    let available = inner.height as usize;
    // Cursor row takes 2 lines (main + detail); account for 1 extra
    let max_visible = available.saturating_sub(1).max(1);

    let scroll_offset = if MAX_PATTERNS <= max_visible {
        0
    } else {
        cursor
            .saturating_sub(max_visible / 2)
            .min(MAX_PATTERNS - max_visible)
    };

    let visible_count = MAX_PATTERNS.min(max_visible);

    let mut y = inner.y;
    for visible_idx in 0..visible_count {
        let idx = scroll_offset + visible_idx;
        let is_expanded = idx == cursor;
        let row_h = if is_expanded { 2u16 } else { 1u16 };
        if y >= inner.y + inner.height {
            break;
        }

        let row_area = Rect {
            x: inner.x,
            y,
            width: inner.width,
            height: row_h.min(inner.y + inner.height - y),
        };

        let is_cursor = is_focused && idx == app.patterns_nav.pattern_cursor;
        let is_selected = idx == app.patterns_nav.pattern_cursor;
        let is_edit = edit_pattern == Some(idx);
        let is_playing = playing_patterns.contains(&idx);
        let is_staged_play = staged_to_play.contains(&idx);
        let is_staged_stop = staged_to_stop.contains(&idx);
        let is_in_range = is_focused
            && app
                .patterns_nav
                .pattern_selection_range()
                .is_some_and(|r| r.contains(&idx));

        let is_muted = app.mute.is_muted(bank, idx);
        let is_soloed = app.mute.is_soloed(bank, idx);
        let has_staged_mute = app.playback.has_staged_mute(bank, idx);
        let has_staged_solo = app.playback.has_staged_solo(bank, idx);
        let has_staged_props = app.playback.has_staged_props(bank, idx);
        let preview_muted = is_muted ^ has_staged_mute;
        let preview_soloed = is_soloed ^ has_staged_solo;
        let is_effectively_muted = app.mute.is_effectively_muted(bank, idx);

        let (bg, fg, prefix) = if is_cursor {
            (theme.selection.cursor, theme.selection.cursor_fg, "")
        } else if is_in_range {
            (theme.selection.in_range_bg, theme.selection.in_range_fg, "")
        } else if is_playing {
            if is_staged_stop {
                (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- ")
            } else if has_staged_solo {
                if preview_soloed {
                    (theme.list.soloed_bg, theme.list.soloed_fg, "+S")
                } else {
                    (theme.list.playing_bg, theme.list.playing_fg, "-S")
                }
            } else if has_staged_mute {
                if preview_muted {
                    (theme.list.muted_bg, theme.list.muted_fg, "+M")
                } else {
                    (theme.list.playing_bg, theme.list.playing_fg, "-M")
                }
            } else if is_soloed {
                (theme.list.soloed_bg, theme.list.soloed_fg, ">S")
            } else if is_muted {
                (theme.list.muted_bg, theme.list.muted_fg, ">M")
            } else if is_effectively_muted {
                (theme.list.muted_bg, theme.list.muted_fg, "> ")
            } else {
                (theme.list.playing_bg, theme.list.playing_fg, "> ")
            }
        } else if is_staged_play {
            (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ ")
        } else if has_staged_solo {
            if preview_soloed {
                (theme.list.soloed_bg, theme.list.soloed_fg, "+S")
            } else {
                (theme.ui.bg, theme.ui.text_muted, "-S")
            }
        } else if has_staged_mute {
            if preview_muted {
                (theme.list.muted_bg, theme.list.muted_fg, "+M")
            } else {
                (theme.ui.bg, theme.ui.text_muted, "-M")
            }
        } else if is_soloed {
            (theme.list.soloed_bg, theme.list.soloed_fg, " S")
        } else if is_muted {
            (theme.list.muted_bg, theme.list.muted_fg, " M")
        } else if is_selected {
            (theme.list.hover_bg, theme.list.hover_fg, "")
        } else if is_edit {
            (theme.list.edit_bg, theme.list.edit_fg, "")
        } else {
            (theme.ui.bg, theme.ui.text_muted, "")
        };

        let pattern = &app.project_state.project.banks[bank].patterns[idx];
        let name = pattern.name.as_deref().unwrap_or("");
        let length = pattern.length;
        let speed = pattern.speed;

        let base_style = Style::new().bg(bg).fg(fg);
        let bold_style = base_style.add_modifier(Modifier::BOLD);

        let content_area = if is_expanded {
            let border_color = if is_focused { theme.selection.cursor } else { theme.ui.unfocused };
            let block = Block::default()
                .borders(Borders::LEFT | Borders::RIGHT)
                .border_type(BorderType::QuadrantOutside)
                .border_style(Style::new().fg(border_color).bg(bg))
                .style(Style::new().bg(bg));
            let content = block.inner(row_area);
            frame.render_widget(block, row_area);
            content
        } else {
            let bg_block = Block::default().style(Style::new().bg(bg));
            frame.render_widget(bg_block, row_area);
            row_area
        };

        let text_area = Rect {
            x: content_area.x,
            y: content_area.y,
            width: content_area.width,
            height: 1,
        };

        let name_style = if is_playing || is_staged_play {
            bold_style
        } else {
            base_style
        };
        let dim_style = base_style.remove_modifier(Modifier::BOLD);

        let mut spans = vec![Span::styled(format!("{}{:02}", prefix, idx + 1), name_style)];
        if !name.is_empty() {
            spans.push(Span::styled(format!(" {name}"), name_style));
        }

        let content_count = pattern.content_step_count();
        let speed_str = if speed != PatternSpeed::NORMAL {
            format!("  {}", speed.label())
        } else {
            String::new()
        };
        let props_indicator = if has_staged_props { "~" } else { "" };
        let right_info = if content_count > 0 {
            format!("{props_indicator}{content_count}/{length}{speed_str}")
        } else {
            format!("{props_indicator}  {length}{speed_str}")
        };
        let left_width: usize = spans.iter().map(|s| s.content.chars().count()).sum();
        let right_width = right_info.chars().count();
        let padding = (text_area.width as usize).saturating_sub(left_width + right_width + 1);

        spans.push(Span::styled(" ".repeat(padding), dim_style));
        spans.push(Span::styled(right_info, dim_style));

        let spans = if is_playing && !is_cursor && !is_in_range {
            let ratio = snapshot.get_smooth_progress(bank, idx, length, speed.multiplier()).unwrap_or(0.0);
            let filled = (ratio * text_area.width as f64).min(text_area.width as f64) as usize;
            apply_progress_bg(spans, filled, theme.ui.bg)
        } else {
            spans
        };

        frame.render_widget(Paragraph::new(Line::from(spans)), text_area);

        if is_expanded && content_area.height >= 2 {
            let detail_area = Rect {
                x: content_area.x,
                y: content_area.y + 1,
                width: content_area.width,
                height: 1,
            };

            let right_label = format!(
                "{} · {}",
                pattern.quantization.label(),
                pattern.sync_mode.label()
            );
            let w = detail_area.width as usize;
            let label = if let Some(desc) = &pattern.description {
                let right_len = right_label.chars().count();
                let max_desc = w.saturating_sub(right_len + 1);
                let truncated: String = desc.chars().take(max_desc).collect();
                let pad = w.saturating_sub(truncated.chars().count() + right_len);
                format!("{truncated}{}{right_label}", " ".repeat(pad))
            } else {
                format!("{right_label:>w$}")
            };
            let padded_label = label;

            let filled_width = if is_playing {
                let ratio = snapshot.get_smooth_progress(bank, idx, length, speed.multiplier()).unwrap_or(0.0);
                (ratio * detail_area.width as f64).min(detail_area.width as f64) as usize
            } else {
                0
            };

            let dim_fg = theme.ui.text_muted;
            let progress_bg = theme.list.playing_bg;
            let byte_offset = padded_label
                .char_indices()
                .nth(filled_width)
                .map_or(padded_label.len(), |(i, _)| i);
            let (left, right) = padded_label.split_at(byte_offset);
            let detail_spans = vec![
                Span::styled(left.to_string(), Style::new().bg(progress_bg).fg(dim_fg)),
                Span::styled(right.to_string(), Style::new().bg(theme.ui.bg).fg(dim_fg)),
            ];
            frame.render_widget(Paragraph::new(Line::from(detail_spans)), detail_area);
        }

        y += row_area.height;
    }

    render_scroll_indicators(
        frame,
        inner,
        scroll_offset,
        visible_count,
        MAX_PATTERNS,
        theme.ui.text_muted,
        IndicatorAlign::Center,
    );
}

fn render_mini_tile_grid(
    frame: &mut Frame,
    app: &App,
    snapshot: &SequencerSnapshot,
    area: Rect,
    bank: usize,
    pattern_idx: usize,
) {
    let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
    let length = pattern.length;
    if length == 0 || area.height == 0 || area.width < 8 {
        return;
    }

    let playing_step = snapshot.get_step(bank, pattern_idx);

    let num_rows = length.div_ceil(8);
    let row_gap: u16 = 1;
    let max_tile_height: u16 = 4;

    let max_rows = (area.height as usize + row_gap as usize) / (1 + row_gap as usize);
    let num_rows = num_rows.min(max_rows.max(1));

    let available_for_rows =
        area.height.saturating_sub((num_rows.saturating_sub(1) as u16) * row_gap);
    let tile_height = (available_for_rows / num_rows as u16).min(max_tile_height).max(1);

    let total_grid_height =
        (num_rows as u16) * tile_height + (num_rows.saturating_sub(1) as u16) * row_gap;
    let y_offset = area.height.saturating_sub(total_grid_height) / 2;

    let grid_area = Rect {
        x: area.x,
        y: area.y + y_offset,
        width: area.width,
        height: total_grid_height.min(area.height),
    };

    let mut row_constraints: Vec<Constraint> = Vec::new();
    for i in 0..num_rows {
        row_constraints.push(Constraint::Length(tile_height));
        if i < num_rows - 1 {
            row_constraints.push(Constraint::Length(row_gap));
        }
    }
    let row_areas = Layout::vertical(row_constraints).split(grid_area);

    for row_idx in 0..num_rows {
        if row_idx * 2 >= row_areas.len() {
            break;
        }
        let row_area = row_areas[row_idx * 2];
        let start_step = row_idx * 8;
        let end_step = (start_step + 8).min(length);
        let cols_in_row = end_step - start_step;

        let mut col_constraints: Vec<Constraint> = Vec::new();
        for col in 0..cols_in_row {
            col_constraints.push(Constraint::Fill(1));
            if col < cols_in_row - 1 {
                if (col + 1) % 4 == 0 {
                    col_constraints.push(Constraint::Length(2));
                } else {
                    col_constraints.push(Constraint::Length(1));
                }
            }
        }
        let col_areas = Layout::horizontal(col_constraints).split(row_area);

        for col_idx in 0..cols_in_row {
            let step_idx = start_step + col_idx;
            let cell_area = col_areas[col_idx * 2];
            render_mini_tile(frame, pattern, cell_area, step_idx, playing_step);
        }
    }
}

fn render_mini_tile(
    frame: &mut Frame,
    pattern: &cagire_project::Pattern,
    area: Rect,
    step_idx: usize,
    playing_step: Option<usize>,
) {
    let theme = theme::get();
    let step = pattern.step(step_idx);
    let is_active = step.map(|s| s.active).unwrap_or(false);
    let has_content = step.map(|s| s.has_content()).unwrap_or(false);
    let source_idx = step.and_then(|s| s.source);
    let is_playing = playing_step == Some(step_idx);

    let (bg, fg) = if is_playing {
        if is_active {
            (theme.tile.playing_active_bg, theme.tile.playing_active_fg)
        } else {
            (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg)
        }
    } else if let Some(src) = source_idx {
        let i = src as usize % 5;
        let (r, g, b) = theme.tile.link_dim[i];
        (Color::Rgb(r, g, b), theme.tile.active_fg)
    } else if has_content {
        (theme.tile.content_bg, theme.tile.active_fg)
    } else if is_active {
        (theme.tile.active_bg, theme.tile.active_fg)
    } else {
        (theme.tile.inactive_bg, theme.tile.inactive_fg)
    };

    let symbol = if is_playing {
        "\u{25b6}".to_string()
    } else if let Some(src) = source_idx {
        format!("\u{2192}{:02}", src + 1)
    } else if has_content {
        format!("\u{00b7}{:02}\u{00b7}", step_idx + 1)
    } else {
        format!("{:02}", step_idx + 1)
    };

    let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
    frame.render_widget(bg_fill, area);

    let center_y = area.y + area.height / 2;

    let step_name = if let Some(src) = source_idx {
        pattern.step(src as usize).and_then(|s| s.name.as_ref())
    } else {
        step.and_then(|s| s.name.as_ref())
    };

    if let Some(name) = step_name {
        if center_y > area.y {
            let name_area = Rect {
                x: area.x,
                y: center_y - 1,
                width: area.width,
                height: 1,
            };
            let name_widget = Paragraph::new(name.as_str())
                .alignment(Alignment::Center)
                .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
            frame.render_widget(name_widget, name_area);
        }
    }

    let symbol_area = Rect {
        x: area.x,
        y: center_y,
        width: area.width,
        height: 1,
    };
    let symbol_widget = Paragraph::new(symbol)
        .alignment(Alignment::Center)
        .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
    frame.render_widget(symbol_widget, symbol_area);

    if has_content && center_y + 1 < area.y + area.height {
        let script = pattern.resolve_script(step_idx).unwrap_or("");
        if let Some(first_token) = script.split_whitespace().next() {
            let hint_area = Rect {
                x: area.x,
                y: center_y + 1,
                width: area.width,
                height: 1,
            };
            let hint_widget = Paragraph::new(first_token)
                .alignment(Alignment::Center)
                .style(Style::new().bg(bg).fg(theme.ui.text_dim));
            frame.render_widget(hint_widget, hint_area);
        }
    }
}

fn render_properties(
    frame: &mut Frame,
    app: &App,
    area: Rect,
    bank: usize,
    pattern_idx: usize,
) {
    use cagire_project::FollowUp;

    let theme = theme::get();
    let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];

    let name = pattern.name.as_deref().unwrap_or("-");
    let desc = pattern.description.as_deref().unwrap_or("-");
    let content_count = pattern.content_step_count();
    let steps_label = format!("{}/{}", content_count, pattern.length);
    let speed_label = pattern.speed.label();
    let quant_label = pattern.quantization.label();
    let sync_label = pattern.sync_mode.label();

    let label_style = Style::new().fg(theme.ui.text_muted);
    let value_style = Style::new().fg(theme.ui.text_primary);

    let mut rows: Vec<Line> = vec![
        Line::from(vec![
            Span::styled(" Name  ", label_style),
            Span::styled(name, value_style),
        ]),
        Line::from(vec![
            Span::styled(" Desc  ", label_style),
            Span::styled(desc, value_style),
        ]),
        Line::from(vec![
            Span::styled(" Steps ", label_style),
            Span::styled(steps_label, value_style),
        ]),
        Line::from(vec![
            Span::styled(" Speed ", label_style),
            Span::styled(speed_label, value_style),
        ]),
        Line::from(vec![
            Span::styled(" Quant ", label_style),
            Span::styled(quant_label, value_style),
        ]),
        Line::from(vec![
            Span::styled(" Sync  ", label_style),
            Span::styled(sync_label, value_style),
        ]),
    ];

    if pattern.follow_up != FollowUp::Loop {
        let follow_label = match pattern.follow_up {
            FollowUp::Loop => unreachable!(),
            FollowUp::Stop => "Stop".to_string(),
            FollowUp::Chain { bank: b, pattern: p } => format!("Chain B{:02}:P{:02}", b + 1, p + 1),
        };
        rows.push(Line::from(vec![
            Span::styled(" After ", label_style),
            Span::styled(follow_label, value_style),
        ]));
    }

    frame.render_widget(Paragraph::new(rows), area);
}
