Toy implementation of the gemini protocol
use std::{borrow::Cow, fmt::Display, str::pattern::Pattern};

/// Modify the lifetime of self by serialising the data into the target.
/// The output will reference the copy in the target
trait RaiseTo<Target> {
    type Output<'a>
    where
        Target: 'a;
    fn raise_to<'a>(&self, target: &'a mut Target) -> Self::Output<'a>;
}

#[derive(Clone, Debug)]
pub enum Media<'a> {
    Gemini(Gemini<'a>),
    Text(&'a str),
}
impl RaiseTo<String> for Media<'_> {
    type Output<'a> = Media<'a>;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        match self {
            Media::Gemini(gem) => Media::Gemini(gem.raise_to(target)),
            Media::Text(text) => Media::Text(text.raise_to(target)),
        }
    }
}

#[derive(Clone, Debug)]
pub struct Gemini<'a> {
    // TODO include lang component
    pub lines: Vec<Line<'a>>,
}
impl RaiseTo<String> for Gemini<'_> {
    type Output<'a> = Gemini<'a>;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        Self {
            lines: self.lines.iter().map(|ln| ln.raise_to(target)).collect(),
        }
    }
}
#[derive(Clone, Debug)]
enum RawLine<'a> {
    Toggle { alt: TextLine<'a> },
    Line(Line<'a>),
}
#[derive(Copy, Clone, Debug)]
pub enum HeadingLevel {
    L1 = 1,
    L2 = 2,
    L3 = 3,
}
#[derive(Clone, Debug)]
pub enum Line<'a> {
    Heading {
        level: HeadingLevel,
        title: TextLine<'a>,
    },
    Link {
        url: TextLine<'a>,
        description: Option<TextLine<'a>>,
    },
    Text(TextLine<'a>),
    Preformatted(Preformat<'a>),
    ListItem(TextLine<'a>),
    Quote(TextLine<'a>),
}
impl RaiseTo<String> for Line<'_> {
    type Output<'a> = Line<'a>;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        match self {
            Line::Heading { level, title } => Line::Heading {
                level: *level,
                title: title.raise_to(target),
            },
            Line::Link { url, description } => {
                let url = url.raise_to(target);
                let description = description.map(|d| d.raise_to(target));
                Line::Link { url, description }
            }
            Line::Text(t) => Line::Text(t.raise_to(target)),
            Line::Preformatted(p) => Line::Preformatted(p.raise_to(target)),
            Line::ListItem(li) => Line::ListItem(li.raise_to(target)),
            Line::Quote(q) => Line::Quote(q.raise_to(target)),
        }
    }
}
#[derive(Clone, Debug)]
pub struct Preformat<'a> {
    pub alt: TextLine<'a>,
    pub lines: Vec<TextLine<'a>>,
}
impl RaiseTo<String> for Preformat<'_> {
    type Output<'a> = Preformat<'a>;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        let alt = self.alt.raise_to(target);
        let lines = self.lines.iter().map(|ln| ln.raise_to(target)).collect();
        Self { alt, lines }
    }
}
fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(
    s: &'a str,
    p: P,
) -> (&'a str, Option<&'a str>) {
    match s.split_once(p) {
        Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),
        None => (s, None),
    }
}
fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {
    let line = string.0;
    if line.starts_with("```") {
        // ignore anything after the lead chars on preformat lines
        return None;
    } else {
        // ignore any other formatting between preformat toggle lines
        return Some(string);
    }
}

/// match on a string prefix, and bind the remainder to an identifier
// TODO allow prefix-binding-suffix and binding-suffix as well.
// No plans for infix strings, that would add ambiguity as well
// as complicating the parser.
// TODO Making this a TT muncher would simplify parsing and make it
// easier to make new cases
macro_rules! match_str {
    (($name:ident) $($s:expr , $rem:ident => $body:expr ,)* _ => $else:expr) => {
        $(if $name.starts_with($s) {
            let $rem = &$name[$s.len()..];
            $body
        } else)* {$else}
    };
}

fn string_to_line(string: TextLine<'_>) -> RawLine {
    let line = string.0;
    RawLine::Line({
        match_str! {(line)
            "```", rem => {
                // ignore anything after the lead chars on preformat lines
                return RawLine::Toggle {
                    alt: TextLine(rem),
                }
            },
            "=> ", rem => {
                match rem.split_once(' ') {
                    Some((url, desc)) => {
                        let url = url.trim_start_matches(' ');
                        Line::Link {
                            url: TextLine(url),
                            description: Some(TextLine(desc)),
                        }
                    }
                    None => Line::Link {
                        url: TextLine(rem),
                        description: None,
                    },
                }
            },
            "* ", rem => {
                Line::ListItem(TextLine(rem))
            },
            "# ", rem => {
                Line::Heading {
                    level: HeadingLevel::L1,
                    title: TextLine(rem),
                }
            },
            "## ", rem => {
                Line::Heading {
                    level: HeadingLevel::L2,
                    title: TextLine(rem),
                }
            },
            "### ", rem => {
                Line::Heading {
                    level: HeadingLevel::L3,
                    title: TextLine(rem),
                }
            },
            ">", rem => {
                Line::Quote(TextLine(rem))
            },
            _ => {
                Line::Text(TextLine(line))
            }
        }
    })
}
fn string_to_lines(value: &str) -> Vec<Line> {
    {
        let mut outer = vec![];
        let mut preformat_block: Option<Preformat> = None;
        let lines = value.lines().map(TextLine);
        for line in lines {
            if preformat_block.is_some() {
                match string_to_preformat(line) {
                    // if we hit a toggle line, switch preformatting mode
                    None => {
                        let Some(i) = preformat_block.take()
                            else {unreachable!("This is within the is_some arm of the if")};
                        outer.push(Line::Preformatted(i));
                    }
                    Some(p) => preformat_block.as_mut().unwrap().lines.push(p),
                }
            } else {
                match string_to_line(line) {
                    // if we hit a toggle line, switch preformatting mode
                    RawLine::Toggle { alt } => {
                        preformat_block = Some(Preformat { alt, lines: vec![] });
                    }
                    RawLine::Line(l) => outer.push(l),
                }
            }
        }
        outer
    }
}
// guaranteed to be only a single line
#[derive(Copy, Clone, Debug)]
pub struct TextLine<'a>(&'a str);

impl<'a> From<TextLine<'a>> for Cow<'a, str> {
    fn from(value: TextLine<'a>) -> Self {
        value.0.into()
    }
}
impl<'a> Display for TextLine<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}
impl<'a> AsRef<str> for TextLine<'a> {
    fn as_ref(&self) -> &str {
        self.0
    }
}

impl RaiseTo<String> for TextLine<'_> {
    type Output<'a> = TextLine<'a>;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        let old_end = target.len();
        target.push_str(self.0);
        TextLine(&target[old_end..])
    }
}

impl RaiseTo<String> for &'_ str {
    type Output<'a> = &'a str;

    fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
        let old_end = target.len();
        target.push_str(self);
        &target[old_end..]
    }
}

impl<'a> From<&'a str> for Gemini<'a> {
    fn from(value: &'a str) -> Self {
        Gemini {
            lines: string_to_lines(value),
        }
    }
}