//! Core functions and structs to define the content sources of your website.
//!
//! Content sources represent the content of your website, such as articles, blog posts, etc. Then, content sources can be passed to [`coronate()`](crate::coronate), through the [`content_sources!`](crate::content_sources) macro, to be loaded.
use std::{any::Any, path::PathBuf, sync::Arc};

use rustc_hash::FxHashMap;

mod highlight;
pub mod markdown;
mod slugger;

use crate::{
    assets::RouteAssets,
    route::{DynamicRouteContext, PageContext, PageParams},
};
pub use markdown::{
    components::{
        BlockQuoteKind, BlockquoteComponent, CodeComponent, EmphasisComponent, HardBreakComponent,
        HeadingComponent, HorizontalRuleComponent, ImageComponent, LinkComponent, LinkType,
        ListComponent, ListItemComponent, ListType, MarkdownComponents, ParagraphComponent,
        StrikethroughComponent, StrongComponent, TableAlignment, TableCellComponent,
        TableComponent, TableHeadComponent, TableRowComponent, TaskListMarkerComponent,
    },
    *,
};

pub use highlight::{HighlightOptions, highlight_code};

/// Helps implement a struct as a Markdown content entry.
///
/// ## Example
/// ```rust
/// use maudit::{coronate, content_sources, routes, BuildOptions, BuildOutput};
/// use maudit::content::{markdown_entry, glob_markdown};
///
/// #[markdown_entry]
/// pub struct ArticleContent {
///   pub title: String,
///   pub description: String,
/// }
///
/// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
///   coronate(
///     routes![],
///     content_sources![
///       "articles" => glob_markdown::<ArticleContent>("content/articles/*.md")
///     ],
///     BuildOptions::default(),
///   )
/// }
/// ```
///
/// ## Expand
/// ```rust
/// use maudit::content::{markdown_entry};
///
/// #[markdown_entry]
/// pub struct Article {
///   pub title: String,
///   pub content: String,
/// }
/// ```
/// expands to
/// ```rust
/// #[derive(serde::Deserialize)]
/// pub struct Article {
///   pub title: String,
///   pub content: String,
///   #[serde(skip)]
///   __internal_headings: Vec<maudit::content::MarkdownHeading>
/// }
///
/// impl maudit::content::MarkdownContent for Article {
///   fn get_headings(&self) -> &Vec<maudit::content::MarkdownHeading> {
///     &self.__internal_headings
///   }
/// }
///
/// impl maudit::content::InternalMarkdownContent for Article {
///   fn set_headings(&mut self, headings: Vec<maudit::content::MarkdownHeading>) {
///     self.__internal_headings = headings;
///   }
/// }
/// ```
pub use maudit_macros::markdown_entry;

/// A single entry of a [`ContentSource`].
///
/// ## Example
/// ```rust
/// use maudit::route::prelude::*;
/// # use maudit::content::markdown_entry;
/// #
/// # #[markdown_entry]
/// # pub struct ArticleContent {
/// #    pub title: String,
/// #    pub description: String,
/// # }
///
/// #[route("/articles/my-article")]
/// pub struct Article;
///
/// #[derive(Params)]
/// pub struct ArticleParams {
///     pub article: String,
/// }
///
/// impl Route for Article {
///    fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
///      let articles = ctx.content.get_source::<ArticleContent>("articles");
///      let article = articles.get_entry("my-article"); // returns a Entry<ArticleContent>
///
///      article.render(ctx)
///   }
/// }
/// ```
pub struct EntryInner<T> {
    pub id: String,
    render: OptionalContentRenderFn,
    pub raw_content: Option<String>,
    data_loader: Option<DataLoadingFn<T>>,
    cached_data: std::sync::OnceLock<T>,
    pub file_path: Option<PathBuf>,
}

/// Helper type for easier usage of `EntryInner`. Content sources always return Arc-wrapped entries, but the user ergonomics of writing `Arc<EntryInner<T>>` is not great.
pub type Entry<T> = Arc<EntryInner<T>>;

