#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlineStyle {
    Bold,
    Italic,
}

#[derive(Debug, Clone)]
pub enum ListStyle {
    Numbered(usize),
    Bullet(char),
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct InlineStyles {
    pub bold: bool,
    pub italic: bool,
}

pub trait Typesetter {
    fn push_list(&mut self, style: ListStyle);
    fn pop_list(&mut self);

    fn push_list_item(&mut self);
    fn pop_list_item(&mut self);

    fn push_paragraph(&mut self);
    fn pop_paragraph(&mut self);

    fn push_code_block(&mut self);
    fn pop_code_block(&mut self);

    fn push_blockquote(&mut self);
    fn pop_blockquote(&mut self);

    fn push_style(&mut self, style: InlineStyle);
    fn pop_style(&mut self);

    fn append_text(&mut self, space: bool, text: &str);
    fn finish_line(&mut self);
}

#[derive(Debug, Default, Clone)]
struct Token {
    text: String,
    styles: InlineStyles,
}

#[derive(Debug, Default, Clone)]
struct Word {
    tokens: Vec<Token>,
}

#[derive(Debug, Default, Clone)]
struct LineBuilder {
    prefix: String,
    blockquote: usize,
    words: Vec<Word>,
    word: Word,
    indent: usize,
    full: bool,
}

impl Token {
    fn width(&self) -> usize {
        self.text.chars().count()
    }
}

impl Word {
    fn width(&self) -> usize {
        self.tokens.iter().map(Token::width).sum()
    }

    fn is_empty(&self) -> bool {
        self.tokens.is_empty()
    }

    fn push(&mut self, ch: char, styles: InlineStyles) {
        if let Some(last) = self.tokens.last_mut() {
            if last.styles == styles {
                last.text.push(ch);
                return;
            }
        }
        let mut token = Token {
            text: String::new(),
            styles,
        };
        token.text.push(ch);
        self.tokens.push(token);
    }
}

impl LineBuilder {
    fn width(&self, blockquote_width: usize) -> usize {
        self.prefix.chars().count()
            + self.indent * 2
            + self.words.iter().map(Word::width).sum::<usize>()
            + self.words.len()
            + self.word.width()
            + self.blockquote * blockquote_width
            + 2
    }

    fn is_empty(&self) -> bool {
        self.words.is_empty()
    }

    fn finish_word(&mut self) {
        let word = std::mem::take(&mut self.word);
        if !word.is_empty() {
            self.words.push(word);
        }
    }

    fn push(&mut self, ch: char, styles: InlineStyles) {
        if ch == ' ' {
            // Word break
            self.finish_word();
            return;
        }

        self.word.push(ch, styles);
    }
}

pub struct TerminalPrinter {
    style: InlineStyles,
    cursor: usize,
    margin_left: usize,

    prefix_style: Option<InlineStyles>,
    verbatim_style: Option<InlineStyles>,
    blockquote_prefix: &'static str,
    verbatim_prefix: &'static str,
}

impl Default for TerminalPrinter {
    fn default() -> Self {
        Self {
            style: InlineStyles::default(),
            margin_left: 0,
            cursor: 0,
            prefix_style: Some(InlineStyles {
                bold: true,
                italic: false,
            }),
            verbatim_style: Some(InlineStyles {
                bold: true,
                italic: true,
            }),
            blockquote_prefix: "║ ",
            verbatim_prefix: "│ ",
        }
    }
}

impl TerminalPrinter {
    fn reset_style(&mut self) {
        print!("\x1B[0m");
        self.style = InlineStyles::default();
    }

    fn set_style(&mut self, styles: InlineStyles) {
        // Update styles
        if styles.bold != self.style.bold {
            if styles.bold {
                print!("\x1B[1m");
            } else {
                print!("\x1B[22m");
            }
        }
        if styles.italic != self.style.italic {
            if styles.italic {
                print!("\x1B[3m");
            } else {
                print!("\x1B[23m");
            }
        }

        self.style = styles;
    }

    fn print_whitespace(&mut self, count: usize, styles: InlineStyles) {
        self.set_style(styles);
        for _ in 0..count {
            print!(" ");
        }
        self.cursor += count;
    }

    fn print_token(&mut self, token: &Token) {
        self.set_style(token.styles);
        print!("{}", token.text);
        self.cursor += token.text.chars().count();
    }

    fn print_word(&mut self, word: &Word) {
        word.tokens.iter().for_each(|token| self.print_token(token));
    }

    fn print_prefix(&mut self, prefix: &str) {
        if let Some(prefix_style) = self.prefix_style {
            self.set_style(prefix_style);
            print!("{prefix}");
            self.reset_style();
        } else {
            print!("{prefix}");
        }

        self.cursor += prefix.chars().count();
    }

    fn print_blockquote(&mut self, prefix: &str, depth: usize) {
        for _ in 0..depth {
            print!("{prefix}");
            self.cursor += prefix.chars().count();
        }
    }

    fn print_line(&mut self, line: &LineBuilder, page_width: usize) {
        self.cursor = 0;
        self.print_blockquote(self.blockquote_prefix, line.blockquote);
        self.print_whitespace(self.margin_left + line.indent * 2, Default::default());
        self.print_prefix(&line.prefix);
        let page_width = page_width - self.cursor;

        if line.full {
            let line_width = line.words.iter().map(Word::width).sum::<usize>();
            assert!(
                line_width < page_width,
                "Line width: {line_width}, Page width: {page_width}"
            );
            let need_spaces = page_width - line_width;
            let word_breaks = line.words.len() - 1;
            let even_spaces = (need_spaces / word_breaks).max(1);
            let odd_spaces = need_spaces % word_breaks;
            for (i, word) in line.words.iter().enumerate() {
                if i != 0 {
                    let mut spaces = even_spaces;
                    if i - 1 < odd_spaces {
                        spaces += 1;
                    }
                    self.print_whitespace(spaces, word.tokens[0].styles);
                }
                self.print_word(word);
            }
            self.reset_style();
        } else {
            for (i, word) in line.words.iter().enumerate() {
                if i != 0 {
                    self.print_whitespace(1, word.tokens[0].styles);
                }
                self.print_word(word);
            }
        }

        println!();
    }

