use std::cell::Cell;
use std::io::{self, Write};

use anstyle::{AnsiColor, Effects, Style};

use crate::markdown::{MdStream, MdTree};

const DEFAULT_COLUMN_WIDTH: usize = 140;

thread_local! {
    /// Track the position of viewable characters in our buffer
    static CURSOR: Cell<usize> = const { Cell::new(0) };
    /// Width of the terminal
    static WIDTH: Cell<usize> = const { Cell::new(DEFAULT_COLUMN_WIDTH) };

}

/// Print to the terminal output to a buffer
/// optionally with a formatter for code blocks
pub(crate) fn entrypoint(
    stream: &MdStream<'_>,
    buf: &mut Vec<u8>,
    formatter: Option<&(dyn Fn(&str, &mut Vec<u8>) -> io::Result<()> + 'static)>,
) -> io::Result<()> {
    write_stream(stream, buf, None, 0, formatter)?;
    buf.write_all(b"\n")
}

/// Write the buffer, reset to the default style after each,
/// optionally with a formatter for code blocks
fn write_stream(
    MdStream(stream): &MdStream<'_>,
    buf: &mut Vec<u8>,

    default: Option<Style>,
    indent: usize,
    formatter: Option<&(dyn Fn(&str, &mut Vec<u8>) -> io::Result<()> + 'static)>,
) -> io::Result<()> {
    for tt in stream {
        write_tt(tt, buf, default, indent, formatter)?;
    }
    Ok(())
}

fn write_tt(
    tt: &MdTree<'_>,
    buf: &mut Vec<u8>,
    default: Option<Style>,
    indent: usize,
    formatter: Option<&(dyn Fn(&str, &mut Vec<u8>) -> io::Result<()> + 'static)>,
) -> io::Result<()> {
    match tt {
        MdTree::CodeBlock { txt, lang: _ } => {
            reset_opt_style(buf, default)?;
            if let Some(formatter) = formatter {
                formatter(txt, buf)?;
            } else {
                let style = Style::new().effects(Effects::DIMMED);
                write!(buf, "{style}{txt}{style:#}")?;
            }
            render_opt_style(buf, default)?;
        }
        MdTree::CodeInline(txt) => {
            reset_opt_style(buf, default)?;
            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::DIMMED)))?;
            render_opt_style(buf, default)?;
        }
        MdTree::Strong(txt) => {
            reset_opt_style(buf, default)?;
            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::BOLD)))?;
            render_opt_style(buf, default)?;
        }
        MdTree::Emphasis(txt) => {
            reset_opt_style(buf, default)?;
            write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::ITALIC)))?;
            render_opt_style(buf, default)?;
        }
        MdTree::Strikethrough(txt) => {
            reset_opt_style(buf, default)?;
            write_wrapping(
                buf,
                txt,
                indent,
                None,
                Some(Style::new().effects(Effects::STRIKETHROUGH)),
            )?;
            render_opt_style(buf, default)?;
        }
        MdTree::PlainText(txt) => {
            write_wrapping(buf, txt, indent, None, None)?;
        }
        MdTree::Link { disp, link } => {
            write_wrapping(buf, disp, indent, Some(link), None)?;
        }
        MdTree::ParagraphBreak => {
            buf.write_all(b"\n\n")?;
            reset_cursor();
        }
        MdTree::LineBreak => {
            buf.write_all(b"\n")?;
            reset_cursor();
        }
        MdTree::HorizontalRule => {
            (0..WIDTH.get()).for_each(|_| buf.write_all(b"-").unwrap());
            reset_cursor();
        }
        MdTree::Heading(n, stream) => {
            let cs = match n {
                1 => AnsiColor::BrightCyan.on_default().effects(Effects::BOLD | Effects::UNDERLINE),
                2 => AnsiColor::BrightCyan.on_default().effects(Effects::UNDERLINE),
                3 => AnsiColor::BrightCyan.on_default().effects(Effects::ITALIC),
                4.. => AnsiColor::Cyan.on_default().effects(Effects::UNDERLINE | Effects::ITALIC),
                0 => unreachable!(),
            };
            reset_opt_style(buf, default)?;
            write!(buf, "{cs}")?;
            write_stream(stream, buf, Some(cs), 0, None)?;
            write!(buf, "{cs:#}")?;
            render_opt_style(buf, default)?;
            buf.write_all(b"\n")?;
        }
        MdTree::OrderedListItem(n, stream) => {
            let base = format!("{n}. ");
            write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
            write_stream(stream, buf, None, indent + 4, None)?;
        }
        MdTree::UnorderedListItem(stream) => {
            let base = "* ";
            write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
            write_stream(stream, buf, None, indent + 4, None)?;
        }
        // Patterns popped in previous step
        MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(),
    }

    Ok(())
}

fn render_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
    if let Some(style) = &style {
        write!(buf, "{style}")?;
    }
    Ok(())
}

fn reset_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
    if let Some(style) = &style {
        write!(buf, "{style:#}")?;
    }
    Ok(())
}

/// End of that block, just wrap the line
fn reset_cursor() {
    CURSOR.set(0);
}

/// Change to be generic on Write for testing. If we have a link URL, we don't
/// count the extra tokens to make it clickable.
fn write_wrapping(
    buf: &mut Vec<u8>,
    text: &str,
    indent: usize,
    link_url: Option<&str>,
    style: Option<Style>,
) -> io::Result<()> {
    render_opt_style(buf, style)?;

    let ind_ws = &b"          "[..indent];
    let mut to_write = text;
    if let Some(url) = link_url {
        // This is a nonprinting prefix so we don't increment our cursor
        write!(buf, "\x1b]8;;{url}\x1b\\")?;
    }
    CURSOR.with(|cur| {
        loop {
            if cur.get() == 0 {
                buf.write_all(ind_ws)?;
                cur.set(indent);
            }
            let ch_count = WIDTH.get() - cur.get();
            let mut iter = to_write.char_indices();
            let Some((end_idx, _ch)) = iter.nth(ch_count) else {
                // Write entire line
                buf.write_all(to_write.as_bytes())?;
                cur.set(cur.get() + to_write.chars().count());
                break;
            };

            if let Some((break_idx, ch)) = to_write[..end_idx]
                .char_indices()
                .rev()
                .find(|(_idx, ch)| ch.is_whitespace() || ['_', '-'].contains(ch))
            {
                // Found whitespace to break at
                if ch.is_whitespace() {
                    writeln!(buf, "{}", &to_write[..break_idx])?;
                    to_write = to_write[break_idx..].trim_start();
                } else {
                    // Break at a `-` or `_` separator
                    writeln!(buf, "{}", &to_write.get(..break_idx + 1).unwrap_or(to_write))?;
                    to_write = to_write.get(break_idx + 1..).unwrap_or_default().trim_start();
                }
            } else {
                // No whitespace, we need to just split
                let ws_idx =
                    iter.find(|(_, ch)| ch.is_whitespace()).map_or(to_write.len(), |(idx, _)| idx);
                writeln!(buf, "{}", &to_write[..ws_idx])?;
                to_write = to_write.get(ws_idx + 1..).map_or("", str::trim_start);
            }
            cur.set(0);
        }
        if link_url.is_some() {
            buf.write_all(b"\x1b]8;;\x1b\\")?;
        }
        reset_opt_style(buf, style)?;
        Ok(())
    })
}

#[cfg(test)]
#[path = "tests/term.rs"]
mod tests;