pub trait ContentEntry<T> {
    fn create(
        id: String,
        render: OptionalContentRenderFn,
        raw_content: Option<String>,
        data: T,
        file_path: Option<PathBuf>,
    ) -> Entry<T> {
        Arc::new(EntryInner {
            id,
            render,
            raw_content,
            data_loader: None,
            cached_data: std::sync::OnceLock::from(data),
            file_path,
        })
    }

    fn create_lazy(
        id: String,
        render: OptionalContentRenderFn,
        raw_content: Option<String>,
        data_loader: DataLoadingFn<T>,
        file_path: Option<PathBuf>,
    ) -> Entry<T> {
        Arc::new(EntryInner {
            id,
            render,
            raw_content,
            data_loader: Some(data_loader),
            cached_data: std::sync::OnceLock::new(),
            file_path,
        })
    }
}

impl<T> ContentEntry<T> for Entry<T> {}

/// Trait for contexts that can provide access to content
pub trait ContentContext {
    fn content(&self) -> &ContentSources;
    fn assets(&mut self) -> &mut RouteAssets;
}

impl ContentContext for PageContext<'_> {
    fn content(&self) -> &ContentSources {
        self.content
    }

    fn assets(&mut self) -> &mut RouteAssets {
        self.assets
    }
}

impl ContentContext for DynamicRouteContext<'_> {
    fn content(&self) -> &ContentSources {
        self.content
    }

    fn assets(&mut self) -> &mut RouteAssets {
        self.assets
    }
}

type DataLoadingFn<T> = Box<dyn Fn(&mut dyn ContentContext) -> T + Send + Sync>;

type OptionalContentRenderFn =
    Option<Box<dyn Fn(&str, &mut crate::route::PageContext) -> String + Send + Sync>>;

impl<T> EntryInner<T> {
    pub fn data<C: ContentContext>(&self, ctx: &mut C) -> &T {
        self.cached_data.get_or_init(|| {
            if let Some(ref loader) = self.data_loader {
                loader(ctx)
            } else {
                panic!("No data loader available and no cached data")
            }
        })
    }

    pub fn render(&self, ctx: &mut PageContext) -> String {
        (self.render.as_ref().unwrap())(self.raw_content.as_ref().unwrap(), ctx)
    }
}

/// Represents an untyped content source.
pub type Untyped = FxHashMap<String, String>;

/// Main struct to access all content sources.
///
/// # Example
/// In `main.rs`:
/// ```rust
/// use maudit::{coronate, content_sources, routes, BuildOptions, BuildOutput};
/// use maudit::content::{markdown_entry, glob_markdown};
///
/// #[markdown_entry]
/// pub struct ArticleContent {
///   pub title: String,
///   pub description: String,
/// }
///
/// fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
///   coronate(
///     routes![],
///     content_sources![
///       "articles" => glob_markdown::<ArticleContent>("content/articles/*.md")
///     ],
///     BuildOptions::default(),
///   )
/// }
/// ```
///
/// In a page:
/// ```rust
/// use maudit::route::prelude::*;
/// # use maudit::content::markdown_entry;
/// #
/// # #[markdown_entry]
/// # pub struct ArticleContent {
/// #    pub title: String,
/// #    pub description: String,
/// # }
///
/// #[route("/articles/[article]")]
/// pub struct Article;
///
/// #[derive(Params, Clone)]
/// pub struct ArticleParams {
///     pub article: String,
/// }
///
/// impl Route<ArticleParams> for Article {
///    fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
///      let params = ctx.params::<ArticleParams>();
///      let articles = ctx.content.get_source::<ArticleContent>("articles");
///      let article = articles.get_entry(&params.article);
///      article.render(ctx)
///   }
///
///   fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<ArticleParams> {
///     let articles = ctx.content.get_source::<ArticleContent>("articles");
///
///     articles.into_pages(|entry| Page::from_params(ArticleParams {
///        article: entry.id.clone(),
///     }))
///   }
/// }
/// ```
pub struct ContentSources(pub Vec<Box<dyn ContentSourceInternal>>);

