//! Internal formatting specification
use std::convert::identity;
use std::fmt::Write;

use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::{anychar, char, one_of, satisfy};
use nom::combinator::{
    cond, cut, eof, flat_map, map, map_opt, not, opt, peek, recognize, value,
    verify,
};
use nom::multi::{count, many_m_n, many_till, many1};
use nom::sequence::{pair, preceded, terminated};
use nom::{Finish, IResult, Parser};

use super::{Color, Modifier};

pub fn encode(text: &str, markdown_only: bool) -> String {
    let Some(tokens) = parse(text, markdown_only) else {
        return text.to_string();
    };

    let mut out = String::with_capacity(irc::proto::format::BYTE_LIMIT);

    for token in tokens {
        token.encode(&mut out);
    }

    out
}

fn parse(input: &str, markdown_only: bool) -> Option<Vec<Token>> {
    let mut tokens = cut(many_till(|i| token(input, i, markdown_only), eof));

    tokens
        .parse(input)
        .finish()
        .ok()
        .map(|(_, (tokens, _))| tokens)
}

fn token<'a>(
    source: &'a str,
    input: &'a str,
    markdown_only: bool,
) -> IResult<&'a str, Token> {
    alt((
        map(escaped(markdown_only), Token::Escaped),
        map(markdown(source, markdown_only), Token::Markdown),
        skip(markdown_only, map(dollar, Token::Dollar)),
        map(anychar, Token::Plain),
    ))
    .parse(input)
}

fn escaped<'a>(
    markdown_only: bool,
) -> impl Parser<&'a str, Output = char, Error = nom::error::Error<&'a str>> {
    alt((
        value('\\', terminated(tag("\\"), not(one_of("*_`|")))),
        value('*', tag("\\*")),
        value('_', tag("\\_")),
        value('`', tag("\\`")),
        value('|', tag("\\|")),
        skip(markdown_only, value('$', tag("\\$"))),
        skip(markdown_only, value('$', tag("$$"))),
    ))
}

fn skip<'a, O, P>(
    skip: bool,
    inner: P,
) -> impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>
where
    P: Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>,
{
    map_opt(cond(!skip, inner), identity)
}

