// SPDX-FileCopyrightText: 2020-2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

//! Parses HTML files generated by rustdoc.
//!
//! For details on the format of the parsed HTML files, check the following items in the
//! `html::render` module of `librustdoc` (in the Rust source):
//! - The `krate` and `render_item` methods of the `Context` struct are the main entry points for
//!   the rendering.
//! - The `print_item` and `item_*`functions generate the HTML for an item (module, struct, …).
//! - The `AllTypes::print` function generates the HTML for the `all.html` page using the
//!   `print_entries` function.

mod util;

use std::path;

use anyhow::Context;
use ego_tree::NodeRef;
use scraper::{ElementRef, Html, Node, Selector};

use crate::doc;

use util::NodeRefExt;

pub struct Parser {
    document: Html,
}

impl Parser {
    pub fn from_file(path: impl AsRef<path::Path>) -> anyhow::Result<Parser> {
        log::info!("Reading HTML from file '{}'", path.as_ref().display());
        let s = std::fs::read_to_string(path).context("Could not read HTML file")?;
        let document = Html::parse_document(&s);
        log::info!("HTML file parsed successfully");
        Ok(Parser { document })
    }

    pub fn from_string(s: impl Into<String>) -> anyhow::Result<Parser> {
        log::info!("Reading HTML from string");
        let document = Html::parse_document(&s.into());
        log::info!("HTML string parsed successfully");
        Ok(Parser { document })
    }

    pub fn find_item(&self, item: &str) -> anyhow::Result<Option<String>> {
        let item = select(&self.document, "ul.docblock li a")?
            .into_iter()
            .find(|e| e.text_contents() == item)
            .and_then(|e| e.get_attribute("href"));
        Ok(item)
    }

    pub fn find_member(&self, name: &doc::Fqn) -> anyhow::Result<Option<doc::ItemType>> {
        let member = get_member(&self.document, name.last())?;
        if let Some(member) = member {
            let id = member
                .get_attribute("id")
                .with_context(|| format!("The member {} does not have an ID", name))?;
            let ty = id.splitn(2, '.').next().unwrap().parse()?;
            Ok(Some(ty))
        } else {
            Ok(None)
        }
    }

    pub fn parse_item_doc(&self, name: &doc::Fqn, ty: doc::ItemType) -> anyhow::Result<doc::Doc> {
        log::info!("Parsing item documentation for '{}'", name);
        let definition_selector = match ty {
            doc::ItemType::Constant => "pre.const",
            doc::ItemType::Function => "pre.fn",
            doc::ItemType::Typedef => "pre.typedef",
            _ => ".docblock.type-decl",
        };
        let definition = select_first(&self.document, definition_selector)?;
        // Since Rust 1.54.0, the main description is wrapped in a details element
        let mut description = select_first(
            &self.document,
            "#main > details.top-doc > .docblock:not(.type-decl)",
        )?;
        if description.is_none() {
            description = select_first(&self.document, "#main > .docblock:not(.type-decl)")?;
        }

        let mut doc = doc::Doc::new(name.clone(), ty);
        doc.description = description.map(|e| element_ref_to_text(&e));
        doc.definition = definition.map(|e| element_ref_to_code(&e));

        let members = vec![
            get_variants(&self.document, name)?,
            get_fields(&self.document, name)?,
            get_assoc_types(&self.document, name)?,
            get_methods(&self.document, name)?,
            get_implementations(&self.document, name)?,
        ];
        for (ty, groups) in members.into_iter() {
            if !groups.is_empty() {
                doc.groups.insert(ty, groups);
            }
        }

        Ok(doc)
    }

