use jotdown::{
    html::Renderer, AttributeKind, AttributeValue, Attributes, Container, Event, Render,
};
use std::{fmt::Write, process::Command};

cfg_if::cfg_if! {
    if #[cfg(feature = "emojis")] {
        extern crate emojis;
    }
}
cfg_if::cfg_if! {
    if #[cfg(feature = "inkjet")] {
        use inkjet::{formatter, Highlighter, Language};
    } else if #[cfg(feature = "syntect")] {
        use syntect::{
            html::{ClassStyle, ClassedHTMLGenerator},
            parsing::{SyntaxReference, SyntaxSet},
            util::LinesWithEndings,
        };
    }
}

#[derive(Clone, Debug, Default)]
struct DocumentRendererOpts {
    anchor: String,
    no_emojis: bool,
    symbols: Option<String>,
    skip_title: bool,
    toc: String,
    toc_skip_title: bool,
}

/// Implementation of [`jotdown::Render`] that renders documents with advanced
/// features.
///
/// The implementation mutates the document structure, so it is **required** to
/// use either [`push_events()`](DocumentRenderer::push_events) or
/// [`write_events()`](DocumentRenderer::write_events) and pass the entire
/// document. Do not push single events individually.
#[derive(Default)]
pub struct DocumentRenderer<'a> {
    html: Renderer<'a>,
    opts: DocumentRendererOpts,
}

impl DocumentRenderer<'_> {
    /// Create a new renderer with the provided options.
    ///
    /// # Arguments
    ///
    /// * `anchor` - works the same way as
    ///   [`RenderOpts::anchor`](struct.RenderOpts.html#structfield.anchor)
    /// * `no_symbols` - disable djot symbol processing
    /// * `skip_title` - skip rendering the title (first top-level heading)
    /// * `toc` - the (pre-rendered) table of contents, if any
    /// * `toc_skip_title` - if `false`, the renderer will write `toc` (if
    ///   provided) at the beginning of the document; if `true`, the renderer
    ///   will write `toc` (if provided) after the document title; has no effect
    ///   if `toc` is `None`; see also
    ///   [`TocOpts::skip_title`](struct.TocOpts.html#structfield.skip_title)
    pub fn new(
        anchor: Option<String>,
        no_emojis: bool,
        symbols: Option<String>,
        skip_title: bool,
        toc: Option<String>,
        toc_skip_title: bool,
    ) -> Self {
        DocumentRenderer {
            html: Renderer::default(),
            opts: DocumentRendererOpts {
                anchor: anchor.unwrap_or_default(),
                no_emojis,
                symbols,
                skip_title,
                toc: toc.unwrap_or_default(),
                toc_skip_title,
            },
        }
    }
}

cfg_if::cfg_if! {
    if #[cfg(feature = "inkjet")] {
        struct SyntaxHighlightContext {
            lang: Option<Language>,
        }
    } else if #[cfg(feature = "syntect")] {
        struct SyntaxHighlightContext {
            lang: Option<SyntaxReference>,
            ss: SyntaxSet,
        }
    } else {
        struct SyntaxHighlightContext {
            lang: Option<bool>,
        }
    }
}

struct Context {
    // Context specific to the syntax highlighter
    highlighting: SyntaxHighlightContext,
    // This is true if skipping the title was requested _and_ the current event
    // is inside the title
    skip_in_title_active: bool,
    // If skipping the title was requested and a title was detected, this is
    // Some(<section ID>)
    skip_section_id: Option<String>,
    // This is set to true once the toc has been written
    toc_written: bool,
}

fn map_symbol_emoji(sym: &str) -> Option<Vec<Event<'static>>> {
    cfg_if::cfg_if! {
        if #[cfg(feature = "emojis")] {
            match sym {
                // poor-man's skin tone support...
                "skin-tone-1" => Some(vec![Event::Str("\u{1f3fb}".into())]),
                "skin-tone-2" => Some(vec![Event::Str("\u{1f3fc}".into())]),
                "skin-tone-3" => Some(vec![Event::Str("\u{1f3fd}".into())]),
                "skin-tone-4" => Some(vec![Event::Str("\u{1f3fe}".into())]),
                "skin-tone-5" => Some(vec![Event::Str("\u{1f3ff}".into())]),
                _ => emojis::get_by_shortcode(sym).map(|emoji| vec![Event::Str(emoji.as_str().into())])
            }
        } else {
            _ = sym;
            None
        }
    }
}

