#![deny(missing_docs)]

/*!
A Rust library for converting Pukram-formatted text to HTML.

Pukram is a lightweight markup language inspired by Markdown, designed for easy formatting of text content.
This library is primarily targeted at developers building web servers who need to convert Pukram-formatted
text into HTML for displaying on websites. It can also be used to convert Pukram files to HTML, which
can then be further converted to PDF using other tools.
*/

use std::{
    collections::HashMap,
    io::{Result, Write},
    marker::PhantomData,
};

use pukram_formatting::Formatting;

fn try_split_once(value: &str, delimiter: char) -> (&str, Option<&str>) {
    value
        .split_once(delimiter)
        .map_or((value, None), |(first, second)| (first, Some(second)))
}

fn split_once_or_empty(value: &str, delimiter: char) -> (&str, &str) {
    value.split_once(delimiter).unwrap_or((value, ""))
}

fn split_link(value: &str, delimiter: char) -> (&str, &str) {
    let (name, link) = try_split_once(value, delimiter);
    let name = name.trim();
    (name, link.map_or(name, |link| link.trim()))
}

trait FormattingExtensions {
    fn start<W: Write>(&self, output: &mut W) -> Result<()>;
    fn finish<W: Write>(&self, output: &mut W) -> Result<()>;
}

impl FormattingExtensions for Formatting {
    fn start<W: Write>(&self, output: &mut W) -> Result<()> {
        fn w<W: Write>(output: &mut W, name: &str) -> Result<()> {
            write!(output, "<{name}>")
        }

        if self.is_bold() {
            w(output, "b")?;
        }
        if self.is_italic() {
            w(output, "i")?;
        }
        if self.is_mono() {
            w(output, "code")?;
        }
        match (self.is_top(), self.is_bottom()) {
            (true, true) => w(output, "small")?,
            (true, false) => w(output, "sup")?,
            (false, true) => w(output, "sub")?,
            (false, false) => (),
        }
        if self.is_underscore() {
            w(output, "u")?;
        }
        if self.is_strikethrough() {
            w(output, "s")?;
        }

        Ok(())
    }

    fn finish<W: Write>(&self, output: &mut W) -> Result<()> {
        fn w<W: Write>(output: &mut W, name: &str) -> Result<()> {
            write!(output, "</{name}>")
        }

        if self.is_strikethrough() {
            w(output, "s")?;
        }
        if self.is_underscore() {
            w(output, "u")?;
        }
        match (self.is_top(), self.is_bottom()) {
            (true, true) => w(output, "small")?,
            (true, false) => w(output, "sup")?,
            (false, true) => w(output, "sub")?,
            (false, false) => (),
        }
        if self.is_mono() {
            w(output, "code")?;
        }
        if self.is_italic() {
            w(output, "i")?;
        }
        if self.is_bold() {
            w(output, "b")?;
        }

        Ok(())
    }
}

/// Struct to hold information about the converted text.
pub struct TextInfo {
    /// Number of lines in the converted text.
    pub lines: usize,
    /// Number of words in the converted text.
    pub words: usize,
    /// Number of characters in the converted text.
    pub chars: usize,
}

/// Convert Pukram to HTML.
///
/// This function is a convenience wrapper around `convert_subheader` with a start level of 0.
#[inline]
pub fn convert<R: AsRef<str>>(
    input: impl IntoIterator<Item = R>,
    output: &mut impl Write,
) -> TextInfo {
    convert_subheader(input, output, 0)
}

struct AllowList(u8);

impl Default for AllowList {
    fn default() -> Self {
        Self(Self::ALL)
    }
}

impl AllowList {
    const ALL: u8 = 0b11111111;

    const LINKS: u8 = 0b00000001;
    const IMAGES: u8 = 0b00000010;
    const AUDIO: u8 = 0b00000100;

    pub fn links(&self) -> bool {
        self.0 & Self::LINKS > 0
    }

    pub fn images(&self) -> bool {
        self.0 & Self::IMAGES > 0
    }

    pub fn audio(&self) -> bool {
        self.0 & Self::AUDIO > 0
    }

    pub fn set_links(&mut self, enable: bool) {
        if enable {
            self.0 |= Self::LINKS;
        } else {
            self.0 &= !Self::LINKS;
        }
    }

    pub fn set_images(&mut self, enable: bool) {
        if enable {
            self.0 |= Self::IMAGES;
        } else {
            self.0 &= !Self::IMAGES;
        }
    }

    pub fn set_audio(&mut self, enable: bool) {
        if enable {
            self.0 |= Self::AUDIO;
        } else {
            self.0 &= !Self::AUDIO;
        }
    }
}