    pub fn parse_member_doc(&self, name: &doc::Fqn, ty: doc::ItemType) -> anyhow::Result<doc::Doc> {
        log::info!("Parsing member documentation for '{}'", name);
        let heading = select_first(&self.document, &get_member_selector(ty, name.last()))?
            .with_context(|| format!("Could not find member {}", name))?;

        // Since Rust 1.54.0, the <code> element is replaced with a <h4 class="code-header">
        let code = if let Some(code) = select_first_in_element(&heading, "code")? {
            Ok(code)
        } else if let Some(code) = select_first_in_element(&heading, "h4.code-header")? {
            Ok(code)
        } else {
            Err(anyhow::anyhow!(
                "The member {} does not have a definition",
                name
            ))
        }?;

        // Since Rust 1.54.0, there is an additional summary element around the definition
        // ElementRef derefs to NodeRef so we can call next_sibling_element directly
        let mut docblock = (*heading).next_sibling_element();
        if docblock.is_none() {
            docblock = (*heading).parent().and_then(|p| p.next_sibling_element());
        }

        let mut doc = doc::Doc::new(name.clone(), ty);
        doc.definition = Some(element_ref_to_code(&code));
        doc.description = docblock.and_then(|n| ElementRef::wrap(n)).map(|e| element_ref_to_text(&e));
        Ok(doc)
    }

    pub fn parse_module_doc(&self, name: &doc::Fqn) -> anyhow::Result<doc::Doc> {
        log::info!("Parsing module documentation for '{}'", name);
        let description = select_first(&self.document, ".docblock")?;

        let mut doc = doc::Doc::new(name.clone(), doc::ItemType::Module);
        doc.description = description.map(|e| element_ref_to_text(&e));
        for item_type in MODULE_MEMBER_TYPES {
            let mut group = doc::MemberGroup::new(None);
            group.members = get_members(&self.document, name, *item_type)?;
            if !group.members.is_empty() {
                doc.groups.insert(*item_type, vec![group]);
            }
        }
        Ok(doc)
    }

    pub fn find_examples(&self) -> anyhow::Result<Vec<doc::Example>> {
        let examples = select(&self.document, ".rust-example-rendered")?;
        Ok(examples
            .into_iter()
            .map(|e| get_example(&e))
            .collect())
    }
}

fn element_ref_to_text(e: &ElementRef<'_>) -> doc::Text {
    doc::Text {
        plain: element_ref_to_plain(e),
        html: e.html(),
    }
}

fn element_ref_to_code(e: &ElementRef<'_>) -> doc::Code {
    doc::Code::new(element_ref_to_plain(e))
}

fn element_ref_to_plain(e: &ElementRef<'_>) -> String {
    let mut s = String::new();
    push_element_to_text(&mut s, e);
    s.trim().to_string()
}

fn push_element_to_text(s: &mut String, e: &ElementRef<'_>) {
    if e.has_class("notable-traits") {
        return;
    }

    let is_docblock = e.has_class("docblock");
    let tag = e.value().name();

    let add_newline = if tag == "br" {
        true
    } else if e.has_class("fmt-newline") || is_docblock {
        !s.is_empty() && !s.ends_with('\n')
    } else {
        false
    };
    if add_newline {
        s.push('\n');
    }

    for child in e.children() {
        push_node_to_text(s, child);
    }

    if is_docblock && !s.is_empty() && !s.ends_with('\n') {
        s.push('\n');
    }
}

fn push_node_to_text(s: &mut String, node: NodeRef<'_, Node>) {
    match node.value() {
        Node::Text(text) => {
            s.push_str(text);
        }
        Node::Element(_) => {
            if let Some(e) = ElementRef::wrap(node) {
                push_element_to_text(s, &e);
            }
        }
        _ => {}
    }
}

fn select<'a>(
    document: &'a Html,
    selector: &str,
) -> anyhow::Result<Vec<ElementRef<'a>>> {
    let sel = Selector::parse(selector)
        .map_err(|e| anyhow::anyhow!("Could not parse selector {}: {:?}", selector, e))?;
    Ok(document.select(&sel).collect())
}

fn select_first<'a>(
    document: &'a Html,
    selector: &str,
) -> anyhow::Result<Option<ElementRef<'a>>> {
    let sel = Selector::parse(selector)
        .map_err(|e| anyhow::anyhow!("Could not parse selector {}: {:?}", selector, e))?;
    Ok(document.select(&sel).next())
}