// [CommonMark](https://spec.commonmark.org/) is the reference specification for
// markdown parsing; not all features can/should be supported, but supported
// features should follow the specification as closely as possible.
fn markdown<'a>(
    source: &'a str,
    markdown_only: bool,
) -> impl Parser<&'a str, Output = Markdown, Error = nom::error::Error<&'a str>>
{
    // EOF is considered WS in common mark spec
    let ws =
        |i| alt((satisfy(char::is_whitespace), map(eof, |_| ' '))).parse(i);
    let punc =
        |d| move |i| satisfy(|c| c != d && c.is_ascii_punctuation()).parse(i);

    // Previous consumed character
    let prev = |i: &str| source[..source.len() - i.len()].chars().last();
    // Prev char is whitespace or punctuation, not matching delimiter char
    let prev_is_ws_or_punc = move |d, i: &str| {
        // None == Start of input which is considered whitespace
        prev(i).is_none_or(|c| {
            c != d && (c.is_whitespace() || c.is_ascii_punctuation())
        })
    };
    // Prev char is punctuation, not matching delimiter char
    let prev_is_punc = move |d, i: &str| {
        prev(i).is_some_and(|c| c != d && c.is_ascii_punctuation())
    };

    // A delimiter run is a sequence of one or more characters
    let delimiter_run = |d, n| terminated(count(char(d), n), not(char(d)));

    // A left-flanking delimiter run is a
    let left_flanking = move |d, n| {
        move |i: &'a str| {
            alt((
                // delimiter run that is not followed by Unicode whitespace or punctuation
                map(
                    pair(delimiter_run(d, n), peek(not(alt((ws, punc(d)))))),
                    move |_| prev(i),
                ),
                // OR delimiter run followed by a Unicode punctuation character and preceded
                // by Unicode whitespace or a Unicode punctuation character
                map(
                    (
                        verify(delimiter_run(d, n), move |_: &Vec<_>| {
                            prev_is_ws_or_punc(d, i)
                        }),
                        peek(punc(d)),
                    ),
                    move |_| prev(i),
                ),
            ))
            .parse(i)
        }
    };
    // A right-flanking delimiter run is a
    let right_flanking = move |d, n| {
        move |i: &'a str| {
            alt((
                // delimiter run that is not preceded by Unicode whitespace or punctuation
                map(
                    verify(delimiter_run(d, n), move |_: &Vec<_>| {
                        !prev_is_ws_or_punc(d, i)
                    }),
                    move |_| prev(i),
                ),
                // OR delimiter run that is preceded by a Unicode punctuation character and
                // followed by Unicode whitespace or a Unicode punctuation character
                map(
                    pair(
                        verify(delimiter_run(d, n), move |_: &Vec<_>| {
                            prev_is_punc(d, i)
                        }),
                        peek(alt((ws, punc(d)))),
                    ),
                    move |_| prev(i),
                ),
            ))
            .parse(i)
        }
    };

    // can open emphasis if it is part of a left-flanking delimiter run
    let open_emphasis_relaxed = left_flanking;
    // can open emphasis if it is part of a right-flanking delimiter run
    let close_emphasis_relaxed = right_flanking;

    // can open emphasis if it is
    let open_emphasis_strict = |d, n| {
        preceded(
            peek(alt((
                // part of a left-flanking delimiter run and not part of a right-flanking delimiter run
                map(
                    pair(peek(left_flanking(d, n)), not(right_flanking(d, n))),
                    |_| (),
                ),
                // OR part of a left-flanking delimiter run and part of a right-flanking delimiter run
                // preceded by a Unicode punctuation character
                map(
                    pair(
                        peek(left_flanking(d, n)),
                        verify(
                            right_flanking(d, n),
                            move |c: &Option<char>| {
                                c.is_some_and(|c| {
                                    c != d && c.is_ascii_punctuation()
                                })
                            },
                        ),
                    ),
                    |_| (),
                ),
            ))),
            left_flanking(d, n),
        )
    };
    // can close emphasis if it is
    let close_emphasis_strict = |d, n| {
        preceded(
            peek(alt((
                // part of a right-flanking delimiter run and not part of a left-flanking delimiter run
                map(
                    pair(peek(right_flanking(d, n)), not(left_flanking(d, n))),
                    |_| (),
                ),
                // OR part of a right-flanking delimiter run and part of a left-flanking delimiter run
                // followed by a Unicode punctuation character
                map(
                    (
                        peek(right_flanking(d, n)),
                        left_flanking(d, n),
                        satisfy(move |c| c != d && c.is_ascii_punctuation()),
                    ),
                    |_| (),
                ),
            ))),
            right_flanking(d, n),
        )
    };

    // open <tokens> close
    let relaxed_run = |d, n| {
        map(
            pair(
                open_emphasis_relaxed(d, n),
                many_till(
                    move |input| token(source, input, markdown_only),
                    close_emphasis_relaxed(d, n),
                ),
            ),
            |(_, (tokens, _))| tokens,
        )
    };
    // open <tokens> close
    let strict_run = |d, n| {
        map(
            pair(
                open_emphasis_strict(d, n),
                many_till(
                    move |input| token(source, input, markdown_only),
                    close_emphasis_strict(d, n),
                ),
            ),
            |(_, (tokens, _))| tokens,
        )
    };

    let italic = alt((relaxed_run('*', 1), strict_run('_', 1)));
    let bold = alt((relaxed_run('*', 2), strict_run('_', 2)));
    let italic_bold = alt((relaxed_run('*', 3), strict_run('_', 3)));
    let strikethrough = relaxed_run('~', 2);
    let spoiler = relaxed_run('|', 2);
    let spaced_code = map(
        flat_map(pair(many1(char('`')), tag(" ")), |(backticks, _)| {
            many_till(
                anychar,
                pair(
                    tag(" "),
                    terminated(
                        count(char('`'), backticks.len()),
                        not(char('`')),
                    ),
                ),
            )
        }),
        |(chars, _)| chars.into_iter().map(Token::Plain).collect::<Vec<_>>(),
    );
    let dense_code = map(
        flat_map(many1(char('`')), |backticks| {
            verify(
                many_till(
                    anychar,
                    terminated(
                        count(char('`'), backticks.len()),
                        not(char('`')),
                    ),
                ),
                |(chars, _)| chars.last().is_none_or(|c| *c != '`'),
            )
        }),
        |(chars, _)| chars.into_iter().map(Token::Plain).collect::<Vec<_>>(),
    );
    let code = alt((spaced_code, dense_code));
    let plain_code_delimiters = map(many1(char('`')), |backticks| {
        backticks.into_iter().map(Token::Plain).collect::<Vec<_>>()
    });

    alt((
        map(code, Markdown::Code),
        map(italic_bold, Markdown::ItalicBold),
        map(bold, Markdown::Bold),
        map(italic, Markdown::Italic),
        map(strikethrough, Markdown::Strikethrough),
        map(spoiler, Markdown::Spoiler),
        map(plain_code_delimiters, Markdown::Plain),
    ))
}

