//! Main page view — sequencer grid, visualizations (scope/spectrum), script previews.

use std::collections::HashSet;

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

use crate::app::App;
use crate::engine::SequencerSnapshot;
use crate::state::MainLayout;
use crate::theme;
use crate::views::render::highlight_script_lines;
use crate::state::{ScopeMode, SpectrumMode};
use crate::widgets::{Lissajous, Orientation, Scope, Spectrum, SpectrumStyle, VuMeter, Waveform};

pub fn layout(area: Rect) -> [Rect; 3] {
    Layout::horizontal([
        Constraint::Fill(1),
        Constraint::Length(2),
        Constraint::Length(10),
    ])
    .areas(area)
}

pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
    let [main_area, _, vu_area] = layout(area);

    if matches!(app.audio.config.layout, MainLayout::Top) {
        render_top_layout(frame, app, snapshot, main_area);
    } else {
        let has_viz = app.audio.config.show_scope
            || app.audio.config.show_spectrum
            || app.audio.config.show_lissajous
            || app.audio.config.show_preview;
        let (viz_area, sequencer_area) =
            viz_seq_split(main_area, app.audio.config.layout, has_viz);
        if has_viz {
            render_viz_area(frame, app, snapshot, viz_area);
        }
        render_sequencer(frame, app, snapshot, sequencer_area);
    }

    render_vu_meter(frame, app, vu_area);
}

fn render_top_layout(
    frame: &mut Frame,
    app: &App,
    snapshot: &SequencerSnapshot,
    main_area: Rect,
) {
    let has_audio_viz = app.audio.config.show_scope
        || app.audio.config.show_spectrum
        || app.audio.config.show_lissajous;
    let has_preview = app.audio.config.show_preview;

    let mut constraints = Vec::new();
    if has_audio_viz {
        constraints.push(Constraint::Fill(1));
    }
    if has_preview {
        constraints.push(Constraint::Length(preview_height(has_audio_viz)));
    }
    constraints.push(Constraint::Fill(1));

    let areas = Layout::vertical(&constraints).split(main_area);

    let mut idx = 0;
    if has_audio_viz {
        render_audio_viz(frame, app, areas[idx]);
        idx += 1;
    }
    if has_preview {
        let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
        let has_prelude = !app.project_state.project.prelude.trim().is_empty();
        if has_prelude {
            let [script_area, prelude_area] =
                Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]);
            render_script_preview(frame, app, snapshot, &user_words, script_area);
            render_prelude_preview(frame, app, &user_words, prelude_area);
        } else {
            render_script_preview(frame, app, snapshot, &user_words, areas[idx]);
        }
        idx += 1;
    }
    render_sequencer(frame, app, snapshot, areas[idx]);
}

pub(crate) fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
    let mut panels: Vec<VizPanel> = Vec::new();
    if app.audio.config.show_scope { panels.push(VizPanel::Scope); }
    if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); }
    if app.audio.config.show_lissajous { panels.push(VizPanel::Lissajous); }

    if panels.is_empty() { return; }

    let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
    let areas: Vec<Rect> = Layout::horizontal(&constraints).split(area).to_vec();

    for (panel, panel_area) in panels.iter().zip(areas.iter()) {
        match panel {
            VizPanel::Scope => render_scope(frame, app, *panel_area, Orientation::Horizontal),
            VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
            VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
            VizPanel::Preview => {}
        }
    }
}

fn preview_height(has_audio_viz: bool) -> u16 {
    if has_audio_viz { 10 } else { 14 }
}

pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
    if matches!(app.audio.config.layout, MainLayout::Top) {
        let has_audio_viz = app.audio.config.show_scope
            || app.audio.config.show_spectrum
            || app.audio.config.show_lissajous;
        let has_preview = app.audio.config.show_preview;

        let mut constraints = Vec::new();
        if has_audio_viz {
            constraints.push(Constraint::Fill(1));
        }
        if has_preview {
            constraints.push(Constraint::Length(preview_height(has_audio_viz)));
        }
        constraints.push(Constraint::Fill(1));

        let areas = Layout::vertical(&constraints).split(main_area);
        areas[areas.len() - 1]
    } else {
        let has_viz = app.audio.config.show_scope
            || app.audio.config.show_spectrum
            || app.audio.config.show_lissajous
            || app.audio.config.show_preview;
        let (_, seq_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz);
        seq_area
    }
}