fn select_in_element<'a>(
    element: &ElementRef<'a>,
    selector: &str,
) -> anyhow::Result<Vec<ElementRef<'a>>> {
    let sel = Selector::parse(selector)
        .map_err(|e| anyhow::anyhow!("Could not parse selector {}: {:?}", selector, e))?;
    Ok(element.select(&sel).collect())
}

fn select_first_in_element<'a>(
    element: &ElementRef<'a>,
    selector: &str,
) -> anyhow::Result<Option<ElementRef<'a>>> {
    let sel = Selector::parse(selector)
        .map_err(|e| anyhow::anyhow!("Could not parse selector {}: {:?}", selector, e))?;
    Ok(element.select(&sel).next())
}

fn select_first_in_node<'a>(
    node: &NodeRef<'a, Node>,
    selector: &str,
) -> anyhow::Result<Option<ElementRef<'a>>> {
    if let Some(e) = ElementRef::wrap(*node) {
        select_first_in_element(&e, selector)
    } else {
        Ok(None)
    }
}

fn get_example(element: &ElementRef<'_>) -> doc::Example {
    // ElementRef derefs to NodeRef
    let description_element = (*element).previous_sibling_element();
    let description = description_element
        .and_then(|n| ElementRef::wrap(n))
        .and_then(|n| {
            if n.text_contents().ends_with(':') {
                Some(n)
            } else {
                None
            }
        })
        .map(|e| element_ref_to_text(&e));
    doc::Example::new(description, element_ref_to_code(element))
}

const MODULE_MEMBER_TYPES: &[doc::ItemType] = &[
    doc::ItemType::ExternCrate,
    doc::ItemType::Import,
    doc::ItemType::Primitive,
    doc::ItemType::Module,
    doc::ItemType::Macro,
    doc::ItemType::Struct,
    doc::ItemType::Enum,
    doc::ItemType::Constant,
    doc::ItemType::Static,
    doc::ItemType::Trait,
    doc::ItemType::Function,
    doc::ItemType::Typedef,
    doc::ItemType::Union,
];

fn get_id_part_elem(element: &ElementRef<'_>, i: usize) -> Option<String> {
    if let Some(id) = element.get_attribute("id") {
        id.splitn(2, '.')
            .nth(i)
            .and_then(|s| s.splitn(2, '-').next())
            .map(ToOwned::to_owned)
    } else {
        None
    }
}

fn get_id_part_node(node: &NodeRef<'_, Node>, i: usize) -> Option<String> {
    if let Some(id) = node.get_attribute("id") {
        id.splitn(2, '.')
            .nth(i)
            .and_then(|s| s.splitn(2, '-').next())
            .map(ToOwned::to_owned)
    } else {
        None
    }
}

fn get_fields(
    document: &Html,
    parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
    let ty = doc::ItemType::StructField;
    let mut fields = MemberDocs::new(parent, ty);
    let heading = select_first(document, &format!("#{}", get_item_group_id(ty)))?;

    // NodeRef is Copy so we can use by value in loops
    let mut next: Option<NodeRef<'_, Node>> = heading
        .as_ref()
        .and_then(|e| (*e).next_sibling_element());
    let mut name: Option<String> = None;
    let mut definition: Option<doc::Code> = None;

    while let Some(element) = next {
        if let Some(e) = ElementRef::wrap(element) {
            if e.is_element_name("span") && e.has_class("structfield") {
                fields.push(&mut name, &mut definition, None)?;
                name = get_id_part_elem(&e, 1);
                definition = Some(element_ref_to_code(&e));
            } else if e.is_element_name("div") {
                if e.has_class("docblock") {
                    fields.push(&mut name, &mut definition, Some(element_ref_to_text(&e)))?;
                }
            } else {
                fields.push(&mut name, &mut definition, None)?;
                break;
            }
        }
        next = element.next_sibling_element();
    }

    Ok((ty, fields.into_member_groups(None)))
}

