use crate::Declaration;
use crate::diff::JsDocDiff;
use crate::html::DiffStatus;
use crate::html::DocNodeWithContext;
use crate::html::FileMode;
use crate::html::GenerateCtx;
use crate::html::RenderContext;
use crate::html::ShortPath;
use crate::html::partition::flatten_namespace;
use crate::html::render_context::ToCEntry;
use crate::html::usage::UsagesCtx;
use crate::js_doc::JsDoc;
use crate::js_doc::JsDocTag;
use crate::node::DeclarationDef;
use crate::node::DocNodeKind;
use deno_ast::swc::ast::Accessibility;
use deno_ast::swc::atoms::once_cell::sync::Lazy;
use indexmap::IndexSet;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::sync::Arc;

lazy_static! {
  static ref TARGET_RE: Regex = Regex::new(r"\s*\* ?|\.").unwrap();
}

pub enum IdKind {
  Constructor,
  ConstructSignature,
  Property,
  Method,
  Function,
  Variable,
  Class,
  Enum,
  Interface,
  TypeAlias,
  Namespace,
  Accessor,
  Parameter,
  Return,
  TypeParam,
  Throws,
  IndexSignature,
  CallSignature,
  Example,
}

impl IdKind {
  fn as_str(&self) -> &'static str {
    match self {
      IdKind::Constructor => "constructor",
      IdKind::ConstructSignature => "construct_signature",
      IdKind::Property => "property",
      IdKind::Method => "method",
      IdKind::Function => "function",
      IdKind::Variable => "variable",
      IdKind::Class => "class",
      IdKind::Enum => "enum",
      IdKind::Interface => "interface",
      IdKind::TypeAlias => "typeAlias",
      IdKind::Namespace => "namespace",
      IdKind::Accessor => "accessor",
      IdKind::Parameter => "parameter",
      IdKind::Return => "return",
      IdKind::TypeParam => "type_param",
      IdKind::Throws => "throws",
      IdKind::IndexSignature => "index_signature",
      IdKind::CallSignature => "call_signature",
      IdKind::Example => "example",
    }
  }
}

pub struct IdBuilder<'a> {
  ctx: &'a RenderContext<'a>,
  components: Vec<Cow<'a, str>>,
}

impl<'a> IdBuilder<'a> {
  pub fn new(ctx: &'a RenderContext<'a>) -> Self {
    let mut builder = Self {
      ctx,
      components: Vec::new(),
    };

    if let Some(prefix) = &ctx.ctx.id_prefix {
      builder.components.push(Cow::Borrowed(prefix));
    }

    builder
  }

  pub fn new_with_parent(ctx: &'a RenderContext<'a>, parent: &Id) -> Self {
    let mut builder = Self::new(ctx);

    let component = parent.as_ref();
    if !component.is_empty() {
      builder
        .components
        .push(Cow::Owned(sanitize_id_part(component)));
    }

    builder
  }

  pub fn kind(mut self, kind: IdKind) -> Self {
    self.components.push(Cow::Borrowed(kind.as_str()));
    self
  }

  pub fn name(mut self, name: &str) -> Self {
    if !name.is_empty() {
      self.components.push(Cow::Owned(sanitize_id_part(name)));
    }
    self
  }

  pub fn index(mut self, index: usize) -> Self {
    self.components.push(Cow::Owned(index.to_string()));
    self
  }

  pub fn build(self) -> Id {
    self.ctx.toc.anchorize(&self.components.join("_"))
  }

  /// Build an ID without registering it in the anchorizer.
  /// Use this for IDs that reference anchors on other pages (e.g. href targets).
  pub fn build_unregistered(self) -> Id {
    self.ctx.toc.sanitize(&self.components.join("_"))
  }
}

#[derive(
  Debug,
  Clone,
  Serialize,
  Deserialize,
  Ord,
  PartialOrd,
  Eq,
  PartialEq,
  Hash,
  Default,
)]
pub struct Id(String);