impl From<Vec<Box<dyn ContentSourceInternal>>> for ContentSources {
    fn from(content_sources: Vec<Box<dyn ContentSourceInternal>>) -> Self {
        Self(content_sources)
    }
}

impl ContentSources {
    pub fn new(content_sources: Vec<Box<dyn ContentSourceInternal>>) -> Self {
        Self(content_sources)
    }

    pub fn sources(&self) -> &Vec<Box<dyn ContentSourceInternal>> {
        &self.0
    }

    pub fn sources_mut(&mut self) -> &mut Vec<Box<dyn ContentSourceInternal>> {
        &mut self.0
    }

    pub fn init_all(&mut self) {
        for source in &mut self.0 {
            source.init();
        }
    }

    pub fn get_untyped_source(&self, name: &str) -> &ContentSource<Untyped> {
        self.get_source::<Untyped>(name)
    }

    pub fn get_untyped_source_safe(&self, name: &str) -> Option<&ContentSource<Untyped>> {
        self.get_source_safe::<Untyped>(name)
    }

    pub fn get_source<T: 'static>(&self, name: &str) -> &ContentSource<T> {
        self.0
            .iter()
            .find_map(
                |source| match source.as_any().downcast_ref::<ContentSource<T>>() {
                    Some(source) if source.name == name => Some(source),
                    _ => None,
                },
            )
            .unwrap_or_else(|| panic!("Content source with name '{}' not found", name))
    }

    pub fn get_source_safe<T: 'static>(&self, name: &str) -> Option<&ContentSource<T>> {
        self.0.iter().find_map(
            |source| match source.as_any().downcast_ref::<ContentSource<T>>() {
                Some(source) if source.name == name => Some(source),
                _ => None,
            },
        )
    }
}

type ContentSourceInitMethod<T> = Box<dyn Fn() -> Vec<Arc<EntryInner<T>>> + Send + Sync>;

/// A source of content such as articles, blog posts, etc.
pub struct ContentSource<T = Untyped> {
    pub name: String,
    pub entries: Vec<Arc<EntryInner<T>>>,
    pub(crate) init_method: ContentSourceInitMethod<T>,
}

impl<T> ContentSource<T> {
    pub fn new<P>(name: P, entries: ContentSourceInitMethod<T>) -> Self
    where
        P: Into<String>,
    {
        Self {
            name: name.into(),
            entries: vec![],
            init_method: entries,
        }
    }

    pub fn get_entry(&self, id: &str) -> &Entry<T> {
        self.entries
            .iter()
            .find(|entry| entry.id == id)
            .unwrap_or_else(|| panic!("Entry with id '{}' not found", id))
    }

    pub fn get_entry_safe(&self, id: &str) -> Option<&Entry<T>> {
        self.entries.iter().find(|entry| entry.id == id)
    }

    pub fn into_params<P>(&self, cb: impl FnMut(&Entry<T>) -> P) -> Vec<P>
    where
        P: Into<PageParams>,
    {
        self.entries.iter().map(cb).collect()
    }

    pub fn into_pages<Params, Props>(
        &self,
        cb: impl FnMut(&Entry<T>) -> crate::route::Page<Params, Props>,
    ) -> crate::route::Pages<Params, Props>
    where
        Params: Into<PageParams>,
    {
        self.entries.iter().map(cb).collect()
    }
}

#[doc(hidden)]
/// Used internally by Maudit and should not be implemented by the user.
/// We expose it because it's implemented for [`ContentSource`], which is public.
pub trait ContentSourceInternal: Send + Sync {
    fn init(&mut self);
    fn get_name(&self) -> &str;
    fn as_any(&self) -> &dyn Any; // Used for type checking at runtime
}

impl<T: 'static + Sync + Send> ContentSourceInternal for ContentSource<T> {
    fn init(&mut self) {
        self.entries = (self.init_method)();
    }
    fn get_name(&self) -> &str {
        &self.name
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}