/// Configuration settings for the conversion process.
pub struct Settings<W: Write, F: Fn(&str, &mut W, usize)> {
    handler: F,
    start_level: usize,
    use_textboxes: bool,
    allow: AllowList,
    handler_data: PhantomData<W>,
}

impl<W: Write> Default for Settings<W, fn(&str, &mut W, usize)> {
    fn default() -> Self {
        fn handler(_input: &str, _output: &mut impl Write, _level: usize) {}
        Self {
            handler,
            start_level: 0,
            use_textboxes: false,
            allow: AllowList::default(),
            handler_data: PhantomData,
        }
    }
}

impl<W: Write, F: Fn(&str, &mut W, usize)> Settings<W, F> {
    /// Set the starting header level for the converted HTML.
    pub fn with_start_level(self, start_level: usize) -> Self {
        Self {
            start_level,
            ..self
        }
    }

    /// Set whether to use textboxes for certain elements.
    pub fn with_use_textboxes(self, use_textboxes: bool) -> Self {
        Self {
            use_textboxes,
            ..self
        }
    }

    /// Set whether links are allowed.
    pub fn with_allow_links(mut self, allow_links: bool) -> Self {
        self.allow.set_links(allow_links);
        self
    }

    /// Set whether images are allowed.
    pub fn with_allow_images(mut self, allow_images: bool) -> Self {
        self.allow.set_images(allow_images);
        self
    }

    /// Set whether audio is allowed.
    pub fn with_allow_audio(mut self, allow_audio: bool) -> Self {
        self.allow.set_audio(allow_audio);
        self
    }

    /// Set a custom handler function for processing special list elements.
    pub fn with_handler<F2: Fn(&str, &mut W, usize)>(self, handler: F2) -> Settings<W, F2> {
        let Self {
            start_level,
            use_textboxes,
            allow,
            handler_data,
            ..
        } = self;

        Settings {
            handler,
            start_level,
            use_textboxes,
            allow,
            handler_data,
        }
    }
}

/// Convert Pukram to HTML with subheaders.
///
/// This function is a convenience wrapper around `convert_extended` with default settings and a specified start level.
#[inline]
pub fn convert_subheader<R: AsRef<str>>(
    input: impl IntoIterator<Item = R>,
    output: &mut impl Write,
    start_level: usize,
) -> TextInfo {
    convert_extended(
        input,
        output,
        Settings::default().with_start_level(start_level),
    )
}