impl Id {
  pub(crate) fn as_str(&self) -> &str {
    self.0.as_str()
  }

  pub(crate) fn from_raw(s: String) -> Self {
    Id(s)
  }

  pub fn empty() -> Self {
    Id(String::new())
  }
}

impl Display for Id {
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    write!(f, "{}", self.0)
  }
}

impl AsRef<str> for Id {
  fn as_ref(&self) -> &str {
    self.0.as_str()
  }
}

fn sanitize_id_part(part: &str) -> String {
  html_escape::encode_quoted_attribute(&TARGET_RE.replace_all(part, "_"))
    .into_owned()
}

/// A container to hold a list of symbols with their namespaces:
///
/// ["setTimeout"]
/// ["Deno", "read"]
/// ["Deno", "errors"]
/// ["Deno", "errors", "HttpError"]
#[derive(Clone, Debug)]
pub(crate) struct NamespacedSymbols(
  Arc<HashMap<Vec<String>, Option<Arc<ShortPath>>>>,
);

impl NamespacedSymbols {
  pub(crate) fn new(
    ctx: &GenerateCtx,
    doc_nodes: &[DocNodeWithContext],
  ) -> Self {
    let symbols = compute_namespaced_symbols(
      ctx,
      Box::new(doc_nodes.iter().map(Cow::Borrowed)),
    );
    Self(Arc::new(symbols))
  }

  pub(crate) fn get(&self, path: &[String]) -> Option<&Option<Arc<ShortPath>>> {
    self.0.get(path)
  }
}

pub fn compute_namespaced_symbols<'a>(
  ctx: &'a GenerateCtx,
  symbols: Box<dyn Iterator<Item = Cow<'a, DocNodeWithContext>> + 'a>,
) -> HashMap<Vec<String>, Option<Arc<ShortPath>>> {
  let mut namespaced_symbols =
    HashMap::<Vec<String>, Option<Arc<ShortPath>>>::new();

  // Reusable buffer for building drilldown paths
  let mut path_buf = Vec::new();

  for symbol in symbols {
    let name_path: Arc<[String]> = symbol.sub_qualifier().into();
    // Precompute prefix (ns_qualifiers) once per symbol
    let ns_prefix = &*symbol.ns_qualifiers;
    let origin = Some(symbol.origin.clone());

    // Helper: build a drilldown path by reusing the buffer
    let mut build_drilldown_path =
      |member_name: &str, is_static: bool| -> Vec<String> {
        path_buf.clear();
        path_buf.extend(ns_prefix.iter().cloned());
        path_buf.extend(
          qualify_drilldown_name(symbol.get_name(), member_name, is_static)
            .split('.')
            .map(|part| part.to_string()),
        );
        path_buf.clone()
      };

    for decl in &symbol.declarations {
      // TODO: handle export aliasing

      match &decl.def {
        DeclarationDef::Class(class_def) => {
          for method in &class_def.methods {
            let path = build_drilldown_path(&method.name, method.is_static);
            namespaced_symbols.insert(path, origin.clone());
          }
          for property in &class_def.properties {
            let path = build_drilldown_path(&property.name, property.is_static);
            namespaced_symbols.insert(path, origin.clone());
          }
        }
        DeclarationDef::Interface(interface_def) => {
          for method in &interface_def.methods {
            let path = build_drilldown_path(&method.name, true);
            namespaced_symbols.insert(path, origin.clone());
          }
          for property in &interface_def.properties {
            let path = build_drilldown_path(&property.name, true);
            namespaced_symbols.insert(path, origin.clone());
          }
        }
        DeclarationDef::TypeAlias(type_alias_def) => {
          if let crate::ts_type::TsTypeDefKind::TypeLiteral(type_literal) =
            &type_alias_def.ts_type.kind
          {
            for method in &type_literal.methods {
              let path = build_drilldown_path(&method.name, true);
              namespaced_symbols.insert(path, origin.clone());
            }
            for property in &type_literal.properties {
              let path = build_drilldown_path(&property.name, true);
              namespaced_symbols.insert(path, origin.clone());
            }
          }
        }
        DeclarationDef::Variable(variable_def) => {
          if let Some(type_literal) =
            variable_def.ts_type.as_ref().and_then(|ts_type| {
              if let crate::ts_type::TsTypeDefKind::TypeLiteral(tl) =
                &ts_type.kind
              {
                Some(tl)
              } else {
                None
              }
            })
          {
            for method in &type_literal.methods {
              let path = build_drilldown_path(&method.name, true);
              namespaced_symbols.insert(path, origin.clone());
            }
            for property in &type_literal.properties {
              let path = build_drilldown_path(&property.name, true);
              namespaced_symbols.insert(path, origin.clone());
            }
          }
        }
        _ => {}
      }

      namespaced_symbols.insert(name_path.to_vec(), origin.clone());

      if matches!(decl.def, DeclarationDef::Namespace(..)) {
        let children =
          symbol.namespace_children.as_ref().unwrap().iter().flat_map(
            |element| {
              element.declarations.iter().flat_map(|decl| {
                if let Some(reference_def) = decl.reference_def() {
                  Box::new(
                    ctx.resolve_reference(Some(&symbol), &reference_def.target),
                  )
                    as Box<dyn Iterator<Item = Cow<DocNodeWithContext>>>
                } else {
                  Box::new(std::iter::once(Cow::Borrowed(element))) as _
                }
              })
            },
          );

        namespaced_symbols
          .extend(compute_namespaced_symbols(ctx, Box::new(children)))
      }
    }
  }

  namespaced_symbols
}