fn map_symbol_custom(exe: &str, sym: &str) -> Option<Vec<Event<'static>>> {
    Command::new(exe)
        .arg(sym)
        .output()
        .ok()
        .and_then(|o| {
            if o.status.success() {
                String::from_utf8(o.stdout).ok()
            } else {
                None
            }
        })
        .map(|s| {
            vec![
                Event::Start(
                    Container::RawBlock {
                        format: "html".into(),
                    },
                    Attributes::new(),
                ),
                Event::Str(s.into()),
                Event::End(Container::RawBlock {
                    format: "html".into(),
                }),
            ]
        })
}

fn map_symbol(opts: &DocumentRendererOpts, sym: &str) -> Option<Vec<Event<'static>>> {
    if opts.no_emojis {
        None
    } else {
        map_symbol_emoji(sym)
    }
    .or_else(|| {
        if let Some(exe) = &opts.symbols {
            map_symbol_custom(exe, sym)
        } else {
            None
        }
    })
}

fn map_event<'s>(e: Event<'s>, ctx: &mut Context, opts: &DocumentRendererOpts) -> Vec<Event<'s>> {
    if ctx.skip_in_title_active {
        // skip everything until we get to the end of level one heading, at
        // which we may also have to write the toc
        match &e {
            Event::End(Container::Heading { level: 1, .. }) => {
                ctx.skip_in_title_active = false;
                if opts.toc_skip_title && !opts.toc.is_empty() && !ctx.toc_written {
                    ctx.toc_written = true;
                    vec![
                        Event::Start(
                            Container::RawBlock {
                                format: "html".into(),
                            },
                            Attributes::new(),
                        ),
                        Event::Str(opts.toc.clone().into()),
                        Event::End(Container::RawBlock {
                            format: "html".into(),
                        }),
                    ]
                } else {
                    vec![]
                }
            }
            _ => vec![],
        }
    } else {
        match &e {
            Event::Start(Container::CodeBlock { language }, _attrs) => {
                cfg_if::cfg_if! {
                        if #[cfg(feature = "inkjet")] {
                        ctx.highlighting.lang = Language::from_token(language);
                        vec![
                            Event::Start(Container::RawBlock { format: "html".into() }, Attributes::new()),
                            e,
                        ]
                    } else if #[cfg(feature = "syntect")] {
                        ctx.highlighting.lang = ctx.highlighting.ss.find_syntax_by_token(language).cloned();
                        vec![
                                Event::Start(Container::RawBlock { format: "html".into() }, Attributes::new()),
                                e,
                            ]
                    } else {
                        let _ = language;
                        vec![e]
                    }
                }
            }
            Event::End(Container::CodeBlock { language: _ }) => {
                cfg_if::cfg_if! {
                    if #[cfg(feature = "inkjet")] {
                        ctx.highlighting.lang = None;
                        vec![e, Event::End(Container::RawBlock { format: "html".into() })]
                    } else if #[cfg(feature = "syntect")] {
                        ctx.highlighting.lang = None;
                        vec![e, Event::End(Container::RawBlock { format: "html".into() })]
                    } else {
                        vec![e]
                    }
                }
            }
            Event::Start(
                Container::Heading {
                    level: _,
                    has_section: _,
                    id,
                },
                _attrs,
            ) => {
                if !opts.anchor.is_empty() {
                    let mut link_attrs = Attributes::new();
                    link_attrs.push((
                        AttributeKind::Pair {
                            key: "aria-hidden".into(),
                        },
                        AttributeValue::from("true"),
                    ));
                    let link = Container::Link(
                        format!("#{}", id).into(),
                        jotdown::LinkType::Span(jotdown::SpanLinkType::Inline),
                    );
                    vec![
                        e,
                        Event::Start(link.clone(), link_attrs),
                        Event::Str(opts.anchor.clone().into()),
                        Event::End(link),
                    ]
                } else {
                    vec![e]
                }
            }
            Event::End(Container::Heading {
                level: 1,
                has_section: _,
                id: _,
            }) => {
                if opts.toc_skip_title && !opts.toc.is_empty() && !ctx.toc_written {
                    ctx.toc_written = true;
                    vec![
                        e,
                        Event::Start(
                            Container::RawBlock {
                                format: "html".into(),
                            },
                            Attributes::new(),
                        ),
                        Event::Str(opts.toc.clone().into()),
                        Event::End(Container::RawBlock {
                            format: "html".into(),
                        }),
                    ]
                } else {
                    vec![e]
                }
            }
            Event::Start(Container::Section { id }, attr) => {
                let mut filtered = Attributes::new();
                for (k, v) in attr {
                    if !k.key().is_some_and(|key| key.starts_with("dmos:")) {
                        filtered.push((k.clone(), v.clone()))
                    }
                }
                vec![Event::Start(
                    Container::Section { id: id.clone() },
                    filtered,
                )]
            }
            Event::End(Container::Section { id }) => {
                if ctx
                    .skip_section_id
                    .as_ref()
                    .is_some_and(|skip_id| skip_id == id.as_ref())
                {
                    vec![]
                } else {
                    vec![e]
                }
            }
            Event::Str(s) => match &ctx.highlighting.lang {
                None => vec![e],
                Some(l) => {
                    cfg_if::cfg_if! {
                        if #[cfg(feature = "inkjet")] {
                            let highlighted = Highlighter::new()
                                .highlight_to_string(*l, &formatter::Html, s)
                                .unwrap();
                        } else if #[cfg(feature = "syntect")] {
                            let mut generator = ClassedHTMLGenerator::new_with_class_style(l, &ctx.highlighting.ss, ClassStyle::Spaced);
                            for line in LinesWithEndings::from(s) {
                                generator
                                    .parse_html_for_line_which_includes_newline(line)
                                    .unwrap();
                                //TODO error handling?
                            }
                            let highlighted = generator.finalize();
                        } else {
                            let _ = l;
                            let highlighted = s.to_string();
                        }
                    }

                    vec![Event::Str(highlighted.into())]
                }
            },
            Event::Symbol(sym) => {
                if let Some(e_) = map_symbol(opts, sym) {
                    e_
                } else {
                    vec![e]
                }
            }
            _ => vec![e],
        }
    }
}

