use std::{
    borrow::Cow,
    collections::HashMap,
    fs,
    hash::Hash,
    path::{Path, PathBuf},
};

/// Object responsible for providing Mustache source code to a [`RenderManager`].
/// You *should not* try to cache the calls to [`get_src`]; the render manager does this
/// for you.
pub trait SourceProvider {
    /// Key type that sources will be indexed by.
    type Key: Clone + Hash + PartialEq + Eq;
    /// Get the source code at a given `key`, or return an error message if it
    /// fails. *Do not* cache this method, as the [`RenderManager`] will already
    /// cache it.
    fn get_src(&mut self, key: &Self::Key) -> Result<String, Cow<'static, str>>;
    /// Append a `name` to a key. This is used to resolve a partial from a specific file.
    fn resolve_partial(&self, key: &Self::Key, name: &str) -> Self::Key;
    /// This method is called by the error handling code to display the source's name.
    fn display_key(key: &'_ Self::Key) -> Cow<'_, str>;
}

/// This is the [`SourceProvider`] you will want to use most often. It reads files
/// from the original file's parent directory.
pub struct FsProvider;
impl SourceProvider for FsProvider {
    type Key = PathBuf;

    fn get_src(&mut self, key: &Self::Key) -> Result<String, Cow<'static, str>> {
        fs::read_to_string(key)
            .map_err(|_| format!("cannot read file {}", key.to_string_lossy()).into())
    }
    fn resolve_partial(&self, key: &Self::Key, name: &str) -> Self::Key {
        let mut key = key.parent().map_or_else(PathBuf::default, Path::to_owned);
        key.push(name);
        key.set_extension("mustache");
        key
    }

    fn display_key(key: &'_ Self::Key) -> Cow<'_, str> {
        key.to_string_lossy()
    }
}

/// This [`SourceProvider`] supplies sources from a hash map.
pub struct StaticProvider(pub HashMap<&'static str, &'static str>);

impl SourceProvider for StaticProvider {
    type Key = &'static str;

    fn get_src(&mut self, key: &Self::Key) -> Result<String, std::borrow::Cow<'static, str>> {
        self.0
            .get(key)
            .cloned()
            .map(String::from)
            .ok_or("unknown partial".into())
    }
    fn resolve_partial(&self, _: &Self::Key, name: &str) -> Self::Key {
        if let Some((k, _)) = self.0.get_key_value(name) {
            *k
        } else {
            name.to_string().leak()
        }
    }

    fn display_key(key: &'_ Self::Key) -> Cow<'_, str> {
        Cow::Borrowed(*key)
    }
}

#[cfg(feature = "ext-lambdas")]
pub(super) struct LambdaSourceProvider(pub Option<String>);

#[cfg(feature = "ext-lambdas")]
impl SourceProvider for LambdaSourceProvider {
    type Key = ();

    fn get_src(&mut self, (): &Self::Key) -> Result<String, std::borrow::Cow<'static, str>> {
        self.0
            .take()
            .ok_or("partials are not allowed inside lambdas".into())
    }
    fn resolve_partial(&self, (): &Self::Key, _: &str) -> Self::Key {
        ()
    }

    fn display_key((): &'_ Self::Key) -> Cow<'_, str> {
        Cow::Borrowed("<λ source>")
    }
}