#[derive(Clone, Default)]
pub struct NamespacedGlobalSymbols(Arc<HashMap<Vec<String>, String>>);

impl NamespacedGlobalSymbols {
  pub fn new(symbols: HashMap<Vec<String>, String>) -> Self {
    Self(Arc::new(symbols))
  }

  pub fn get(&self, path: &[String]) -> Option<&String> {
    self.0.get(path)
  }
}

/// Different current and target locations
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum UrlResolveKind<'a> {
  Root,
  AllSymbols,
  Category {
    category: &'a str,
  },
  File {
    file: &'a ShortPath,
  },
  Symbol {
    file: &'a ShortPath,
    symbol: &'a str,
  },
}

impl UrlResolveKind<'_> {
  pub fn get_file(&self) -> Option<&ShortPath> {
    match self {
      UrlResolveKind::Root => None,
      UrlResolveKind::AllSymbols => None,
      UrlResolveKind::Category { .. } => None,
      UrlResolveKind::File { file } => Some(file),
      UrlResolveKind::Symbol { file, .. } => Some(file),
    }
  }
}

pub fn href_path_resolve(
  current: UrlResolveKind,
  target: UrlResolveKind,
) -> String {
  let backs = match current {
    UrlResolveKind::File { file } => "../".repeat(if file.is_main {
      1
    } else {
      file.path.split('/').count()
    }),
    UrlResolveKind::Symbol { file, .. } => "../".repeat(if file.is_main {
      1
    } else {
      file.path.split('/').count() + 1
    }),
    UrlResolveKind::Root => String::new(),
    UrlResolveKind::AllSymbols => String::from("./"),
    UrlResolveKind::Category { .. } => String::from("./"),
  };

  match target {
    UrlResolveKind::Root => backs,
    UrlResolveKind::File { file: target_file } if target_file.is_main => backs,
    UrlResolveKind::AllSymbols => format!("{backs}./all_symbols.html"),
    UrlResolveKind::Symbol {
      file: target_file,
      symbol: target_symbol,
      ..
    } => {
      format!(
        "{backs}./{}/~/{}.html",
        target_file.path,
        sanitize_symbol_path_part(target_symbol)
      )
    }
    UrlResolveKind::File { file: target_file } => {
      format!("{backs}./{}/index.html", target_file.path)
    }
    UrlResolveKind::Category { category } => {
      format!("{backs}./{}.html", slugify(category))
    }
  }
}