fn get_methods(
    document: &Html,
    parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
    let ty = doc::ItemType::Method;
    let mut groups: Vec<doc::MemberGroup> = Vec::new();

    // Rust < 1.45
    groups.append(&mut get_method_groups(
        document,
        parent,
        "methods".to_owned(),
        ty,
        "h4",
    )?);
    // Rust >= 1.45, < 1.54.0
    groups.append(&mut get_method_groups(
        document,
        parent,
        "implementations".to_owned(),
        ty,
        "h4",
    )?);
    // Rust >= 1.54.0
    groups.append(&mut get_method_groups(
        document,
        parent,
        "implementations".to_owned(),
        ty,
        "h2",
    )?);

    let heading = select_first(document, "#deref-methods")?;
    if let Some(heading) = heading {
        let title = heading.text_contents();
        if let Some(impl_items) = (*heading).next_sibling_element() {
            let group = get_method_group(
                parent,
                Some(title),
                &impl_items,
                doc::ItemType::Method,
                "h4",
            )?;
            if let Some(group) = group {
                groups.push(group);
            }
        }
    }

    let heading = select_first(document, "#required-methods")?;
    if let Some(heading) = heading {
        if let Some(methods) = (*heading).next_sibling_element() {
            let title = "Required Methods";
            // Rust < 1.54.0
            let group = if let Some(group) = get_method_group(
                parent,
                Some(title.to_owned()),
                &methods,
                doc::ItemType::TyMethod,
                "h3",
            )? {
                Some(group)
            } else {
                // Rust >= 1.54.0
                get_method_group(
                    parent,
                    Some(title.to_owned()),
                    &methods,
                    doc::ItemType::TyMethod,
                    "h4",
                )?
            };
            if let Some(group) = group {
                groups.push(group);
            }
        }
    }

    let heading = select_first(document, "#provided-methods")?;
    if let Some(heading) = heading {
        if let Some(methods) = (*heading).next_sibling_element() {
            let title = "Provided Methods";
            // Rust < 1.54.0
            let group = if let Some(group) = get_method_group(
                parent,
                Some(title.to_owned()),
                &methods,
                doc::ItemType::TyMethod,
                "h3",
            )? {
                Some(group)
            } else {
                // Rust >= 1.54.0
                get_method_group(
                    parent,
                    Some(title.to_owned()),
                    &methods,
                    doc::ItemType::TyMethod,
                    "h4",
                )?
            };
            if let Some(group) = group {
                groups.push(group);
            }
        }
    }

    Ok((ty, groups))
}

fn get_assoc_types(
    document: &Html,
    parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
    let ty = doc::ItemType::AssocType;
    let mut groups: Vec<doc::MemberGroup> = Vec::new();

    let heading = select_first(document, "#associated-types")?;
    if let Some(heading) = heading {
        if let Some(methods) = (*heading).next_sibling_element() {
            // Rust < 1.54.0
            let group = if let Some(group) = get_method_group(
                parent,
                None,
                &methods,
                doc::ItemType::AssocType,
                "h3",
            )? {
                Some(group)
            } else {
                // Rust >= 1.54.0
                get_method_group(
                    parent,
                    None,
                    &methods,
                    doc::ItemType::AssocType,
                    "h4",
                )?
            };
            if let Some(group) = group {
                groups.push(group);
            }
        }
    }

    Ok((ty, groups))
}