fn dollar(input: &str) -> IResult<&str, Dollar> {
    let color_name = |input| {
        alt((
            map(tag("white"), |_| Color::White),
            map(tag("black"), |_| Color::Black),
            map(tag("blue"), |_| Color::Blue),
            map(tag("green"), |_| Color::Green),
            map(tag("red"), |_| Color::Red),
            map(tag("brown"), |_| Color::Brown),
            map(tag("magenta"), |_| Color::Magenta),
            map(tag("orange"), |_| Color::Orange),
            map(tag("yellow"), |_| Color::Yellow),
            map(tag("lightgreen"), |_| Color::LightGreen),
            map(tag("cyan"), |_| Color::Cyan),
            map(tag("lightcyan"), |_| Color::LightCyan),
            map(tag("lightblue"), |_| Color::LightBlue),
            map(tag("pink"), |_| Color::Pink),
            map(tag("grey"), |_| Color::Grey),
            map(tag("lightgrey"), |_| Color::LightGrey),
        ))
        .parse(input)
    };
    // 1-2 digits -> Color
    let color_digit = |input| {
        map_opt(
            recognize(many_m_n(1, 2, satisfy(|c| c.is_ascii_digit()))),
            |s: &str| s.parse().ok().and_then(Color::code),
        )
        .parse(input)
    };
    let color = move |input| alt((color_name, color_digit)).parse(input);

    // Optional , then Color
    let background =
        map(opt((char(','), color)), |maybe: Option<(char, Color)>| {
            maybe.map(|(_, color)| color)
        });

    // $cFG[,BG]
    let start_color =
        map((tag("$c"), (color, background)), |(_, (fg, bg))| (fg, bg));

    alt((
        map(tag("$b"), |_| Dollar::Bold),
        map(tag("$i"), |_| Dollar::Italics),
        map(tag("$m"), |_| Dollar::Monospace),
        map(tag("$s"), |_| Dollar::Strikethrough),
        map(tag("$u"), |_| Dollar::Underline),
        map(tag("$r"), |_| Dollar::Reset),
        map(start_color, |(fg, bg)| Dollar::StartColor(fg, bg)),
        // No valid colors after code == end
        map(tag("$c"), |_| Dollar::EndColor),
    ))
    .parse(input)
}

#[derive(Debug)]
enum Token {
    Escaped(char),
    Markdown(Markdown),
    Dollar(Dollar),
    Plain(char),
}

impl Token {
    fn encode(self, out: &mut String) {
        match self {
            Token::Escaped(c) => out.push(c),
            Token::Markdown(markdown) => match markdown {
                Markdown::Bold(tokens) => {
                    let b = Modifier::Bold.char();
                    out.push(b);
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(b);
                }
                Markdown::Italic(tokens) => {
                    let i = Modifier::Italics.char();
                    out.push(i);
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(i);
                }
                Markdown::ItalicBold(tokens) => {
                    let b = Modifier::Bold.char();
                    let i = Modifier::Italics.char();
                    out.push(b);
                    out.push(i);
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(i);
                    out.push(b);
                }
                Markdown::Code(tokens) => {
                    let m = Modifier::Monospace.char();
                    out.push(m);
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(m);
                }
                Markdown::Spoiler(tokens) => {
                    let c = Modifier::Color.char();
                    let black = Color::Black.digit();
                    let _ = write!(out, "{c}{black},{black}");
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(c);
                }
                Markdown::Strikethrough(tokens) => {
                    let m = Modifier::Strikethrough.char();
                    out.push(m);
                    for token in tokens {
                        token.encode(out);
                    }
                    out.push(m);
                }
                Markdown::Plain(tokens) => {
                    for token in tokens {
                        token.encode(out);
                    }
                }
            },
            Token::Dollar(dollar) => match dollar {
                Dollar::Bold => {
                    out.push(Modifier::Bold.char());
                }
                Dollar::Italics => {
                    out.push(Modifier::Italics.char());
                }
                Dollar::Monospace => {
                    out.push(Modifier::Monospace.char());
                }
                Dollar::Strikethrough => {
                    out.push(Modifier::Strikethrough.char());
                }
                Dollar::Underline => {
                    out.push(Modifier::Underline.char());
                }
                Dollar::Reset => {
                    out.push(Modifier::Reset.char());
                }
                Dollar::StartColor(fg, bg) => {
                    let c = Modifier::Color.char();
                    let fg = fg.digit();
                    let _ = write!(out, "{c}{fg}");

                    if let Some(bg) = bg.map(Color::digit) {
                        let _ = write!(out, ",{bg}");
                    }
                }
                Dollar::EndColor => {
                    out.push(Modifier::Color.char());
                }
            },
            Token::Plain(c) => out.push(c),
        }
    }
}

