use cagire_ratatui::ListSelect;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
use ratatui::Frame;

use crate::app::App;
use crate::state::{DeviceKind, EngineSection, SettingKind};
use crate::theme;
use crate::state::{ScopeMode, SpectrumMode};
use crate::widgets::{
    render_scroll_indicators, render_section_header, IndicatorAlign, Lissajous, Orientation, Scope,
    Spectrum, SpectrumStyle, Waveform,
};

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

pub fn render(frame: &mut Frame, app: &App, area: Rect) {
    let [left_col, _, right_col] = layout(area);

    render_settings_section(frame, app, left_col);
    render_visualizers(frame, app, right_col);
}

fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .title(" Engine ")
        .border_style(Style::new().fg(theme.engine.border_magenta));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let padded = Rect {
        x: inner.x + 1,
        y: inner.y + 1,
        width: inner.width.saturating_sub(2),
        height: inner.height.saturating_sub(1),
    };

    // Calculate section heights
    let intro_lines: usize = 3;
    let plugin_mode = app.plugin_mode;
    let devices_lines = if plugin_mode {
        0
    } else {
        devices_section_height(app) as usize
    };
    let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
    let sample_content = app.audio.config.sample_paths.len().max(2); // at least 2 for empty message
    let samples_lines: usize = 2 + sample_content; // header(2) + content

    let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
    let total_lines = intro_lines + 1 + devices_lines + settings_lines + samples_lines + sections_gap;

    let max_visible = padded.height as usize;

    // Calculate scroll offset based on focused section
    let intro_offset = intro_lines + 1;
    let settings_start = if plugin_mode { intro_offset } else { intro_offset + devices_lines + 1 };
    let (focus_start, focus_height) = match app.audio.section {
        EngineSection::Devices => (intro_offset, devices_lines),
        EngineSection::Settings => (settings_start, settings_lines),
        EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
    };

    let scroll_offset = if total_lines <= max_visible {
        0
    } else {
        let focus_end = focus_start + focus_height;
        if focus_end <= max_visible {
            0
        } else {
            focus_start.min(total_lines.saturating_sub(max_visible))
        }
    };

    let viewport_top = padded.y as i32;
    let viewport_bottom = (padded.y + padded.height) as i32;

    let mut y = viewport_top - scroll_offset as i32;

    // Intro text
    let intro_top = y;
    let intro_bottom = y + intro_lines as i32;
    if intro_bottom > viewport_top && intro_top < viewport_bottom {
        let clipped_y = intro_top.max(viewport_top) as u16;
        let clipped_height =
            (intro_bottom.min(viewport_bottom) - intro_top.max(viewport_top)) as u16;
        let intro_area = Rect {
            x: padded.x,
            y: clipped_y,
            width: padded.width,
            height: clipped_height,
        };
        let dim = Style::new().fg(theme.engine.dim);
        let intro = Paragraph::new(vec![
            Line::from(Span::styled("  Audio devices, settings, and sample paths.", dim)),
            Line::from(Span::styled("  Supports .wav, .ogg, .mp3 samples and .sf2 soundfonts.", dim)),
            Line::from(Span::styled("  Press R to restart the audio engine after changes.", dim)),
        ]);
        frame.render_widget(intro, intro_area);
    }
    y += intro_lines as i32 + 1;

    // Devices section (skip in plugin mode)
    if !plugin_mode {
        let devices_top = y;
        let devices_bottom = y + devices_lines as i32;
        if devices_bottom > viewport_top && devices_top < viewport_bottom {
            let clipped_y = devices_top.max(viewport_top) as u16;
            let clipped_height =
                (devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
            let devices_area = Rect {
                x: padded.x,
                y: clipped_y,
                width: padded.width,
                height: clipped_height,
            };
            render_devices(frame, app, devices_area);
        }
        y += devices_lines as i32 + 1;
    }

    // Settings section
    let settings_top = y;
    let settings_bottom = y + settings_lines as i32;
    if settings_bottom > viewport_top && settings_top < viewport_bottom {
        let clipped_y = settings_top.max(viewport_top) as u16;
        let clipped_height =
            (settings_bottom.min(viewport_bottom) - settings_top.max(viewport_top)) as u16;
        let settings_area = Rect {
            x: padded.x,
            y: clipped_y,
            width: padded.width,
            height: clipped_height,
        };
        render_settings(frame, app, settings_area);
    }
    y += settings_lines as i32 + 1;

    // Samples section
    let samples_top = y;
    let samples_bottom = y + samples_lines as i32;
    if samples_bottom > viewport_top && samples_top < viewport_bottom {
        let clipped_y = samples_top.max(viewport_top) as u16;
        let clipped_height =
            (samples_bottom.min(viewport_bottom) - samples_top.max(viewport_top)) as u16;
        let samples_area = Rect {
            x: padded.x,
            y: clipped_y,
            width: padded.width,
            height: clipped_height,
        };
        render_samples(frame, app, samples_area);
    }

    render_scroll_indicators(
        frame,
        padded,
        scroll_offset,
        max_visible,
        total_lines,
        theme.engine.scroll_indicator,
        IndicatorAlign::Right,
    );
}

fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
    let [scope_area, _, lissajous_area, _gap, spectrum_area] = Layout::vertical([
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Fill(1),
        Constraint::Length(1),
        Constraint::Fill(1),
    ])
    .areas(area);

    render_scope(frame, app, scope_area);
    render_lissajous(frame, app, lissajous_area);
    render_spectrum(frame, app, spectrum_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
    }
}

fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .title(" Scope ")
        .border_style(Style::new().fg(theme.engine.border_green));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let orientation = if app.audio.config.scope_vertical {
        Orientation::Vertical
    } else {
        Orientation::Horizontal
    };
    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);
        }
    }
}

fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .title(" Lissajous ")
        .border_style(Style::new().fg(theme.engine.border_green));

    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_spectrum(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let block = Block::default()
        .borders(Borders::ALL)
        .title(" Spectrum ")
        .border_style(Style::new().fg(theme.engine.border_cyan));

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

fn truncate_name(name: &str, max_len: usize) -> String {
    if name.len() > max_len {
        format!("{}...", &name[..max_len.saturating_sub(3)])
    } else {
        name.to_string()
    }
}

pub fn list_height(item_count: usize) -> u16 {
    let visible = item_count.min(5) as u16;
    if item_count > 5 {
        visible + 1
    } else {
        visible
    }
}

pub fn devices_section_height(app: &App) -> u16 {
    let output_h = list_height(app.audio.output_devices.len());
    let input_h = list_height(app.audio.input_devices.len());
    3 + output_h.max(input_h)
}

fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let section_focused = app.audio.section == EngineSection::Devices;

    let [header_area, content_area] =
        Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area);

    render_section_header(frame, "DEVICES", section_focused, header_area);

    let [output_col, separator, input_col] = Layout::horizontal([
        Constraint::Percentage(48),
        Constraint::Length(3),
        Constraint::Percentage(48),
    ])
    .areas(content_area);

    let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output;
    let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;

    render_device_column(
        frame,
        output_col,
        "Output",
        &app.audio.output_devices,
        app.audio.current_output_device_index(),
        app.audio.output_list.cursor,
        app.audio.output_list.scroll_offset,
        output_focused,
        section_focused,
    );

    let sep_style = Style::new().fg(theme.engine.separator);
    let sep_lines: Vec<Line> = (0..separator.height)
        .map(|_| Line::from(Span::styled("│", sep_style)))
        .collect();
    frame.render_widget(Paragraph::new(sep_lines), separator);

    render_device_column(
        frame,
        input_col,
        "Input",
        &app.audio.input_devices,
        app.audio.current_input_device_index(),
        app.audio.input_list.cursor,
        app.audio.input_list.scroll_offset,
        input_focused,
        section_focused,
    );
}

#[allow(clippy::too_many_arguments)]
fn render_device_column(
    frame: &mut Frame,
    area: Rect,
    label: &str,
    devices: &[doux::audio::AudioDeviceInfo],
    selected_idx: usize,
    cursor: usize,
    scroll_offset: usize,
    focused: bool,
    section_focused: bool,
) {
    let theme = theme::get();
    let [label_area, list_area] =
        Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);

    let label_style = if focused {
        Style::new()
            .fg(theme.engine.focused)
            .add_modifier(Modifier::BOLD)
    } else if section_focused {
        Style::new().fg(theme.engine.label_focused)
    } else {
        Style::new().fg(theme.engine.label_dim)
    };

    let arrow = if focused { "> " } else { "  " };
    frame.render_widget(
        Paragraph::new(format!("{arrow}{label}")).style(label_style),
        label_area,
    );

    let items: Vec<String> = devices.iter().map(|d| truncate_name(&d.name, 25)).collect();

    ListSelect::new(&items, selected_idx, cursor)
        .focused(focused)
        .scroll_offset(scroll_offset)
        .render(frame, list_area);
}

fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let section_focused = app.audio.section == EngineSection::Settings;

    let [header_area, content_area] =
        Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area);

    render_section_header(frame, "SETTINGS", section_focused, header_area);

    let highlight = Style::new()
        .fg(theme.engine.focused)
        .add_modifier(Modifier::BOLD);
    let normal = Style::new().fg(theme.engine.normal);
    let label_style = Style::new().fg(theme.engine.label);
    let value_style = Style::new().fg(theme.engine.value);

    let polyphony_focused = section_focused && app.audio.setting_kind == SettingKind::Polyphony;
    let nudge_focused = section_focused && app.audio.setting_kind == SettingKind::Nudge;
    let nudge_ms = app.metrics.nudge_ms;
    let nudge_label = if nudge_ms == 0.0 {
        "0 ms".to_string()
    } else {
        format!("{nudge_ms:+.1} ms")
    };

    let mut rows = Vec::new();

    if !app.plugin_mode {
        let channels_focused =
            section_focused && app.audio.setting_kind == SettingKind::Channels;
        let buffer_focused =
            section_focused && app.audio.setting_kind == SettingKind::BufferSize;

        rows.push(Row::new(vec![
            Span::styled(
                if channels_focused {
                    "> Channels"
                } else {
                    "  Channels"
                },
                label_style,
            ),
            render_selector(
                &format!("{}", app.audio.config.channels),
                channels_focused,
                highlight,
                normal,
            ),
        ]));
        rows.push(Row::new(vec![
            Span::styled(
                if buffer_focused {
                    "> Buffer"
                } else {
                    "  Buffer"
                },
                label_style,
            ),
            render_selector(
                &if app.audio.config.host_name.to_lowercase().contains("jack") {
                    "JACK managed".to_string()
                } else {
                    format!("{}", app.audio.config.buffer_size)
                },
                buffer_focused,
                highlight,
                normal,
            ),
        ]));
    }

    rows.push(Row::new(vec![
        Span::styled(
            if polyphony_focused {
                "> Voices"
            } else {
                "  Voices"
            },
            label_style,
        ),
        render_selector(
            &format!("{}", app.audio.config.max_voices),
            polyphony_focused,
            highlight,
            normal,
        ),
    ]));
    rows.push(Row::new(vec![
        Span::styled(
            if nudge_focused { "> Nudge" } else { "  Nudge" },
            label_style,
        ),
        render_selector(&nudge_label, nudge_focused, highlight, normal),
    ]));
    rows.push(Row::new(vec![
        Span::styled("  Sample rate", label_style),
        Span::styled(
            format!("{:.0} Hz", app.audio.config.sample_rate),
            value_style,
        ),
    ]));

    if !app.plugin_mode {
        rows.push(Row::new(vec![
            Span::styled("  Audio host", label_style),
            Span::styled(
                if app.audio.config.host_name.is_empty() {
                    "-".to_string()
                } else {
                    app.audio.config.host_name.clone()
                },
                value_style,
            ),
        ]));
    }

    let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
    frame.render_widget(table, content_area);
}

fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
    let theme = theme::get();
    let section_focused = app.audio.section == EngineSection::Samples;

    let [header_area, content_area] = Layout::vertical([
        Constraint::Length(2),
        Constraint::Min(1),
    ])
    .areas(area);

    let path_count = app.audio.config.sample_paths.len();
    let sample_count: usize = app.audio.total_sample_count();
    let header_text = format!("SAMPLES  {path_count} paths · {sample_count} indexed");
    render_section_header(frame, &header_text, section_focused, header_area);

    let dim = Style::new().fg(theme.engine.dim);
    let path_style = Style::new().fg(theme.engine.path);
    let cursor_style = Style::new()
        .fg(theme.engine.focused)
        .add_modifier(Modifier::BOLD);

    let cursor = app.audio.sample_list.cursor;
    let scroll_offset = app.audio.sample_list.scroll_offset;
    let visible_rows = content_area.height as usize;

    let mut lines: Vec<Line> = Vec::new();
    if app.audio.config.sample_paths.is_empty() {
        lines.push(Line::from(Span::styled(
            "  No sample paths configured",
            dim,
        )));
        lines.push(Line::from(Span::styled(
            "  Add folders containing audio files",
            dim,
        )));
    } else {
        for (i, path) in app
            .audio
            .config
            .sample_paths
            .iter()
            .enumerate()
            .skip(scroll_offset)
            .take(visible_rows)
        {
            let is_cursor = section_focused && i == cursor;
            let prefix = if is_cursor { "> " } else { "  " };
            let count = app.audio.config.sample_counts.get(i).copied().unwrap_or(0);
            let path_str = path.to_string_lossy();
            let count_str = format!("  ({count})");
            let max_path = (content_area.width as usize)
                .saturating_sub(prefix.len() + count_str.len());
            let display = truncate_name(&path_str, max_path);
            let style = if is_cursor { cursor_style } else { path_style };
            lines.push(Line::from(vec![
                Span::styled(prefix.to_string(), if is_cursor { cursor_style } else { dim }),
                Span::styled(display, style),
                Span::styled(count_str, dim),
            ]));
        }
    }
    frame.render_widget(Paragraph::new(lines), content_area);
}

fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
    let style = if focused { highlight } else { normal };
    if focused {
        Span::styled(format!("< {value} >"), style)
    } else {
        Span::styled(format!("  {value}  "), style)
    }
}