fn get_method_groups(
    document: &Html,
    parent: &doc::Fqn,
    heading_id: String,
    ty: doc::ItemType,
    subheading_type: &str,
) -> anyhow::Result<Vec<doc::MemberGroup>> {
    let mut groups: Vec<doc::MemberGroup> = Vec::new();
    let heading = select_first(document, &format!("#{}", heading_id))?;
    let mut next: Option<NodeRef<'_, Node>> = heading
        .as_ref()
        .and_then(|e| (*e).next_sibling_element());

    while let Some(subheading) = next.take() {
        if let Some(e) = ElementRef::wrap(subheading) {
            if e.is_element_name("h3") && e.has_class("impl") {
                // Get first element child as title
                let title_node = subheading.children().find(|n| n.value().is_element());
                let impl_items_node = subheading.next_sibling_element();
                if let (Some(title), Some(impl_items)) = (title_node, impl_items_node) {
                    if let Some(group) =
                        get_impl_items(parent, &title, &impl_items, ty, subheading_type)?
                    {
                        groups.push(group);
                    }
                    next = impl_items.next_sibling_element();
                }
            } else if e.is_element_name("details") {
                let summary_node = subheading.children().find(|n| n.value().is_element());
                if let Some(summary) = summary_node {
                    let h3 = select_first_in_node(&summary, "h3")?;
                    if let Some(h3_elem) = h3 {
                        // Prefer first child code element, else use whole h3 node
                        let title: NodeRef<'_, Node> = (*h3_elem)
                            .children()
                            .find(|n| n.is_element_name("code"))
                            .unwrap_or(*h3_elem);
                        let impl_items_node = summary.next_sibling_element();
                        if let Some(impl_items) = impl_items_node {
                            if let Some(group) =
                                get_impl_items(parent, &title, &impl_items, ty, subheading_type)?
                            {
                                groups.push(group);
                            }
                            next = subheading.next_sibling_element();
                        }
                    }
                }
            }
        }
    }

    Ok(groups)
}

fn get_impl_items(
    parent: &doc::Fqn,
    title: &NodeRef<'_, Node>,
    impl_items: &NodeRef<'_, Node>,
    ty: doc::ItemType,
    subheading_type: &str,
) -> anyhow::Result<Option<doc::MemberGroup>> {
    let title = title.text_contents();
    if impl_items.is_element_name("div") && impl_items.has_class("impl-items") {
        get_method_group(parent, Some(title), impl_items, ty, subheading_type)
    } else {
        Ok(None)
    }
}

fn get_method_group(
    parent: &doc::Fqn,
    title: Option<String>,
    impl_items: &NodeRef<'_, Node>,
    ty: doc::ItemType,
    heading_type: &str,
) -> anyhow::Result<Option<doc::MemberGroup>> {
    let mut methods = MemberDocs::new(parent, ty);

    let mut name: Option<String> = None;
    let mut definition: Option<doc::Code> = None;
    for element in impl_items.children() {
        if element.is_element_name(heading_type) && element.has_class("method") {
            methods.push(&mut name, &mut definition, None)?;
            name = get_id_part_node(&element, 1);
            definition = select_first_in_node(&element, "code")?.map(|e| element_ref_to_code(&e));
        } else if element.is_element_name("div") && element.has_class("docblock") {
            if let Some(e) = ElementRef::wrap(element) {
                methods.push(&mut name, &mut definition, Some(element_ref_to_text(&e)))?;
            }
        } else if element.is_element_name("details") {
            // Since Rust 1.54.0, the heading and the docblock are wrapped in details and summary
            // elements.
            if let Some(div) = select_first_in_node(&element, "summary div.method")? {
                // div derefes to NodeRef
                if (*div).children().any(|n| n.is_element_name(heading_type)) {
                    methods.push(&mut name, &mut definition, None)?;
                    name = get_id_part_elem(&div, 1);
                    definition = select_first_in_element(&div, ".code-header")?
                        .map(|e| element_ref_to_code(&e));
                }
            }
            if let Some(docblock) = select_first_in_node(&element, "div.docblock")? {
                methods.push(
                    &mut name,
                    &mut definition,
                    Some(element_ref_to_text(&docblock)),
                )?;
            }
        }
    }

    Ok(methods.into_member_group(title))
}