/// Convert Pukram to HTML with extended settings.
pub fn convert_extended<R: AsRef<str>, W: Write, F: Fn(&str, &mut W, usize)>(
    input: impl IntoIterator<Item = R>,
    mut output: &mut W,
    settings: Settings<W, F>,
) -> TextInfo {
    let Settings {
        start_level,
        handler,
        use_textboxes,
        ..
    } = settings;

    let mut lines = 0;
    let mut words = 0;
    let mut chars = 0;

    let mut ignore_next = false;

    enum Action {
        Name {
            original: Box<str>,
            shown: Option<Box<str>>,
        },
        Link {
            name: Box<str>,
            link: Box<str>,
        },
        Image {
            name: Box<str>,
            path: Box<str>,
        },
        Audio {
            path: Box<str>,
        },
    }

    impl Action {
        fn handle(self, output: &mut impl Write, names: &mut HashMap<Box<str>, Box<str>>) {
            use Action::*;
            match self {
                Name { original, shown } => {
                    if let Some(shown) = shown {
                        names.insert(original, shown);
                    } else {
                        names.remove(&original);
                    }
                }
                Link { name, link } => {
                    let _ = writeln!(output, "<a href=\"{link}\">{name}</a>");
                }
                Image { name, path } => {
                    let _ = writeln!(
                        output,
                        "<img src=\"{path}\" alt=\"{name}\" style='max-height: 100%; max-width: 100%; object-fit: cover'>"
                    );
                }
                Audio { path } => {
                    let _ = writeln!(output, "<audio controls src=\"{path}\"/>");
                }
            }
        }
    }

    fn handle_actions(
        actions: &mut Vec<Action>,
        output: &mut impl Write,
        names: &mut HashMap<Box<str>, Box<str>>,
    ) {
        let actions = std::mem::take(actions);
        let mut actions = actions.into_iter();
        let Some(action) = actions.next() else {
            return;
        };
        action.handle(output, names);
        for action in actions {
            let _ = writeln!(output, "<br>");
            action.handle(output, names);
        }
    }

    let mut actions = Vec::new();

    #[derive(PartialEq, Eq)]
    enum State {
        Pause,
        Block,
        Name,
        NameOnly,
    }

    let mut state = State::Pause;
    let mut last_level = start_level;
    let mut names = HashMap::new();

    let finish_block = |state: &mut State,
                        actions: &mut Vec<Action>,
                        output: &mut W,
                        names: &mut HashMap<Box<str>, Box<str>>| {
        if !actions.is_empty() {
            let _ = writeln!(output, "{}", if *state == Pause { "<p>" } else { "<br>" });
            handle_actions(actions, output, names);
            if *state == Pause {
                let _ = writeln!(output, "</p>");
            }
        }

        use State::*;
        let _ = match *state {
            Name | NameOnly => writeln!(output, "</fieldset></p>"),
            Block => writeln!(output, "</p>"),
            Pause => return,
        };

        *state = Pause;
    };

    for line in input {
        let mut line = line.as_ref();

        let mut line_chars = line.chars();
        let mut level = 0;
        while line_chars.next() == Some('#') {
            level += 1;
        }
        if level > 0 {
            line = &line[level..];
        }
        let line = line.trim();
        if level > 0 {
            let level = start_level + level;
            let _ = write!(output, "<h{level}>{line}</h{level}>");
            last_level = level;
            continue;
        }

        if let Some(stripped) = line.strip_prefix('-') {
            let (kind, args) = split_once_or_empty(stripped.trim_start(), ' ');
            let (kind, default) = kind
                .strip_suffix('!')
                .map_or((kind, false), |kind| (kind, true));
            let (variant, sub) = try_split_once(kind, ':');
            match variant {
                "Character" => {
                    if let Some(sub) = sub
                        && let Some((original, "name")) = sub.split_once(':')
                    {
                        let shown = if default {
                            None
                        } else {
                            Some(args.trim().into())
                        };
                        actions.push(Action::Name {
                            original: original.into(),
                            shown,
                        });
                    }
                }
                "Link" if settings.allow.links() => {
                    let (name, link) = split_link(args, ' ');

                    actions.push(Action::Link {
                        name: name.into(),
                        link: link.into(),
                    });
                }
                "Image" if settings.allow.images() => {
                    let (name, path) = split_link(args, ' ');

                    actions.push(Action::Image {
                        name: name.into(),
                        path: path.into(),
                    });
                }
                "Audio" if settings.allow.audio() => {
                    actions.push(Action::Audio { path: args.into() })
                }
                _ => continue,
            }
            continue;
        }
        if let Some(stripped) = line.strip_prefix('+') {
            handler(stripped.trim_start(), output, last_level);
            continue;
        }
        if line.starts_with('=') {
            continue;
        }

        if line.is_empty() {
            finish_block(&mut state, &mut actions, output, &mut names);
            continue;
        }

        let mut line = line;

        if state == State::Pause {
            let _ = writeln!(output, "<p>");
            if let Some((name, text)) = line.split_once(':') {
                let name = name.trim_end();
                let text = text.trim_start();
                let name = names.get(name).map_or(name, |name| name.as_ref());
                let name = name
                    .replace('&', "&amp;")
                    .replace('<', "&lt;")
                    .replace('>', "&gt;");
                state = if use_textboxes {
                    let _ = writeln!(output, "<fieldset>",);
                    let _ = writeln!(output, "<legend>{name}</legend>");
                    if text.is_empty() {
                        state = State::NameOnly;
                        continue;
                    }
                    State::Name
                } else {
                    let _ = writeln!(output, "<i>{name}</i>:");
                    State::Block
                };
                line = text;
            } else {
                state = State::Block;
            }
        } else if state == State::NameOnly {
            state = State::Name;
        } else {
            let _ = writeln!(output, "<br>");
        }

        if !actions.is_empty() {
            handle_actions(&mut actions, output, &mut names);
            let _ = writeln!(output, "<br>");
        }

        let mut was_whitespace = false;

        lines += 1;

        let mut use_formatting = false;
        let mut formatting = Formatting::default();

        for c in line.chars() {
            if ignore_next {
                ignore_next = false;
            } else {
                if c == '\\' {
                    ignore_next = true;
                    continue;
                }

                let last_formatting = formatting;
                if formatting.apply(c) {
                    if use_formatting {
                        let _ = last_formatting.finish(&mut output);
                        use_formatting = false;
                    }
                    continue;
                }

                if !use_formatting {
                    let _ = formatting.start(&mut output);
                    use_formatting = true;
                }
            }

            chars += 1;

            let is_whitespace = c.is_whitespace();
            if is_whitespace && !was_whitespace {
                words += 1;
            }
            was_whitespace = is_whitespace;

            let _ = match c {
                '<' => write!(output, "&lt;"),
                '>' => write!(output, "&gt;"),
                '&' => write!(output, "&amp;"),
                _ => write!(output, "{c}"),
            };
        }

        if use_formatting {
            let _ = formatting.finish(&mut output);
        }
    }

    finish_block(&mut state, &mut actions, output, &mut names);

    TextInfo {
        lines,
        words,
        chars,
    }
}