/// A trait used to define various functions used to resolve urls.
pub trait HrefResolver: Send + Sync {
  fn resolve_path(
    &self,
    current: UrlResolveKind,
    target: UrlResolveKind,
  ) -> String;

  /// Resolver for global symbols, like the Deno namespace or other built-ins
  fn resolve_global_symbol(&self, symbol: &[String]) -> Option<String>;

  /// Resolver for symbols from non-relative imports
  fn resolve_import_href(&self, symbol: &[String], src: &str)
  -> Option<String>;

  /// Resolve the URL used in source code link buttons.
  fn resolve_source(&self, location: &crate::Location) -> Option<String>;

  /// Resolve external JSDoc module links.
  /// Returns a tuple with link and title.
  fn resolve_external_jsdoc_module(
    &self,
    module: &str,
    symbol: Option<&str>,
  ) -> Option<(String, String)>;
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BreadcrumbCtx {
  pub name: String,
  pub href: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BreadcrumbsCtx {
  pub root: BreadcrumbCtx,
  pub current_entrypoint: Option<BreadcrumbCtx>,
  pub entrypoints: Vec<BreadcrumbCtx>,
  pub symbol: Vec<BreadcrumbCtx>,
}

impl BreadcrumbsCtx {
  pub const TEMPLATE: &'static str = "breadcrumbs";

  pub fn to_strings(&self) -> Vec<Cow<'_, str>> {
    let mut title_parts = vec![Cow::Borrowed(self.root.name.as_str())];

    if let Some(entrypoint) = &self.current_entrypoint {
      title_parts.push(Cow::Borrowed(entrypoint.name.as_str()));
    }

    if !self.symbol.is_empty() {
      title_parts.push(Cow::Owned(
        self
          .symbol
          .iter()
          .map(|crumb| crumb.name.as_str())
          .collect::<Vec<_>>()
          .join("."),
      ));
    }

    title_parts
  }
}

#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
pub struct DocNodeKindCtx {
  pub kind: Cow<'static, str>,
  pub char: char,
  pub title: Cow<'static, str>,
  pub title_lowercase: Cow<'static, str>,
  pub title_plural: Cow<'static, str>,
}

impl From<super::DrilldownKind> for DocNodeKindCtx {
  fn from(kind: super::DrilldownKind) -> Self {
    let (char, kind, title, title_lowercase, title_plural) = match kind {
      super::DrilldownKind::Property => {
        ('p', "Property", "Property", "property", "Properties")
      }
      super::DrilldownKind::Method(_) => {
        ('m', "Method", "Method", "method", "Methods")
      }
    };

    Self {
      kind: kind.into(),
      char,
      title: title.into(),
      title_lowercase: title_lowercase.into(),
      title_plural: title_plural.into(),
    }
  }
}

impl From<DocNodeKind> for DocNodeKindCtx {
  fn from(kind: DocNodeKind) -> Self {
    let (char, kind, title, title_lowercase, title_plural) = match kind {
      DocNodeKind::Function => {
        ('f', "Function", "Function", "function", "Functions")
      }
      DocNodeKind::Variable => {
        ('v', "Variable", "Variable", "variable", "Variables")
      }
      DocNodeKind::Class => ('c', "Class", "Class", "class", "Classes"),
      DocNodeKind::Enum => ('E', "Enum", "Enum", "enum", "Enums"),
      DocNodeKind::Interface => {
        ('I', "Interface", "Interface", "interface", "Interfaces")
      }
      DocNodeKind::TypeAlias => {
        ('T', "TypeAlias", "Type Alias", "type alias", "Type Aliases")
      }
      DocNodeKind::Namespace => {
        ('N', "Namespace", "Namespace", "namespace", "Namespaces")
      }
      DocNodeKind::Reference => {
        unreachable!()
      }
    };

    Self {
      kind: kind.into(),
      char,
      title: title.into(),
      title_lowercase: title_lowercase.into(),
      title_plural: title_plural.into(),
    }
  }
}

#[derive(
  Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq, Hash,
)]
pub struct AnchorCtx {
  pub id: Id,
}