#[derive(Debug)]
enum Markdown {
    Bold(Vec<Token>),
    Italic(Vec<Token>),
    ItalicBold(Vec<Token>),
    Strikethrough(Vec<Token>),
    Code(Vec<Token>),
    Spoiler(Vec<Token>),
    Plain(Vec<Token>),
}

#[derive(Debug)]
enum Dollar {
    Bold,
    Italics,
    Monospace,
    Strikethrough,
    Underline,
    Reset,
    StartColor(Color, Option<Color>),
    EndColor,
}

#[cfg(test)]
mod test {
    use crate::message::formatting::encode;
    #[test]
    fn internal_format() {
        let tests = [
            (("_hello_", false), String::from("\u{1d}hello\u{1d}")),
            (
                ("hello there friend!!", false),
                String::from("hello there friend!!"),
            ),
            (
                ("hello there _friend_!!", false),
                String::from("hello there \u{1d}friend\u{1d}!!"),
            ),
            (
                ("hello there __friend__!!", false),
                String::from("hello there \u{2}friend\u{2}!!"),
            ),
            (
                ("hello there ___friend___!!", false),
                String::from("hello there \u{2}\u{1d}friend\u{1d}\u{2}!!"),
            ),
            (
                ("hello there **_fri_end_**!!", false),
                String::from("hello there \u{2}\u{1d}fri_end\u{1d}\u{2}!!"),
            ),
            (
                ("abc *** def *123* A _ B", false),
                String::from("abc *** def \u{1d}123\u{1d} A _ B"),
            ),
            (
                ("does that show up as ******* for you too?", false),
                String::from("does that show up as ******* for you too?"),
            ),
            (
                ("hello there ~~friend~~!!", false),
                String::from("hello there \u{1e}friend\u{1e}!!"),
            ),
            (
                ("testing__testing__onetwothree", false),
                String::from("testing__testing__onetwothree"),
            ),
            (
                ("some code `let x = 0;`", false),
                String::from("some code \u{11}let x = 0;\u{11}"),
            ),
            (
                ("spoiler --> ||super secret||", false),
                String::from("spoiler --> \u{3}1,1super secret\u{3}"),
            ),
            (
                ("`__length_hint__`", false),
                String::from("\u{11}__length_hint__\u{11}"),
            ),
            (
                ("These are escaped: \\*, \\_, \\`, \\|", false),
                String::from("These are escaped: *, _, `, |"),
            ),
            (
                (
                    r"test escaping with *zero* \*one\* \\*two\\* \\\*three\\\* \\\\*four\\\\* backslashes",
                    false,
                ),
                String::from(
                    "test escaping with \u{1d}zero\u{1d} *one* \\*two\\* \\\\*three\\\\* \\\\\\*four\\\\\\* backslashes",
                ),
            ),
            (
                (
                    r"nicknames like \, \\, \\\ and \\\\ contain backslashes",
                    false,
                ),
                String::from(
                    r"nicknames like \, \\, \\\ and \\\\ contain backslashes",
                ),
            ),
            (
                (
                    "Use `` \\` `` to wrap code, but only strip spaces if on` both` sides.",
                    false,
                ),
                String::from(
                    "Use \u{11}\\`\u{11} to wrap code, but only strip spaces if on\u{11} both\u{11} sides.",
                ),
            ),
            (
                ("``foo`bar``, ` foo `` bar `, `foo\\`bar`", false),
                String::from(
                    "\u{11}foo`bar\u{11}, \u{11}foo `` bar\u{11}, \u{11}foo\\\u{11}bar`",
                ),
            ),
            (
                ("`foo``bar`` ```bar``", false),
                String::from("`foo\u{11}bar\u{11} ```bar``"),
            ),
            (
                (
                    "$c1,0black on white $c2now blue on white$r$b BOLD $i BOLD AND ITALIC$r $ccode yo",
                    false,
                ),
                String::from(
                    "\u{3}1,0black on white \u{3}2now blue on white\u{f}\u{2} BOLD \u{1d} BOLD AND ITALIC\u{f} \u{3}code yo",
                ),
            ),
        ];
        for ((text, markdown_only), expected) in tests {
            let actual = encode(text, markdown_only);
            assert_eq!(actual, expected);
        }
    }
}
