use std::collections::HashSet;

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

use crate::app::App;
use crate::engine::SequencerSnapshot;
use crate::model::SourceSpan;
use crate::theme;
use crate::views::highlight;
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
use crate::widgets::hint_line;

pub fn layout(area: Rect) -> [Rect; 2] {
    Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)]).areas(area)
}

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

    render_editor(frame, app, snapshot, editor_area);
    render_sidebar(frame, app, sidebar_area);
}

fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
    let theme = theme::get();
    let focused = app.script_editor.focused;
    let speed_label = app.project_state.project.script_speed.label();
    let length = app.project_state.project.script_length;
    let title = format!(" Periodic Script ({speed_label}, {length} steps) ");

    let border_color = if focused { theme.modal.editor } else { theme.ui.border };
    let block = Block::default()
        .borders(Borders::ALL)
        .title(title)
        .border_style(Style::new().fg(border_color));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    if inner.height < 2 {
        return;
    }

    let editor_height = inner.height.saturating_sub(1);
    let editor_area = Rect::new(inner.x, inner.y, inner.width, editor_height);
    let hint_area = Rect::new(inner.x, inner.y + editor_height, inner.width, 1);

    let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();

    let trace = if app.ui.runtime_highlight && app.playback.playing {
        snapshot.script_trace()
    } else {
        None
    };

    let text_lines = app.script_editor.editor.lines();
    let mut line_offsets: Vec<usize> = Vec::with_capacity(text_lines.len());
    let mut offset = 0;
    for line in text_lines.iter() {
        line_offsets.push(offset);
        offset += line.len() + 1;
    }

    let resolved_display: Vec<(SourceSpan, String)> = trace
        .map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect())
        .unwrap_or_default();

    let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> {
        let line_start = line_offsets[row];
        let (exec, sel, res) = match trace {
            Some(t) => (
                adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
                adjust_spans_for_line(&t.selected_spans, line_start, line.len()),
                adjust_resolved_for_line(&resolved_display, line_start, line.len()),
            ),
            None => (Vec::new(), Vec::new(), Vec::new()),
        };
        highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words)
    };

    app.script_editor.editor.render(frame, editor_area, &highlighter);

    if !focused {
        let hints = hint_line(&[
            ("Enter", "edit"),
            ("S", "speed"),
            ("L", "length"),
            ("s", "save"),
            ("l", "load"),
            ("?", "keys"),
        ]);
        frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
    } else if app.script_editor.show_stack {
        let stack_text = app
            .script_editor
            .stack_cache
            .borrow()
            .as_ref()
            .map(|c| c.result.clone())
            .unwrap_or_else(|| "Stack: []".to_string());
        let hints = hint_line(&[("Esc", "unfocus"), ("C-e", "eval"), ("C-s", "hide stack")]);
        let [hint_left, stack_right] = Layout::horizontal([
            Constraint::Length(hints.width() as u16),
            Constraint::Fill(1),
        ])
        .areas(hint_area);
        frame.render_widget(Paragraph::new(hints), hint_left);
        let dim = Style::default().fg(theme.hint.text);
        frame.render_widget(
            Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
            stack_right,
        );
    } else {
        let hints = hint_line(&[
            ("Esc", "unfocus"),
            ("C-e", "eval"),
            ("C-s", "stack"),
        ]);
        frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
    }
}

fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) {
    use crate::widgets::Orientation;

    let mut constraints = Vec::new();
    if app.audio.config.show_scope {
        constraints.push(Constraint::Fill(1));
    }
    if app.audio.config.show_spectrum {
        constraints.push(Constraint::Fill(1));
    }
    if app.audio.config.show_lissajous {
        constraints.push(Constraint::Fill(1));
    }
    let has_prelude = !app.project_state.project.prelude.trim().is_empty();
    if has_prelude {
        constraints.push(Constraint::Fill(1));
    }
    if constraints.is_empty() {
        return;
    }

    let areas: Vec<Rect> = Layout::vertical(&constraints).split(area).to_vec();
    let mut idx = 0;

    if app.audio.config.show_scope {
        super::main_view::render_scope(frame, app, areas[idx], Orientation::Horizontal);
        idx += 1;
    }
    if app.audio.config.show_spectrum {
        super::main_view::render_spectrum(frame, app, areas[idx]);
        idx += 1;
    }
    if app.audio.config.show_lissajous {
        super::main_view::render_lissajous(frame, app, areas[idx]);
        idx += 1;
    }
    if has_prelude {
        let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
        super::main_view::render_prelude_preview(frame, app, &user_words, areas[idx]);
    }
}