impl AnchorCtx {
  pub const TEMPLATE: &'static str = "anchor";

  pub fn new(id: Id) -> Self {
    Self { id }
  }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case", tag = "kind", content = "content")]
pub enum SectionContentCtx {
  DocEntry(Vec<DocEntryCtx>),
  Example(Vec<super::jsdoc::ExampleCtx>),
  IndexSignature(Vec<super::symbols::class::IndexSignatureCtx>),
  NamespaceSection(Vec<super::namespace::NamespaceNodeCtx>),
  See(Vec<String>),
  Empty,
}

#[derive(
  Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq, Hash,
)]
pub struct SectionHeaderCtx {
  pub title: String,
  pub anchor: AnchorCtx,
  pub href: Option<String>,
  pub doc: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SectionCtx {
  pub header: Option<SectionHeaderCtx>,
  pub content: SectionContentCtx,
}

impl SectionCtx {
  pub const TEMPLATE: &'static str = "section";

  pub fn new(
    render_context: &RenderContext,
    title: &str,
    content: SectionContentCtx,
  ) -> Self {
    let header = if !title.is_empty() {
      let id = render_context.toc.anchorize(title);
      render_context.toc.add_entry(1, title, &id);

      Some(SectionHeaderCtx {
        title: title.to_string(),
        anchor: AnchorCtx::new(id),
        href: None,
        doc: None,
      })
    } else {
      None
    };

    match &content {
      SectionContentCtx::DocEntry(entries) => {
        for entry in entries {
          let Some(name) = &entry.name else {
            continue;
          };

          render_context.toc.add_entry(2, name, &entry.anchor.id);
        }
      }
      SectionContentCtx::Example(examples) => {
        for example in examples {
          render_context.toc.add_entry(
            2,
            &super::jsdoc::strip(render_context, &example.title),
            &example.anchor.id,
          );
        }
      }
      SectionContentCtx::IndexSignature(_) => {}
      SectionContentCtx::NamespaceSection(nodes) => {
        for node in nodes {
          render_context.toc.add_entry(2, &node.name, &node.anchor.id);
        }
      }
      SectionContentCtx::See(_) => {}
      SectionContentCtx::Empty => {}
    }

    Self { header, content }
  }
}

#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
pub enum Tag {
  New,
  Abstract,
  Deprecated,
  Writeonly,
  Readonly,
  Protected,
  Private,
  Optional,
  Unstable,
  Permissions(Box<[Box<str>]>),
  Other(Box<str>),
}

impl Tag {
  pub const TEMPLATE: &'static str = "tag";

  pub fn from_accessibility(
    accessibility: Option<Accessibility>,
  ) -> Option<Self> {
    match accessibility? {
      Accessibility::Public => None,
      Accessibility::Protected => Some(Tag::Protected),
      Accessibility::Private => Some(Tag::Private),
    }
  }