fn get_variants(
    document: &Html,
    parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
    let ty = doc::ItemType::Variant;
    let mut variants = MemberDocs::new(parent, ty);
    let heading = select_first(document, &format!("#{}", get_item_group_id(ty)))?;

    let mut next: Option<NodeRef<'_, Node>> = heading
        .as_ref()
        .and_then(|e| (*e).next_sibling_element());
    let mut name: Option<String> = None;
    let mut definition: Option<doc::Code> = None;
    while let Some(element) = &next {
        if let Some(e) = ElementRef::wrap(*element) {
            if e.is_element_name("div") {
                if e.has_class("variant") {
                    variants.push(&mut name, &mut definition, None)?;
                    name = get_id_part_elem(&e, 1);
                    definition = Some(element_ref_to_code(&e));
                } else if e.has_class("docblock") {
                    variants.push(&mut name, &mut definition, Some(element_ref_to_text(&e)))?;
                }

                next = element.next_sibling_element();
            } else {
                variants.push(&mut name, &mut definition, None)?;
                break;
            }
        } else {
            next = element.next_sibling_element();
        }
    }

    Ok((ty, variants.into_member_groups(None)))
}

fn get_implementations(
    document: &Html,
    parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
    let mut groups: Vec<doc::MemberGroup> = Vec::new();

    let group_data = vec![
        // Rust < 1.45
        ("Trait Implementations", "implementations-list"),
        // Rust >= 1.45
        ("Trait Implementations", "trait-implementations-list"),
        (
            "Auto Trait Implementations",
            "synthetic-implementations-list",
        ),
        ("Blanket Implementations", "blanket-implementations-list"),
    ];

    for (title, id) in group_data {
        if let Some(group) = get_implementation_group(document, parent, title, id)? {
            groups.push(group);
        }
    }

    Ok((doc::ItemType::Impl, groups))
}

fn get_implementation_group(
    document: &Html,
    parent: &doc::Fqn,
    title: &str,
    list_id: &str,
) -> anyhow::Result<Option<doc::MemberGroup>> {
    let ty = doc::ItemType::Impl;
    let mut impls = MemberDocs::new(parent, ty);
    let list_div = select_first(document, &format!("#{}", list_id))?;

    if let Some(list_div) = list_div {
        // list_div derefs to NodeRef
        for item in (*list_div).children() {
            let h3: Option<NodeRef<'_, Node>> =
                if item.is_element_name("details") {
                    let summary = item.children().find(|n| n.value().is_element());
                    if let Some(summary) = summary {
                        select_first_in_node(&summary, "h3.impl, h3.code-header")?
                            .map(|e| *e)
                    } else {
                        None
                    }
                } else if item.is_element_name("h3") && item.has_class("impl") {
                    Some(item)
                } else if item.is_element_name("div") && item.has_class("impl") {
                    select_first_in_node(&item, "h3")?
                        .map(|e| *e)
                } else {
                    None
                };

            if let Some(h3) = h3 {
                let a = select_first_in_node(&h3, "a")?;
                let mut name = a.map(|n| n.text_contents());
                let first_child_code = h3
                    .children()
                    .find(|n| n.is_element_name("code"));
                let mut definition = Some(
                    first_child_code
                        .map(|n| {
                            if let Some(e) = ElementRef::wrap(n) {
                                element_ref_to_code(&e)
                            } else {
                                doc::Code::new(n.text_contents())
                            }
                        })
                        .unwrap_or_else(|| {
                            if let Some(e) = ElementRef::wrap(h3) {
                                element_ref_to_code(&e)
                            } else {
                                doc::Code::new(h3.text_contents())
                            }
                        }),
                );
                impls.push(&mut name, &mut definition, None)?;
            }
        }
    }

    impls.sort();

    Ok(impls.into_member_group(Some(title.to_owned())))
}