fn map_events<'s, I>(events: I, opts: &DocumentRendererOpts) -> impl IntoIterator<Item = Event<'s>>
where
    I: Iterator<Item = Event<'s>>,
{
    cfg_if::cfg_if! {
        if #[cfg(feature = "inkjet")] {
            let hlctx = SyntaxHighlightContext {
                lang: None,
            };
        } else if #[cfg(feature = "syntect")] {
            let hlctx = SyntaxHighlightContext {
                lang: None,
                ss: SyntaxSet::load_defaults_newlines(),
            };
        } else {
            let hlctx = SyntaxHighlightContext {
                lang: None
            };
        }
    }

    let mut ctx = Context {
        highlighting: hlctx,
        skip_in_title_active: false,
        skip_section_id: None,
        toc_written: false,
    };

    let mut events = events.peekable();
    while events
        .peek()
        .is_some_and(|e| matches!(e, Event::Blankline | Event::Start(Container::Document, _)))
    {
        _ = events.next(); // Ignore any blank lines at beginning of doc
    }

    let (first, second) = (events.next(), events.next());
    if let (
        Some(Event::Start(Container::Section { id }, _)),
        Some(Event::Start(Container::Heading { level, .. }, _)),
    ) = (&first, &second)
    {
        if *level == 1 && opts.skip_title {
            ctx.skip_in_title_active = true;
            ctx.skip_section_id = Some(id.to_string());
        }
    }

    let prepend = if ctx.skip_in_title_active {
        vec![]
    } else {
        vec![first, second]
    };

    let opts_ = opts.clone();
    let events = prepend
        .into_iter()
        .flatten()
        .chain(events)
        .flat_map(move |e| map_event(e, &mut ctx, &opts_));

    if !opts.toc.is_empty() && !opts.toc_skip_title {
        vec![
            Event::Start(
                Container::RawBlock {
                    format: "html".into(),
                },
                Attributes::new(),
            ),
            Event::Str(opts.toc.clone().into()),
            Event::End(Container::RawBlock {
                format: "html".into(),
            }),
        ]
        .into_iter()
        .chain(events)
    } else {
        vec![].into_iter().chain(events)
    }
}

impl<'s> Render<'s> for DocumentRenderer<'s> {
    fn push_events<I, W>(&mut self, events: I, out: W) -> std::fmt::Result
    where
        I: Iterator<Item = Event<'s>>,
        W: Write,
    {
        let events = map_events(events, &self.opts);
        self.html.push_events(events.into_iter(), out)
    }

    fn write_events<I, W>(&mut self, events: I, out: W) -> std::io::Result<()>
    where
        I: Iterator<Item = Event<'s>>,
        W: std::io::Write,
    {
        let events = map_events(events, &self.opts);
        self.html.write_events(events.into_iter(), out)
    }

    fn push_event<W>(&mut self, event: Event<'s>, out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        self.html.push_event(event, out)
    }
}
