use minui::{ColorPair, TabPolicy, Window, cell_width};
use unicode_segmentation::UnicodeSegmentation;

use crate::app::{EditorState, StatusMessageStyle};
use crate::ui::UiStyle;
use crate::ui::widgets::perf::{PerfPopupLayout, perf_popup_layout};
use crate::ui::widgets::popup::{
    PopupChrome, PopupLayout, clip_text_to_cells, draw_popup_frame_at, popup_window_view,
    wrap_text_to_cells,
};

const TOAST_MARGIN_COLS: u16 = 1;
const TOAST_MARGIN_ROWS: u16 = 0;
const TOAST_HORIZONTAL_PADDING: u16 = 1;
const TOAST_VERTICAL_PADDING: u16 = 0;
const TOAST_MAX_TEXT_WIDTH: usize = 40;
const TOAST_GAP_ROWS: u16 = 0;
const TOAST_TAB_POLICY: TabPolicy = TabPolicy::Fixed(4);

pub fn draw_status_toast(
    state: &EditorState,
    style: UiStyle,
    window: &mut dyn Window,
) -> minui::Result<Option<PopupLayout>> {
    let Some(message) = &state.status_msg else {
        return Ok(None);
    };

    let (term_w, term_h) = window.get_size();
    let perf_popup = status_toast_perf_popup_layout(state, term_w, term_h, style);
    let Some(toast) = toast_layout(
        message,
        &state.status_msg_line_styles,
        term_w,
        term_h,
        perf_popup,
    ) else {
        return Ok(None);
    };

    let layout = draw_popup_frame_at(
        window,
        toast.x,
        toast.y,
        toast.inner_w,
        toast.inner_h,
        "",
        PopupChrome::command_line(style),
    )?;
    let mut view = popup_window_view(window, layout);
    for (row, line) in toast.lines.iter().enumerate() {
        if row as u16 >= toast.inner_h {
            break;
        }
        write_toast_line(
            &mut view,
            row as u16 + TOAST_VERTICAL_PADDING,
            TOAST_HORIZONTAL_PADDING,
            &line.text,
            line.style,
            style,
        )?;
    }
    Ok(Some(layout))
}