fn get_members(
    document: &Html,
    parent: &doc::Fqn,
    ty: doc::ItemType,
) -> anyhow::Result<Vec<doc::Doc>> {
    let mut members: Vec<doc::Doc> = Vec::new();
    if let Some(table) = select_first(document, &format!("#{} + table", get_item_group_id(ty)))? {
        let items: Vec<_> = select_in_element(&table, "td:first-child > :first-child")?;
        for item in items {
            let item_name = item.text_contents();
            // item derefs to NodeRef
            let docblock = (*item)
                .parent()
                .and_then(|n| n.next_sibling_element())
                .and_then(|n| ElementRef::wrap(n));

            let mut doc = doc::Doc::new(parent.child(&item_name), ty);
            doc.description = docblock.map(|e| element_ref_to_text(&e));
            members.push(doc);
        }
    }
    if let Some(div) = select_first(
        document,
        &format!("#{} + div.item-table", get_item_group_id(ty)),
    )? {
        // div derefs to NodeRef
        let mut iter = (*div).children();
        while let (Some(item), Some(docblock)) = (iter.next(), iter.next()) {
            if !item.has_class("module-item") || !docblock.has_class("docblock-short") {
                continue;
            }
            let item_name = item.text_contents();
            let mut doc = doc::Doc::new(parent.child(&item_name), ty);
            if let Some(e) = ElementRef::wrap(docblock) {
                doc.description = Some(element_ref_to_text(&e));
            }
            members.push(doc);
        }
    }
    Ok(members)
}

const MEMBER_TYPES: &[doc::ItemType] = &[
    doc::ItemType::StructField,
    doc::ItemType::Variant,
    doc::ItemType::AssocType,
    doc::ItemType::AssocConst,
    doc::ItemType::Method,
];

fn get_member<'a>(document: &'a Html, name: &str) -> anyhow::Result<Option<ElementRef<'a>>> {
    let selectors: Vec<_> = MEMBER_TYPES
        .iter()
        .map(|ty| get_member_selector(*ty, name))
        .collect();
    select_first(document, &selectors.join(", "))
}

fn get_member_selector(ty: doc::ItemType, name: &str) -> String {
    format!("#{}\\.{}", get_item_id(ty), name)
}

struct MemberDocs<'a> {
    docs: Vec<doc::Doc>,
    parent: &'a doc::Fqn,
    ty: doc::ItemType,
}

impl<'a> MemberDocs<'a> {
    pub fn new(parent: &'a doc::Fqn, ty: doc::ItemType) -> Self {
        Self {
            docs: Vec::new(),
            parent,
            ty,
        }
    }

    pub fn sort(&mut self) {
        self.docs.sort_by(|d1, d2| {
            d1.name
                .cmp(&d2.name)
                .then_with(|| d1.definition.cmp(&d2.definition))
        })
    }

    pub fn push(
        &mut self,
        name: &mut Option<String>,
        definition: &mut Option<doc::Code>,
        description: Option<doc::Text>,
    ) -> anyhow::Result<()> {
        let name = name.take();
        let definition = definition.take();

        if let Some(name) = name {
            let mut doc = doc::Doc::new(self.parent.child(&name), self.ty);
            doc.definition = definition;
            doc.description = description;
            self.docs.push(doc);
        }
        Ok(())
    }

    pub fn into_member_group(self, title: Option<String>) -> Option<doc::MemberGroup> {
        if self.docs.is_empty() {
            None
        } else {
            let mut group = doc::MemberGroup::new(title);
            group.members = self.docs;
            Some(group)
        }
    }

    pub fn into_member_groups(self, title: Option<String>) -> Vec<doc::MemberGroup> {
        self.into_member_group(title).into_iter().collect()
    }
}

impl<'a> From<MemberDocs<'a>> for Vec<doc::Doc> {
    fn from(md: MemberDocs<'a>) -> Self {
        md.docs
    }
}

fn get_item_id(ty: doc::ItemType) -> &'static str {
    use doc::ItemType;

    match ty {
        ItemType::Module => "mod",
        ItemType::ExternCrate => "externcrate",
        ItemType::Import => "import",
        ItemType::Struct => "struct",
        ItemType::Union => "union",
        ItemType::Enum => "enum",
        ItemType::Function => "fn",
        ItemType::Typedef => "type",
        ItemType::Static => "static",
        ItemType::Trait => "trait",
        ItemType::Impl => "impl",
        ItemType::TyMethod => "tymethod",
        ItemType::Method => "method",
        ItemType::StructField => "structfield",
        ItemType::Variant => "variant",
        ItemType::Macro => "macro",
        ItemType::Primitive => "primitive",
        ItemType::AssocType => "associatedtype",
        ItemType::Constant => "constant",
        ItemType::AssocConst => "associatedconstant",
        ItemType::ForeignType => "foreigntype",
        ItemType::Keyword => "keyword",
        ItemType::OpaqueTy => "opaque",
        ItemType::ProcAttribute => "attr",
        ItemType::ProcDerive => "derive",
        ItemType::TraitAlias => "traitalias",
    }
}

