use std::ops::Range;

use comemo::Prehashed;
use md::escape::escape_html;
use pulldown_cmark as md;
use typst::diag::FileResult;
use typst::font::{Font, FontBook};
use typst::geom::{Point, Size};
use typst::syntax::{Source, SourceId};
use typst::util::Buffer;
use typst::World;
use yaml_front_matter::YamlFrontMatter;

use super::*;

/// HTML documentation.
#[derive(Serialize)]
#[serde(transparent)]
pub struct Html {
    raw: String,
    #[serde(skip)]
    md: String,
    #[serde(skip)]
    description: Option<String>,
}

impl Html {
    /// Create HTML from a raw string.
    pub fn new(raw: String) -> Self {
        Self { md: String::new(), raw, description: None }
    }

    /// Convert markdown to HTML.
    #[track_caller]
    pub fn markdown(resolver: &dyn Resolver, md: &str) -> Self {
        let mut text = md;
        let mut description = None;
        let document = YamlFrontMatter::parse::<Metadata>(md);
        if let Ok(document) = &document {
            text = &document.content;
            description = Some(document.metadata.description.clone())
        }

        let options = md::Options::ENABLE_TABLES | md::Options::ENABLE_HEADING_ATTRIBUTES;

        let mut handler = Handler::new(resolver);
        let iter = md::Parser::new_ext(text, options)
            .filter_map(|mut event| handler.handle(&mut event).then_some(event));

        let mut raw = String::new();
        md::html::push_html(&mut raw, iter);
        raw.truncate(raw.trim_end().len());

        Html { md: text.into(), raw, description }
    }

    /// The raw HTML.
    pub fn as_str(&self) -> &str {
        &self.raw
    }

    /// The original Markdown, if any.
    pub fn md(&self) -> &str {
        &self.md
    }

    /// The title of the HTML.
    ///
    /// Returns `None` if the HTML doesn't start with an `h1` tag.
    pub fn title(&self) -> Option<&str> {
        let mut s = Scanner::new(&self.raw);
        s.eat_if("<h1>").then(|| s.eat_until("</h1>"))
    }

    /// The description from the front matter.
    pub fn description(&self) -> Option<String> {
        self.description.clone()
    }
}

impl Debug for Html {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "Html({:?})", self.title().unwrap_or(".."))
    }
}

/// Front matter metadata.
#[derive(Deserialize)]
struct Metadata {
    description: String,
}

struct Handler<'a> {
    resolver: &'a dyn Resolver,
    lang: Option<String>,
}

impl<'a> Handler<'a> {
    fn new(resolver: &'a dyn Resolver) -> Self {
        Self { resolver, lang: None }
    }

    fn handle(&mut self, event: &mut md::Event) -> bool {
        let lang = self.lang.take();
        match event {
            // Rewrite Markdown images.
            md::Event::Start(md::Tag::Image(_, path, _)) => {
                *path = self.handle_image(path).into();
            }

            // Rewrite HTML images.
            md::Event::Html(html) if html.starts_with("<img") => {
                let range = html_attr_range(html, "src").unwrap();
                let path = &html[range.clone()];
                let mut buf = html.to_string();
                buf.replace_range(range, &self.handle_image(path));
                *html = buf.into();
            }

            // Rewrite contributor sectinos.
            md::Event::Html(html) if html.starts_with("<contributors") => {
                let from = html_attr(html, "from").unwrap();
                let to = html_attr(html, "to").unwrap();
                let Some(output) = contributors(from, to) else { return false };
                *html = output.raw.into();
            }

            // Rewrite links.
            md::Event::Start(md::Tag::Link(ty, dest, _)) => {
                assert!(
                    matches!(ty, md::LinkType::Inline | md::LinkType::Reference),
                    "unsupported link type: {ty:?}",
                );

                *dest = self
                    .handle_link(dest)
                    .unwrap_or_else(|| panic!("invalid link: {dest}"))
                    .into();
            }

            // Inline raw.
            md::Event::Code(code) => {
                let mut chars = code.chars();
                let parser = match (chars.next(), chars.next_back()) {
                    (Some('['), Some(']')) => typst::syntax::parse,
                    (Some('{'), Some('}')) => typst::syntax::parse_code,
                    _ => return true,
                };

                let root = parser(&code[1..code.len() - 1]);
                let html = typst::ide::highlight_html(&root);
                *event = md::Event::Html(html.into());
            }

            // Code blocks.
            md::Event::Start(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(lang))) => {
                self.lang = Some(lang.as_ref().into());
                return false;
            }
            md::Event::End(md::Tag::CodeBlock(md::CodeBlockKind::Fenced(_))) => {
                return false;
            }

            // Example with preview.
            md::Event::Text(text) => {
                let Some(lang) = lang.as_deref() else { return true };
                let html = code_block(self.resolver, lang, text);
                *event = md::Event::Html(html.raw.into());
            }

            _ => {}
        }

