use std::{
    convert::Infallible,
    fmt::{self, Display},
    iter::repeat_n,
};

use egui::{
    Color32, InputState, Key, Modifiers, TextBuffer, TextEdit, Ui, Vec2, text::CCursorRange,
};

use crate::markdown::MemoizedHighlighter;

#[derive(Default, serde::Deserialize, serde::Serialize)]
pub struct MdTextEdit {
    pub text: String,

    #[serde(skip)]
    highlighter: MemoizedHighlighter,

    #[serde(skip)]
    focused: bool,

    #[serde(skip)]
    cursor: Option<CCursorRange>,
}

pub struct MdTextEditOutput {
    pub changed: bool,
}

impl MdTextEdit {
    pub fn new() -> Self {
        MdTextEdit::default()
    }

    pub fn from_text(text: String) -> Self {
        MdTextEdit {
            text,
            ..Default::default()
        }
    }

    pub fn ui(&mut self, ui: &mut Ui) -> MdTextEditOutput {
        let Self {
            text,
            highlighter,
            focused,
            cursor,
        } = self;

        let w = ui.available_width();

        let mut layouter = |ui: &egui::Ui, markdown: &dyn TextBuffer, _wrap_width: f32| {
            let mut layout_job = highlighter.highlight(ui.style(), markdown.as_str(), *cursor);
            layout_job.wrap.max_width = w - 10.0;
            ui.fonts(|f| f.layout_job(layout_job))
        };

        if *focused {
            ui.input_mut(|input| {
                handle_tab_input(text, input, *cursor);
            });
        }

        let text_edit = TextEdit::multiline(text)
            .layouter(&mut layouter)
            .background_color(Color32::TRANSPARENT)
            .desired_rows(1)
            .min_size(Vec2::new(w, 0.0))
            .lock_focus(true)
            .show(ui);

        *focused = text_edit.response.has_focus();

        if *cursor != text_edit.cursor_range {
            *cursor = text_edit.cursor_range;
            //ui.ctx().request_repaint();
        }

        MdTextEditOutput {
            changed: text_edit.response.changed(),
        }
    }
}

fn handle_tab_input(
    text: &mut String,
    input: &mut InputState,
    cursor: Option<CCursorRange>,
) -> Option<Infallible> {
    let break_if_not = || ();
    let cursor = cursor.and_then(|c| c.single())?;

    let do_unindent = input.consume_key(Modifiers::SHIFT, Key::Tab);
    let do_indent = input.consume_key(Modifiers::NONE, Key::Tab);

    (do_unindent || do_indent).then(break_if_not)?;

    let row_n = text
        .chars()
        .take(cursor.index)
        .filter(|&c| c == '\n')
        .count();

    let row = text.lines().nth(row_n)?;
    let (indent, content) = row.split_once("- ")?;
    indent.trim().is_empty().then(break_if_not)?;

    let indents = indent.chars().count() / 2;
    let indents = if do_indent {
        indents + 1
    } else if indents == 0 {
        return None;
    } else {
        indents.saturating_sub(1)
    };
    *text = text
        .lines()
        .take(row_n)
        .flat_map(|line| [line, "\n"])
        .chain(repeat_n("  ", indents))
        .chain([format!("- {content}\n").as_str()])
        .chain(text.lines().skip(row_n + 1).flat_map(|line| [line, "\n"]))
        .collect();

    None
}

impl Display for MdTextEdit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.text)
    }
}