fn get_item_group_id(ty: doc::ItemType) -> &'static str {
    use doc::ItemType;

    match ty {
        ItemType::Module => "modules",
        ItemType::ExternCrate => "extern-crates",
        ItemType::Import => "imports",
        ItemType::Struct => "structs",
        ItemType::Enum => "enums",
        ItemType::Function => "functions",
        ItemType::Typedef => "types",
        ItemType::Static => "statics",
        ItemType::Trait => "traits",
        ItemType::Impl => "impls",
        ItemType::TyMethod => "required-methods",
        ItemType::Method => "methods",
        ItemType::StructField => "fields",
        ItemType::Variant => "variants",
        ItemType::Macro => "macros",
        ItemType::Primitive => "primitives",
        ItemType::AssocType => "associated-types",
        ItemType::Constant => "constants",
        ItemType::AssocConst => "associated-consts",
        ItemType::Union => "unions",
        ItemType::ForeignType => "foreign-types",
        ItemType::Keyword => "keywords",
        ItemType::OpaqueTy => "opaque-types",
        ItemType::ProcAttribute => "proc-attributes",
        ItemType::ProcDerive => "proc-derives",
        ItemType::TraitAlias => "trait-aliases",
    }
}

#[cfg(test)]
mod tests {
    use crate::doc;
    use crate::test_utils::{with_rustdoc, Format};

    #[test]
    fn test_find_item() {
        with_rustdoc("*", Format::all(), |_, _, path| {
            let path = path.join("kuchiki").join("all.html");
            let parser = super::Parser::from_file(path).unwrap();

            assert_eq!(None, parser.find_item("foobar").unwrap());
            assert_eq!(
                Some("struct.NodeRef.html".to_owned()),
                parser.find_item("NodeRef").unwrap()
            );
        });
    }

    #[test]
    fn test_parse_item_doc() {
        with_rustdoc("*", Format::all(), |_, _, path| {
            let path = path.join("kuchiki").join("struct.NodeRef.html");
            let name: doc::Fqn = "kuchiki::NodeRef".to_owned().into();
            let doc = super::Parser::from_file(path)
                .unwrap()
                .parse_item_doc(&name, doc::ItemType::Struct)
                .unwrap();

            assert_eq!(name, doc.name);
            assert_eq!(doc::ItemType::Struct, doc.ty);
            assert!(doc.definition.is_some());
            assert!(doc.description.is_some());
        });
    }

    #[test]
    fn test_find_member() {
        with_rustdoc("*", Format::all(), |_, _, path| {
            let path = path.join("kuchiki").join("struct.NodeDataRef.html");
            let name: doc::Fqn = "kuchiki::NodeDataRef::as_node".to_owned().into();
            let ty = super::Parser::from_file(path)
                .unwrap()
                .find_member(&name)
                .unwrap();
            assert_eq!(Some(doc::ItemType::Method), ty);
        });
    }

    #[test]
    fn test_parse_member_doc() {
        with_rustdoc("*", Format::all(), |_, _, path| {
            let path = path.join("kuchiki").join("struct.NodeDataRef.html");
            let name: doc::Fqn = "kuchiki::NodeDataRef::as_node".to_owned().into();
            let doc = super::Parser::from_file(path)
                .unwrap()
                .parse_member_doc(&name, doc::ItemType::Method)
                .unwrap();

            assert_eq!(name, doc.name);
            assert_eq!(doc::ItemType::Method, doc.ty);
            let definition = doc.definition.unwrap();
            assert_eq!(
                doc::Code::new("pub fn as_node(&self) -> &NodeRef".to_owned()),
                definition
            );
            assert!(doc.description.is_some());
        });
    }
}