    fn begin_verbatim(&mut self) {
        print!("{}", self.verbatim_prefix);
        if let Some(style) = self.verbatim_style {
            self.set_style(style);
        }
    }

    fn end_verbatim(&mut self) {
        if self.verbatim_style.is_some() {
            self.reset_style();
        }
    }

    fn print_verbatim(&mut self, ch: char) {
        print!("{ch}");
    }
}

pub struct TerminalTypesetter {
    style_stack: Vec<InlineStyle>,
    list_stack: Vec<ListStyle>,
    line: LineBuilder,
    indent: usize,
    page_width: usize,
    printer: TerminalPrinter,
    last_empty: bool,
    verbatim: bool,
    verbatim_empty: bool,
    blockquote: usize,
}

impl Default for TerminalTypesetter {
    fn default() -> Self {
        Self {
            style_stack: vec![],
            list_stack: vec![],
            line: LineBuilder::default(),
            indent: 0,
            page_width: 80,
            printer: TerminalPrinter::default(),
            last_empty: true,
            verbatim: false,
            verbatim_empty: true,
            blockquote: 0,
        }
    }
}

impl TerminalTypesetter {
    pub fn set_page_width(&mut self, width: usize) {
        self.page_width = width.max(40);
    }

    fn styles(&self) -> InlineStyles {
        let mut styles = InlineStyles::default();
        for style in self.style_stack.iter() {
            match style {
                InlineStyle::Bold => styles.bold = true,
                InlineStyle::Italic => styles.italic = true,
            }
        }
        styles
    }

    fn append_char(&mut self, ch: char) {
        if self.verbatim {
            if self.verbatim_empty {
                self.printer.begin_verbatim();
                for _ in 0..self.indent {
                    print!("  ");
                }
            }
            if ch == '\n' {
                self.printer.end_verbatim();
                self.verbatim_empty = true;
            } else {
                self.verbatim_empty = false;
            }
            self.printer.print_verbatim(ch);
            return;
        }

        if self
            .line
            .width(self.printer.blockquote_prefix.chars().count())
            + 1
            >= self.page_width
        {
            // Finish the line, next word goes to the next line
            self.line.full = true;

            // Ugliness reduction by refusing to take trailing short words
            let popped = self.line.words.pop_if(|word| word.width() < 3);
            let mut word = std::mem::take(&mut self.line.word);
            word.push(ch, self.styles());
            self.finish_line();
            self.line.words.extend(popped);
            self.line.word = word;
            return;
        }

        self.line.push(ch, self.styles());
    }

    fn increment_list_index(&mut self) {
        if let Some(ListStyle::Numbered(index)) = self.list_stack.last_mut() {
            *index += 1;
        }
    }

    fn set_indent(&mut self) {
        let indent = self.indent;
        self.line.indent = indent;
        self.line.blockquote = self.blockquote;
    }

    fn hard_break(&mut self, set_indent: bool) {
        let empty = self.last_empty;
        self.finish_line();
        if !empty && self.blockquote == 0 {
            println!();
            self.last_empty = true;
        }
        if set_indent {
            self.set_indent();
        }
    }
}

impl Typesetter for TerminalTypesetter {
    fn push_style(&mut self, style: InlineStyle) {
        self.style_stack.push(style);
    }

    fn pop_style(&mut self) {
        self.style_stack.pop();
    }

    fn append_text(&mut self, space: bool, text: &str) {
        let space = space && !self.verbatim;
        if space {
            self.append_char(' ');
        }
        for ch in text.chars() {
            self.append_char(ch);
        }
    }

    fn finish_line(&mut self) {
        self.line.finish_word();

        if !self.line.is_empty() {
            let line = std::mem::take(&mut self.line);
            self.last_empty = false;
            self.printer.print_line(&line, self.page_width);
            self.set_indent();
        } else {
            self.last_empty = true;
        }
    }

    fn push_list(&mut self, style: ListStyle) {
        self.hard_break(false);
        self.list_stack.push(style);
    }

    fn pop_list(&mut self) {
        self.list_stack.pop();
        self.hard_break(false);
    }

    fn push_list_item(&mut self) {
        self.hard_break(false);
        self.indent += 1;
        if let Some(last) = self.list_stack.last() {
            match *last {
                ListStyle::Bullet(p) => {
                    self.line.prefix.push(p);
                }
                ListStyle::Numbered(n) => {
                    self.line.prefix.push_str(&format!("{n}."));
                }
            }
            self.line.prefix.push(' ');
        }
    }

    fn pop_list_item(&mut self) {
        self.increment_list_index();
        self.indent -= 1;
        self.hard_break(true);
    }

    fn push_paragraph(&mut self) {
        self.hard_break(false);
    }

    fn pop_paragraph(&mut self) {
        self.hard_break(true);
    }

    fn push_code_block(&mut self) {
        self.hard_break(false);
        self.verbatim_empty = true;
        self.verbatim = true;
    }

    fn pop_code_block(&mut self) {
        println!();
        self.verbatim = false;
    }

    fn push_blockquote(&mut self) {
        self.blockquote += 1;
        self.hard_break(true);
    }

    fn pop_blockquote(&mut self) {
        self.blockquote -= 1;
        if self.blockquote == 0 {
            println!();
        }
        self.line.prefix.clear();
        self.hard_break(true);
    }
}