  pub fn from_js_doc(js_doc: &JsDoc) -> IndexSet<Tag> {
    js_doc
      .tags
      .iter()
      .filter_map(|tag| match tag {
        JsDocTag::Deprecated { .. } => Some(Tag::Deprecated),
        _ => None,
      })
      .collect()
  }
}

#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TagDiffKind {
  Added,
  Removed,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TagCtx {
  #[serde(flatten)]
  pub tag: Tag,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub diff: Option<TagDiffKind>,
}

pub fn compute_tag_ctx(
  current: IndexSet<Tag>,
  old: Option<IndexSet<Tag>>,
) -> Vec<TagCtx> {
  match old {
    None => current
      .into_iter()
      .map(|tag| TagCtx { tag, diff: None })
      .collect(),
    Some(old) => {
      let mut result = Vec::new();
      for tag in &current {
        let diff = if old.contains(tag) {
          None
        } else {
          Some(TagDiffKind::Added)
        };
        result.push(TagCtx {
          tag: tag.clone(),
          diff,
        });
      }
      for tag in &old {
        if !current.contains(tag) {
          result.push(TagCtx {
            tag: tag.clone(),
            diff: Some(TagDiffKind::Removed),
          });
        }
      }
      result
    }
  }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DocEntryCtx {
  pub name_prefix: Option<Cow<'static, str>>,
  name: Option<String>,
  name_href: Option<String>,
  content: String,
  pub anchor: AnchorCtx,
  pub tags: Vec<TagCtx>,
  js_doc: Option<String>,
  source_href: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub diff_status: Option<DiffStatus>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub old_content: Option<String>,
}

impl DocEntryCtx {
  pub const TEMPLATE: &'static str = "doc_entry";

  #[allow(clippy::too_many_arguments)]
  pub fn new(
    ctx: &RenderContext,
    id: Id,
    name: Option<String>,
    name_href: Option<String>,
    content: &str,
    tags: IndexSet<Tag>,
    jsdoc: Option<&str>,
    location: &crate::Location,
    diff_status: Option<DiffStatus>,
    old_content: Option<String>,
    old_tags: Option<IndexSet<Tag>>,
    js_doc_diff: Option<&JsDocDiff>,
  ) -> Self {
    let maybe_jsdoc = if let Some(doc_change) =
      js_doc_diff.and_then(|d| d.doc_change.as_ref())
    {
      let old_doc = doc_change.old.as_deref().unwrap_or_default();
      let new_doc = jsdoc.unwrap_or_default();
      crate::html::jsdoc::render_docs_with_diff(ctx, old_doc, new_doc)
    } else {
      jsdoc.map(|doc| crate::html::jsdoc::render_markdown(ctx, doc, true))
    };
    let source_href = ctx.ctx.href_resolver.resolve_source(location);

    DocEntryCtx {
      name_prefix: None,
      name,
      name_href,
      content: content.to_string(),
      anchor: AnchorCtx::new(id),
      tags: compute_tag_ctx(tags, old_tags),
      js_doc: maybe_jsdoc,
      source_href,
      diff_status,
      old_content,
    }
  }

  #[allow(clippy::too_many_arguments)]
  pub fn removed(
    ctx: &RenderContext,
    id: Id,
    name: Option<String>,
    name_href: Option<String>,
    content: &str,
    tags: IndexSet<Tag>,
    jsdoc: Option<&str>,
    location: &crate::Location,
  ) -> Self {
    Self::new(
      ctx,
      id,
      name,
      name_href,
      content,
      tags,
      jsdoc,
      location,
      Some(DiffStatus::Removed),
      None,
      None,
      None,
    )
  }
}

pub(crate) fn all_deprecated(decls: &[&Declaration]) -> bool {
  decls.iter().all(|decl| {
    decl
      .js_doc
      .tags
      .iter()
      .any(|tag| matches!(tag, JsDocTag::Deprecated { .. }))
  })
}

pub fn qualify_drilldown_name(
  parent_name: &str,
  drilldown_name: &str,
  is_static: bool,
) -> String {
  format!(
    "{parent_name}{}.{drilldown_name}",
    if is_static { "" } else { ".prototype" },
  )
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TopSymbolCtx {
  pub kind: IndexSet<DocNodeKindCtx>,
  pub name: String,
  pub href: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TopSymbolsCtx {
  pub symbols: Vec<TopSymbolCtx>,
  pub total_symbols: usize,
  pub all_symbols_href: String,
}

impl TopSymbolsCtx {
  pub fn new(ctx: &RenderContext) -> Option<Self> {
    let symbols = ctx
      .ctx
      .doc_nodes
      .values()
      .flat_map(|nodes| {
        flatten_namespace(ctx.ctx, nodes.iter().map(Cow::Borrowed))
      })
      .filter(|node| !node.is_internal(ctx.ctx))
      .collect::<Vec<_>>();

    if symbols.is_empty() {
      return None;
    }

    let total_symbols = symbols.len();

    let symbols = symbols
      .into_iter()
      .take(5)
      .map(|node| TopSymbolCtx {
        kind: node.get_kind_ctxs(),
        href: ctx.ctx.resolve_path(
          ctx.get_current_resolve(),
          UrlResolveKind::Symbol {
            file: &node.origin,
            symbol: &node.name,
          },
        ),
        name: node.name.to_string(),
      })
      .collect();

    Some(Self {
      symbols,
      total_symbols,
      all_symbols_href: ctx
        .ctx
        .resolve_path(ctx.get_current_resolve(), UrlResolveKind::AllSymbols),
    })
  }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ToCCtx {
  pub usages: Option<UsagesCtx>,
  pub top_symbols: Option<TopSymbolsCtx>,
  pub document_navigation_str: Option<String>,
  pub document_navigation: Vec<ToCEntry>,
}

impl ToCCtx {
  pub const TEMPLATE: &'static str = "toc";

  pub fn new(
    ctx: RenderContext,
    include_top_symbols: bool,
    usage_symbol: Option<Option<&DocNodeWithContext>>,
  ) -> Self {
    if ctx.get_current_resolve() == UrlResolveKind::Root
      && matches!(ctx.ctx.file_mode, FileMode::SingleDts | FileMode::Dts)
    {
      return Self {
        usages: None,
        top_symbols: None,
        document_navigation_str: None,
        document_navigation: vec![],
      };
    }

    Self {
      usages: if ctx.get_current_resolve() == UrlResolveKind::Root
        && ctx.ctx.main_entrypoint.is_none()
      {
        None
      } else {
        usage_symbol.and_then(|usage_symbol| UsagesCtx::new(&ctx, usage_symbol))
      },
      top_symbols: if include_top_symbols {
        TopSymbolsCtx::new(&ctx)
      } else {
        None
      },
      document_navigation_str: ctx.toc.render(),
      document_navigation: std::sync::Arc::into_inner(ctx.toc.toc)
        .unwrap()
        .into_inner()
        .unwrap(),
    }
  }
}

pub fn slugify(name: &str) -> String {
  static REJECTED_CHARS: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"[^\p{L}\p{M}\p{N}\p{Pc} -]").unwrap());

  REJECTED_CHARS
    .replace_all(&name.to_lowercase(), "")
    .replace(' ', "-")
}

/// Whether a character is unsafe inside the file-name component of a symbol
/// page, either because it is reserved on common filesystems (Windows forbids
/// `<>:"/\|?*`) or because it has special meaning in a URL path segment (`#`,
/// `?`, `%`, whitespace, …).
fn is_symbol_path_unsafe(c: char) -> bool {
  c.is_control()
    || c.is_whitespace()
    || matches!(
      c,
      '"'
        | '#'
        | '%'
        | '<'
        | '>'
        | '?'
        | '`'
        | '{'
        | '}'
        | '|'
        | '\\'
        | '^'
        | '*'
        | ':'
        | '/'
    )
}

/// Percent-encode any character of a symbol name that is unsafe to use as the
/// file-name component of its generated page. Symbol names are normally plain
/// identifiers, for which this is a borrow-only no-op; but a symbol whose name
/// comes from a string literal (e.g. `obj["a/b"]` or `'"><img …>'`) can contain
/// characters that would otherwise produce an invalid file path or a broken —
/// and potentially HTML-injecting — link. The same encoding is applied wherever
/// such a path is built (the written file name and every link to it) so they
/// stay consistent. See issue #724.
pub(crate) fn sanitize_symbol_path_part(name: &str) -> Cow<'_, str> {
  if !name.chars().any(is_symbol_path_unsafe) {
    return Cow::Borrowed(name);
  }

  let mut out = String::with_capacity(name.len());
  let mut buf = [0u8; 4];
  for c in name.chars() {
    if is_symbol_path_unsafe(c) {
      for b in c.encode_utf8(&mut buf).as_bytes() {
        out.push_str(&format!("%{b:02X}"));
      }
    } else {
      out.push(c);
    }
  }
  Cow::Owned(out)
}