fn status_toast_perf_popup_layout(
    state: &EditorState,
    term_w: u16,
    term_h: u16,
    style: UiStyle,
) -> Option<PerfPopupLayout> {
    let perf_popup_visible = state.perf_popup().is_some() && !state.mode.has_popup_overlay();
    perf_popup_visible.then(|| perf_popup_layout(term_w, term_h, style))
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ToastLayout {
    x: u16,
    y: u16,
    inner_w: u16,
    inner_h: u16,
    lines: Vec<ToastLine>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ToastLine {
    text: String,
    style: StatusMessageStyle,
}

fn toast_layout(
    message: &str,
    line_styles: &[StatusMessageStyle],
    term_w: u16,
    term_h: u16,
    perf_popup: Option<PerfPopupLayout>,
) -> Option<ToastLayout> {
    if term_w <= 2 || term_h <= 2 {
        return None;
    }

    if let Some(perf) = perf_popup {
        if let Some(layout) = toast_layout_below_perf(message, line_styles, term_w, term_h, perf) {
            return Some(layout);
        }
        if let Some(layout) = toast_layout_left_of_perf(message, line_styles, term_h, perf) {
            return Some(layout);
        }
    }

    let y = TOAST_MARGIN_ROWS.min(term_h.saturating_sub(3));
    let popup_max_w = term_w.saturating_sub(TOAST_MARGIN_COLS);
    let popup_max_h = term_h.saturating_sub(y).saturating_sub(TOAST_MARGIN_ROWS);
    let (lines, inner_w, inner_h) =
        build_toast_body(message, line_styles, popup_max_w, popup_max_h)?;
    let popup_w = inner_w.saturating_add(2);
    let x = term_w.saturating_sub(popup_w.saturating_add(TOAST_MARGIN_COLS));

    Some(ToastLayout {
        x,
        y,
        inner_w,
        inner_h,
        lines,
    })
}

fn toast_layout_below_perf(
    message: &str,
    line_styles: &[StatusMessageStyle],
    term_w: u16,
    term_h: u16,
    perf_popup: PerfPopupLayout,
) -> Option<ToastLayout> {
    let y = perf_popup
        .y
        .saturating_add(perf_popup.inner_h)
        .saturating_add(2)
        .saturating_add(TOAST_GAP_ROWS);
    let popup_max_w = term_w.saturating_sub(TOAST_MARGIN_COLS);
    let popup_max_h = term_h.saturating_sub(y).saturating_sub(TOAST_MARGIN_ROWS);
    let (lines, inner_w, inner_h) =
        build_toast_body(message, line_styles, popup_max_w, popup_max_h)?;
    let popup_w = inner_w.saturating_add(2);
    let x = term_w.saturating_sub(popup_w.saturating_add(TOAST_MARGIN_COLS));

    Some(ToastLayout {
        x,
        y,
        inner_w,
        inner_h,
        lines,
    })
}

fn toast_layout_left_of_perf(
    message: &str,
    line_styles: &[StatusMessageStyle],
    term_h: u16,
    perf_popup: PerfPopupLayout,
) -> Option<ToastLayout> {
    let y = perf_popup.y;
    let popup_max_w = perf_popup.x.saturating_sub(TOAST_MARGIN_COLS);
    let popup_max_h = term_h.saturating_sub(y).saturating_sub(TOAST_MARGIN_ROWS);
    let (lines, inner_w, inner_h) =
        build_toast_body(message, line_styles, popup_max_w, popup_max_h)?;
    let popup_w = inner_w.saturating_add(2);
    let x = perf_popup.x.saturating_sub(popup_w);

    Some(ToastLayout {
        x,
        y,
        inner_w,
        inner_h,
        lines,
    })
}

fn build_toast_body(
    message: &str,
    line_styles: &[StatusMessageStyle],
    popup_max_w: u16,
    popup_max_h: u16,
) -> Option<(Vec<ToastLine>, u16, u16)> {
    if popup_max_w <= 2 {
        return None;
    }
    if popup_max_h <= 2 {
        return None;
    }
    let inner_max_w = popup_max_w.saturating_sub(2);
    let text_max_w = (inner_max_w
        .saturating_sub(TOAST_HORIZONTAL_PADDING.saturating_mul(2))
        .max(1) as usize)
        .min(TOAST_MAX_TEXT_WIDTH);

    let mut lines = Vec::new();
    for (idx, line) in message.lines().enumerate() {
        let line_style = line_styles
            .get(idx)
            .copied()
            .unwrap_or(StatusMessageStyle::Normal);
        lines.extend(
            wrap_toast_line_to_cells(line, text_max_w)
                .into_iter()
                .map(|text| ToastLine {
                    text,
                    style: line_style,
                }),
        );
    }
    let inner_max_h = popup_max_h.saturating_sub(2);
    let text_max_h = inner_max_h
        .saturating_sub(TOAST_VERTICAL_PADDING.saturating_mul(2))
        .max(1) as usize;
    if lines.len() > text_max_h {
        lines.truncate(text_max_h);
        if let Some(last_line) = lines.last_mut() {
            last_line.text = clip_text_to_cells(&last_line.text, text_max_w.saturating_sub(1));
            last_line.text.push('…');
        }
    }

    let widest_line = lines
        .iter()
        .map(|line| text_cell_width(&line.text))
        .max()
        .unwrap_or(0);
    let inner_w = widest_line as u16 + TOAST_HORIZONTAL_PADDING.saturating_mul(2);
    let inner_h = lines.len() as u16 + TOAST_VERTICAL_PADDING.saturating_mul(2);
    Some((lines, inner_w, inner_h))
}

fn wrap_toast_line_to_cells(line: &str, max_cells: usize) -> Vec<String> {
    let indent_len = line
        .char_indices()
        .find_map(|(idx, ch)| (!ch.is_whitespace()).then_some(idx))
        .unwrap_or(line.len());
    let indent = &line[..indent_len];
    if indent.is_empty() {
        return wrap_text_to_cells(line, max_cells);
    }

    let indent_w = text_cell_width(indent);
    let content_max_w = max_cells.saturating_sub(indent_w).max(1);
    let content = &line[indent_len..];
    let wrapped = wrap_text_to_cells(content, content_max_w);
    wrapped
        .into_iter()
        .map(|line| format!("{indent}{line}"))
        .collect()
}

fn write_toast_line(
    window: &mut dyn Window,
    row: u16,
    col: u16,
    line: &str,
    message_style: StatusMessageStyle,
    style: UiStyle,
) -> minui::Result<()> {
    if message_style == StatusMessageStyle::Dim {
        return window.write_str_colored(
            row,
            col,
            line,
            ColorPair::new(style.theme.dark_gray, style.command_line.text.bg),
        );
    }

    let mut cursor = 0;
    let mut cell_col = col;
    while cursor < line.len() {
        let ch = line[cursor..]
            .chars()
            .next()
            .expect("cursor should be on a character boundary");
        if is_toast_token_delimiter(ch) {
            let segment = ch.to_string();
            window.write_str_colored(row, cell_col, &segment, style.command_line.text)?;
            cell_col = cell_col.saturating_add(text_cell_width(&segment) as u16);
            cursor += ch.len_utf8();
            continue;
        }

        let token_end = line[cursor..]
            .char_indices()
            .find_map(|(idx, ch)| is_toast_token_delimiter(ch).then_some(cursor + idx))
            .unwrap_or(line.len());
        let token = &line[cursor..token_end];
        cell_col = write_toast_token(window, row, cell_col, token, style)?;
        cursor = token_end;
    }

    Ok(())
}

fn write_toast_token(
    window: &mut dyn Window,
    row: u16,
    col: u16,
    token: &str,
    style: UiStyle,
) -> minui::Result<u16> {
    let mut cell_col = col;
    for (segment, is_dir) in toast_token_segments(token) {
        let color = if is_dir {
            style.explorer.directory
        } else {
            style.command_line.text
        };
        window.write_str_colored(row, cell_col, segment, color)?;
        cell_col = cell_col.saturating_add(text_cell_width(segment) as u16);
    }

    Ok(cell_col)
}

fn toast_token_segments(token: &str) -> Vec<(&str, bool)> {
    let mut segments = Vec::new();
    let mut cursor = 0;

    for (idx, ch) in token.char_indices() {
        if ch != '/' {
            continue;
        }

        let end = idx + ch.len_utf8();
        segments.push((&token[cursor..end], true));
        cursor = end;
    }

    if cursor < token.len() {
        segments.push((&token[cursor..], false));
    }

    segments
}

fn is_toast_token_delimiter(ch: char) -> bool {
    ch.is_whitespace() || matches!(ch, ',' | ';' | ':')
}

fn text_cell_width(text: &str) -> usize {
    text.graphemes(true)
        .map(|g| (cell_width(g, TOAST_TAB_POLICY) as usize).max(1))
        .sum()
}

#[cfg(test)]
mod tests {
    use super::{toast_layout, toast_token_segments};
    use crate::app::StatusMessageStyle;
    use crate::ui::widgets::perf::PerfPopupLayout;

    #[test]
    fn toast_layout_anchors_to_top_right() {
        let layout = toast_layout("written", &[], 40, 20, None).expect("toast layout should fit");
        assert_eq!(layout.y, 0);
        assert_eq!(layout.x + layout.inner_w + 2 + 1, 40);
    }

    #[test]
    fn toast_layout_prefers_height_for_long_messages() {
        let layout = toast_layout("abcdefghijklmnopqrstuvwxyz", &[], 30, 20, None)
            .expect("toast layout should fit");
        assert_eq!(layout.inner_w, 27);
        assert!(layout.inner_h > 1);
        assert_eq!(
            layout
                .lines
                .iter()
                .map(|line| line.text.as_str())
                .collect::<Vec<_>>(),
            vec!["abcdefghijklmnopqrstuvwxy", "z"]
        );
    }

    #[test]
    fn toast_layout_respects_perf_popup_offset() {
        let layout = toast_layout(
            "written",
            &[],
            40,
            20,
            Some(PerfPopupLayout {
                x: 20,
                y: 0,
                inner_w: 18,
                inner_h: 6,
            }),
        )
        .expect("toast layout should fit");
        assert_eq!(layout.y, 8);
    }

    #[test]
    fn toast_layout_falls_back_to_left_of_perf_when_below_does_not_fit() {
        let layout = toast_layout(
            "written",
            &[],
            40,
            9,
            Some(PerfPopupLayout {
                x: 20,
                y: 0,
                inner_w: 18,
                inner_h: 6,
            }),
        )
        .expect("toast layout should fit");
        assert_eq!(layout.y, 0);
        assert_eq!(layout.x + layout.inner_w + 2, 20);
    }

    #[test]
    fn toast_layout_preserves_styles_for_wrapped_source_lines() {
        let layout = toast_layout(
            "normal words\n   prompt words",
            &[StatusMessageStyle::Normal, StatusMessageStyle::Dim],
            16,
            20,
            None,
        )
        .expect("toast layout should fit");

        assert_eq!(
            layout
                .lines
                .iter()
                .map(|line| (line.text.as_str(), line.style))
                .collect::<Vec<_>>(),
            vec![
                ("normal", StatusMessageStyle::Normal),
                ("words", StatusMessageStyle::Normal),
                ("   prompt", StatusMessageStyle::Dim),
                ("   words", StatusMessageStyle::Dim),
            ]
        );
    }

    #[test]
    fn toast_token_segments_mark_directory_prefixes() {
        assert_eq!(
            toast_token_segments("nested/file.txt"),
            vec![("nested/", true), ("file.txt", false)]
        );
        assert_eq!(
            toast_token_segments("one/two/three.txt"),
            vec![("one/", true), ("two/", true), ("three.txt", false)]
        );
        assert_eq!(toast_token_segments("nested/"), vec![("nested/", true)]);
    }
}