enum VizPanel {
    Scope,
    Spectrum,
    Lissajous,
    Preview,
}

fn render_viz_area(
    frame: &mut Frame,
    app: &App,
    snapshot: &SequencerSnapshot,
    area: Rect,
) {
    let is_vertical_layout = matches!(app.audio.config.layout, MainLayout::Left | MainLayout::Right);
    let show_scope = app.audio.config.show_scope;
    let show_spectrum = app.audio.config.show_spectrum;
    let show_lissajous = app.audio.config.show_lissajous;
    let show_preview = app.audio.config.show_preview;

    let mut panels = Vec::new();
    if show_scope { panels.push(VizPanel::Scope); }
    if show_spectrum { panels.push(VizPanel::Spectrum); }
    if show_lissajous { panels.push(VizPanel::Lissajous); }
    if show_preview { panels.push(VizPanel::Preview); }

    let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();

    let areas: Vec<Rect> = if is_vertical_layout {
        Layout::vertical(&constraints).split(area).to_vec()
    } else {
        Layout::horizontal(&constraints).split(area).to_vec()
    };

    let orientation = if is_vertical_layout {
        Orientation::Vertical
    } else {
        Orientation::Horizontal
    };

    let user_words_once: Option<HashSet<String>> = if panels.iter().any(|p| matches!(p, VizPanel::Preview)) {
        Some(app.dict.lock().keys().cloned().collect())
    } else {
        None
    };

    for (panel, panel_area) in panels.iter().zip(areas.iter()) {
        match panel {
            VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
            VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
            VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
            VizPanel::Preview => {
                let user_words = user_words_once.as_ref().unwrap();
                let has_prelude = !app.project_state.project.prelude.trim().is_empty();
                if has_prelude {
                    let [script_area, prelude_area] = if is_vertical_layout {
                        Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
                            .areas(*panel_area)
                    } else {
                        Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)])
                            .areas(*panel_area)
                    };
                    render_script_preview(frame, app, snapshot, user_words, script_area);
                    render_prelude_preview(frame, app, user_words, prelude_area);
                } else {
                    render_script_preview(frame, app, snapshot, user_words, *panel_area);
                }
            }
        }
    }
}

const TILE_HEIGHT: u16 = 3;
const ROW_GAP: u16 = 1;

pub fn steps_per_page(area_height: u16) -> usize {
    if area_height < 5 {
        return 8;
    }
    let usable = (area_height - 2) as usize;
    let max_rows = (usable + ROW_GAP as usize) / (TILE_HEIGHT as usize + ROW_GAP as usize);
    (max_rows * 8).clamp(8, 128)
}

pub fn viz_seq_split(
    main_area: Rect,
    layout: MainLayout,
    has_viz: bool,
) -> (Rect, Rect) {
    match layout {
        MainLayout::Top => {
            if has_viz {
                let [viz, seq] = Layout::vertical([
                    Constraint::Fill(1),
                    Constraint::Fill(1),
                ])
                .areas(main_area);
                (viz, seq)
            } else {
                (Rect::default(), main_area)
            }
        }
        MainLayout::Bottom => {
            if has_viz {
                let [seq, viz] = Layout::vertical([
                    Constraint::Fill(1),
                    Constraint::Fill(1),
                ])
                .areas(main_area);
                (viz, seq)
            } else {
                (Rect::default(), main_area)
            }
        }
        MainLayout::Left => {
            let viz_width = if has_viz { 33 } else { 0 };
            let [viz, _spacer, seq] = Layout::horizontal([
                Constraint::Percentage(viz_width),
                Constraint::Length(2),
                Constraint::Fill(1),
            ])
            .areas(main_area);
            (viz, seq)
        }
        MainLayout::Right => {
            let viz_width = if has_viz { 33 } else { 0 };
            let [seq, _spacer, viz] = Layout::horizontal([
                Constraint::Fill(1),
                Constraint::Length(2),
                Constraint::Percentage(viz_width),
            ])
            .areas(main_area);
            (viz, seq)
        }
    }
}