        true
    }

    fn handle_image(&self, link: &str) -> String {
        if let Some(file) = FILES.get_file(link) {
            self.resolver.image(link, file.contents())
        } else if let Some(url) = self.resolver.link(link) {
            url
        } else {
            panic!("missing image: {link}")
        }
    }

    fn handle_link(&self, link: &str) -> Option<String> {
        if link.starts_with('#') || link.starts_with("http") {
            return Some(link.into());
        }

        if !link.starts_with('$') {
            return self.resolver.link(link);
        }

        let root = link.split('/').next()?;
        let rest = &link[root.len()..].trim_matches('/');
        let base = match root {
            "$tutorial" => "/docs/tutorial/",
            "$reference" => "/docs/reference/",
            "$category" => "/docs/reference/",
            "$syntax" => "/docs/reference/syntax/",
            "$styling" => "/docs/reference/styling/",
            "$scripting" => "/docs/reference/scripting/",
            "$types" => "/docs/reference/types/",
            "$type" => "/docs/reference/types/",
            "$func" => "/docs/reference/",
            "$changelog" => "/docs/changelog/",
            "$community" => "/docs/community/",
            _ => panic!("unknown link root: {root}"),
        };

        let mut route = base.to_string();
        if root == "$type" && rest.contains('.') {
            let mut parts = rest.split('.');
            let ty = parts.next()?;
            let method = parts.next()?;
            route.push_str(ty);
            route.push_str("/#methods--");
            route.push_str(method);
        } else if root == "$func" {
            let mut parts = rest.split('.');
            let name = parts.next()?;
            let param = parts.next();
            let value =
                LIBRARY.global.get(name).or_else(|_| LIBRARY.math.get(name)).ok()?;
            let Value::Func(func) = value else { return None };
            let info = func.info()?;
            route.push_str(info.category);
            route.push('/');

            if let Some(group) = GROUPS
                .iter()
                .find(|group| group.functions.iter().any(|func| func == info.name))
            {
                route.push_str(&group.name);
                route.push_str("/#");
                route.push_str(info.name);
                if let Some(param) = param {
                    route.push_str("-parameters--");
                    route.push_str(param);
                } else {
                    route.push_str("-summary");
                }
            } else {
                route.push_str(name);
                route.push('/');
                if let Some(param) = param {
                    route.push_str("#parameters--");
                    route.push_str(param);
                }
            }
        } else {
            route.push_str(rest);
        }

        if !route.contains('#') && !route.ends_with('/') {
            route.push('/');
        }

        Some(route)
    }
}

/// Render a code block to HTML.
fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
    let mut display = String::new();
    let mut compile = String::new();
    for line in text.lines() {
        if let Some(suffix) = line.strip_prefix(">>>") {
            compile.push_str(suffix);
            compile.push('\n');
        } else if let Some(suffix) = line.strip_prefix("<<< ") {
            display.push_str(suffix);
            display.push('\n');
        } else {
            display.push_str(line);
            display.push('\n');
            compile.push_str(line);
            compile.push('\n');
        }
    }

    let mut parts = lang.split(':');
    let lang = parts.next().unwrap_or(lang);

    let mut zoom: Option<[Abs; 4]> = None;
    let mut single = false;
    if let Some(args) = parts.next() {
        single = true;
        if !args.contains("single") {
            zoom = args
                .split(',')
                .take(4)
                .map(|s| Abs::pt(s.parse().unwrap()))
                .collect::<Vec<_>>()
                .try_into()
                .ok();
        }
    }

    if !matches!(lang, "example" | "typ") {
        let mut buf = String::from("<pre>");
        escape_html(&mut buf, &display).unwrap();
        buf.push_str("</pre>");
        return Html::new(buf);
    }

    let root = typst::syntax::parse(&display);
    let highlighted = Html::new(typst::ide::highlight_html(&root));
    if lang == "typ" {
        return Html::new(format!("<pre>{}</pre>", highlighted.as_str()));
    }

    let source = Source::new(SourceId::from_u16(0), Path::new("main.typ"), compile);
    let world = DocWorld(source);
    let mut frames = match typst::compile(&world) {
        Ok(doc) => doc.pages,
        Err(err) => {
            let msg = &err[0].message;
            panic!("while trying to compile:\n{text}:\n\nerror: {msg}");
        }
    };

    if let Some([x, y, w, h]) = zoom {
        frames[0].translate(Point::new(-x, -y));
        *frames[0].size_mut() = Size::new(w, h);
    }

    if single {
        frames.truncate(1);
    }

    resolver.example(highlighted, &frames)
}

/// Extract an attribute value from an HTML element.
fn html_attr<'a>(html: &'a str, attr: &str) -> Option<&'a str> {
    html.get(html_attr_range(html, attr)?)
}

/// Extract the range of the attribute value of an HTML element.
fn html_attr_range(html: &str, attr: &str) -> Option<Range<usize>> {
    let needle = format!("{attr}=\"");
    let offset = html.find(&needle)? + needle.len();
    let len = html[offset..].find('"')?;
    Some(offset..offset + len)
}

/// World for example compilations.
struct DocWorld(Source);

impl World for DocWorld {
    fn library(&self) -> &Prehashed<Library> {
        &LIBRARY
    }

    fn main(&self) -> &Source {
        &self.0
    }

    fn resolve(&self, _: &Path) -> FileResult<SourceId> {
        unimplemented!()
    }

    fn source(&self, id: SourceId) -> &Source {
        assert_eq!(id.into_u16(), 0, "invalid source id");
        &self.0
    }

    fn book(&self) -> &Prehashed<FontBook> {
        &FONTS.0
    }

    fn font(&self, id: usize) -> Option<Font> {
        Some(FONTS.1[id].clone())
    }

    fn file(&self, path: &Path) -> FileResult<Buffer> {
        Ok(FILES
            .get_file(path)
            .unwrap_or_else(|| panic!("failed to load {path:?}"))
            .contents()
            .into())
    }
}