pub fn grid_layout(area: Rect, steps_on_page: usize) -> Vec<(Rect, usize)> {
    if area.width < 50 || steps_on_page == 0 {
        return Vec::new();
    }

    let num_rows = steps_on_page.div_ceil(8);
    let steps_per_row = steps_on_page.div_ceil(num_rows);

    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);

    let mut tiles = Vec::with_capacity(steps_on_page);

    for row_idx in 0..num_rows {
        let row_area = row_areas[row_idx * 2];
        let start_step = row_idx * steps_per_row;
        let end_step = (start_step + steps_per_row).min(steps_on_page);
        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 {
            tiles.push((col_areas[col_idx * 2], start_step + col_idx));
        }
    }

    tiles
}

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

    if area.width < 50 {
        let msg = Paragraph::new("Terminal too narrow")
            .alignment(Alignment::Center)
            .style(Style::new().fg(theme.ui.text_muted));
        frame.render_widget(msg, area);
        return;
    }

    let pattern = app.current_edit_pattern();
    let length = pattern.length;
    let spp = steps_per_page(area.height);
    app.editor_ctx.steps_per_page.set(spp);
    let page = app.editor_ctx.step / spp;
    let page_start = page * spp;
    let steps_on_page = (page_start + spp).min(length) - page_start;

    for (tile_rect, step_offset) in grid_layout(area, steps_on_page) {
        let step_idx = page_start + step_offset;
        if step_idx < length {
            render_tile(frame, tile_rect, app, snapshot, step_idx);
        }
    }
}

fn render_tile(
    frame: &mut Frame,
    area: Rect,
    app: &App,
    snapshot: &SequencerSnapshot,
    step_idx: usize,
) {
    let theme = theme::get();
    let pattern = app.current_edit_pattern();
    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 is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
    let is_selected = step_idx == app.editor_ctx.step;
    let in_selection = app
        .editor_ctx
        .selection_range()
        .map(|r| r.contains(&step_idx))
        .unwrap_or(false);

    let is_playing = if app.playback.playing {
        snapshot.get_step(app.editor_ctx.bank, app.editor_ctx.pattern) == Some(step_idx)
    } else {
        false
    };

    let link_color = step.and_then(|s| s.source).map(|src| {
        let i = src as usize % 5;
        (theme.tile.link_bright[i], theme.tile.link_dim[i])
    });

    let (bg, fg) = match (is_playing, is_active, is_selected, is_linked, in_selection) {
        (true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg),
        (true, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg),
        (false, true, true, true, _) => {
            let (r, g, b) = link_color.unwrap().0;
            (Color::Rgb(r, g, b), theme.selection.cursor_fg)
        }
        (false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg),
        (false, true, _, _, true) => (theme.tile.active_in_range_bg, theme.selection.cursor_fg),
        (false, true, false, true, _) => {
            let (r, g, b) = link_color.unwrap().1;
            (Color::Rgb(r, g, b), theme.tile.active_fg)
        }
        (false, true, false, false, _) => {
            let bg = if has_content {
                theme.tile.content_bg
            } else {
                theme.tile.active_bg
            };
            (bg, theme.tile.active_fg)
        }
        (false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
        (false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg),
        (false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
    };

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

    let source_idx = step.and_then(|s| s.source);
    let symbol = if is_playing {
        "▶".to_string()
    } else if let Some(source) = source_idx {
        format!("→{:02}", source + 1)
    } else if has_content {
        format!("·{:02}·", step_idx + 1)
    } else {
        format!("{:02}", step_idx + 1)
    };

    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())
    };

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

    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 viz_gain(data: &[f32], config: &crate::state::audio::AudioConfig) -> f32 {
    if config.normalize_viz {
        let peak = data.iter().fold(0.0_f32, |m, s| m.max(s.abs()));
        if peak > 0.0001 { 1.0 / peak } else { 1.0 }
    } else {
        config.gain_boost
    }
}

pub(crate) fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let orientation = if app.audio.config.scope_vertical {
        Orientation::Vertical
    } else {
        orientation
    };
    let gain = viz_gain(&app.metrics.scope, &app.audio.config);
    match app.audio.config.scope_mode {
        ScopeMode::Line => {
            let scope = Scope::new(&app.metrics.scope)
                .orientation(orientation)
                .color(theme.meter.low)
                .gain(gain);
            frame.render_widget(scope, inner);
        }
        ScopeMode::Filled => {
            let waveform = Waveform::new(&app.metrics.scope)
                .orientation(orientation)
                .color(theme.meter.low)
                .gain(gain);
            frame.render_widget(waveform, inner);
        }
    }
}

pub(crate) fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let gain = if app.audio.config.normalize_viz {
        viz_gain(&app.metrics.spectrum, &app.audio.config)
    } else {
        1.0
    };
    let style = match app.audio.config.spectrum_mode {
        SpectrumMode::Bars => SpectrumStyle::Bars,
        SpectrumMode::Line => SpectrumStyle::Line,
        SpectrumMode::Filled => SpectrumStyle::Filled,
    };
    let spectrum = Spectrum::new(&app.metrics.spectrum)
        .gain(gain)
        .style(style)
        .peaks(app.audio.config.spectrum_peaks);
    frame.render_widget(spectrum, inner);
}

pub(crate) fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let peak = app.metrics.scope.iter().chain(app.metrics.scope_right.iter())
        .fold(0.0_f32, |m, s| m.max(s.abs()));
    let gain = if app.audio.config.normalize_viz {
        if peak > 0.0001 { 1.0 / peak } else { 1.0 }
    } else {
        app.audio.config.gain_boost
    };
    let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
        .color(theme.meter.low)
        .gain(gain)
        .trails(app.audio.config.lissajous_trails);
    frame.render_widget(lissajous, inner);
}

fn render_script_preview(
    frame: &mut Frame,
    app: &App,
    snapshot: &SequencerSnapshot,
    user_words: &HashSet<String>,
    area: Rect,
) {
    let theme = theme::get();

    let pattern = app.current_edit_pattern();
    let step_idx = app.editor_ctx.step;
    let step = pattern.step(step_idx);
    let source_idx = step.and_then(|s| s.source);
    let step_name = step.and_then(|s| s.name.as_ref());

    let title = match (source_idx, step_name) {
        (Some(src), Some(name)) => format!(" {:02}: {} -> {:02} ", step_idx + 1, name, src + 1),
        (None, Some(name)) => format!(" {:02}: {} ", step_idx + 1, name),
        (Some(src), None) => format!(" {:02} -> {:02} ", step_idx + 1, src + 1),
        (None, None) => format!(" Step {:02} ", step_idx + 1),
    };

    let block = Block::default()
        .borders(Borders::ALL)
        .title(title)
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let script = pattern.resolve_script(step_idx).unwrap_or("");
    if script.is_empty() {
        let empty = Paragraph::new("(empty)")
            .alignment(Alignment::Center)
            .style(Style::new().fg(theme.ui.text_dim));
        let centered = Rect {
            y: inner.y + inner.height / 2,
            height: 1,
            ..inner
        };
        frame.render_widget(empty, centered);
        return;
    }

    let trace = if app.ui.runtime_highlight && app.playback.playing {
        let source = pattern.resolve_source(step_idx);
        snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
    } else {
        None
    };

    let lines = highlight_script_lines(script, trace, user_words, inner.height as usize);
    frame.render_widget(Paragraph::new(lines), inner);
}

pub(crate) fn render_prelude_preview(
    frame: &mut Frame,
    app: &App,
    user_words: &HashSet<String>,
    area: Rect,
) {
    let theme = theme::get();
    let prelude = &app.project_state.project.prelude;

    let block = Block::default()
        .borders(Borders::ALL)
        .title(" Prelude ")
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let lines = highlight_script_lines(prelude, None, user_words, inner.height as usize);
    frame.render_widget(Paragraph::new(lines), inner);
}

fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(theme.ui.border));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right);
    frame.render_widget(vu, inner);
}

