//! The treehouse virtual file system.
//!
//! Unlike traditional file systems, there is no separation between directories and files.
//! Instead, our file system is based on _entries_, which may have specific, optional, well-typed
//! metadata attached to them.
//! A directory is formed by returning a list of paths from [`dir`][Dir::dir], and a file is
//! formed by returning `Some` from [`content`][Dir::content].
//!
//! This makes using the file system simpler, as you do not have to differentiate between different
//! entry kinds. All paths act as if they _could_ return byte content, and all paths act as if they
//! _could_ have children.
//!
//! # Composability
//!
//! [`Dir`]s are composable. The [`Dir`] itself starts off with the root path ([`VPath::ROOT`]),
//! which may contain further [`dir`][Dir::dir] entries, or content by itself.
//! This makes it possible to nest a [`Dir`] under another [`Dir`].
//!
//! Additionally, there's also the inverse operation, [`Cd`] (named after the `cd`
//! _change directory_ shell command), which returns a [`Dir`] viewing a subpath within another
//! [`Dir`].
//!
//! # Building directories
//!
//! In-memory directories can be composed using the following primitives:
//!
//! - [`BufferedFile`] - root path content is the provided byte vector.
//! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside.
//!
//! Additionally, for interfacing with the OS file system, [`PhysicalDir`] is available,
//! representing a directory stored on the disk.
//!
//! # Virtual paths
//!
//! Entries within directories are referenced using [`VPath`]s (**v**irtual **path**s).
//! A virtual path is composed out of any amount of `/`-separated components.
//!
//! There are no special directories like `.` and `..` (those are just normal entries, though using
//! them is discouraged). [`VPath`]s are always relative to the root of the [`Dir`] you're querying.
//!
//! A leading or trailing slash is not allowed, because they would have no meaning.
//!
//! [`VPath`] also has an owned version, [`VPathBuf`].

use std::{
    any::TypeId,
    fmt::{self, Debug},
    ops::{ControlFlow, Deref},
    string::FromUtf8Error,
    sync::Arc,
};

mod anchored;
pub mod asynch;
mod cd;
mod content_cache;
mod content_version_cache;
mod edit;
mod file;
mod html_canonicalize;
mod image_size_cache;
mod mem_dir;
mod overlay;
mod path;
mod physical;

pub use anchored::*;
pub use cd::*;
pub use content_cache::*;
pub use content_version_cache::*;
pub use edit::*;
pub use file::*;
pub use html_canonicalize::*;
pub use image_size_cache::*;
pub use mem_dir::*;
pub use overlay::*;
pub use path::*;
pub use physical::*;

pub trait Dir: Debug {
    fn query(&self, path: &VPath, query: &mut Query);
}

pub trait Fork {}

pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option<T>
where
    T: 'static + Fork,
{
    let mut slot = TaggedOption::<'a, tags::Value<T>>(None);
    dir.query(path, Query::new(&mut slot));
    slot.0
}

#[repr(transparent)]
pub struct Query<'a> {
    erased: dyn Erased<'a> + 'a,
}

impl<'a> Query<'a> {
    fn new<'b>(erased: &'b mut (dyn Erased<'a> + 'a)) -> &'b mut Query<'a> {
        unsafe { &mut *(erased as *mut dyn Erased<'a> as *mut Query<'a>) }
    }

    pub fn provide<T>(&mut self, f: impl FnOnce() -> T)
    where
        T: 'static + Fork,
    {
        if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
            result.0 = Some(f());
        }
    }

    pub fn try_provide<T>(&mut self, f: impl FnOnce() -> Option<T>)
    where
        T: 'static + Fork,
    {
        if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
            result.0 = f();
        }
    }
}

mod tags {
    use std::marker::PhantomData;

    pub trait Type<'a>: Sized + 'static {
        type Reified: 'a;
    }

    pub struct Value<T>(PhantomData<T>)
    where
        T: 'static;

    impl<T> Type<'_> for Value<T>
    where
        T: 'static,
    {
        type Reified = T;
    }
}

#[repr(transparent)]
struct TaggedOption<'a, I: tags::Type<'a>>(Option<I::Reified>);

#[expect(clippy::missing_safety_doc)]
unsafe trait Erased<'a>: 'a {
    fn tag_id(&self) -> TypeId;
}

unsafe impl<'a, I: tags::Type<'a>> Erased<'a> for TaggedOption<'a, I> {
    fn tag_id(&self) -> TypeId {
        TypeId::of::<I>()
    }
}

impl<'a> dyn Erased<'a> + 'a {
    fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<'a, I>>
    where
        I: tags::Type<'a>,
    {
        if self.tag_id() == TypeId::of::<I>() {
            // SAFETY: Just checked whether we're pointing to an I.
            Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<'a, I>>() })
        } else {
            None
        }
    }
}

impl Dir for () {
    fn query(&self, _path: &VPath, _query: &mut Query) {
        // Noop implementation.
    }
}

impl<T> Dir for &T
where
    T: Dir,
{
    fn query(&self, path: &VPath, query: &mut Query) {
        (**self).query(path, query)
    }
}

#[derive(Clone)]
pub struct DynDir {
    arc: Arc<dyn Dir + Send + Sync>,
}

impl Dir for DynDir {
    fn query(&self, path: &VPath, query: &mut Query) {
        self.arc.query(path, query);
    }
}

impl fmt::Debug for DynDir {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&*self.arc, f)
    }
}

impl Deref for DynDir {
    type Target = dyn Dir + Send + Sync;

    fn deref(&self) -> &Self::Target {
        &*self.arc
    }
}

pub trait ToDynDir {
    fn to_dyn(self) -> DynDir;
}

impl<T> ToDynDir for T
where
    T: Dir + Send + Sync + 'static,
{
    fn to_dyn(self) -> DynDir {
        DynDir {
            arc: Arc::new(self),
        }
    }
}

pub trait AnchoredAtExt {
    fn anchored_at(self, at: VPathBuf) -> Anchored<Self>
    where
        Self: Sized;
}

impl<T> AnchoredAtExt for T
where
    T: Dir,
{
    fn anchored_at(self, at: VPathBuf) -> Anchored<Self> {
        Anchored::new(self, at)
    }
}

/// List of child entries under a directory.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Entries(pub Vec<VPathBuf>);

/// Byte content in an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Content {
    /// Media type string. <https://en.wikipedia.org/wiki/Media_type>
    kind: String,
    bytes: Vec<u8>,
}

/// Abstract version of an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ContentVersion {
    pub string: String,
}

/// Path relative to `config.site` indicating where the file will be available once served.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Anchor {
    pub path: VPathBuf,
}

/// Size of image entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageSize {
    pub width: u32,
    pub height: u32,
}

impl Content {
    pub fn new(kind: impl Into<String>, bytes: Vec<u8>) -> Self {
        Self {
            kind: kind.into(),
            bytes,
        }
    }

    pub fn kind(&self) -> &str {
        &self.kind
    }

    pub fn bytes(self) -> Vec<u8> {
        self.bytes
    }

    pub fn string(self) -> Result<String, FromUtf8Error> {
        String::from_utf8(self.bytes())
    }
}

impl Fork for Entries {}
impl Fork for Content {}
impl Fork for ContentVersion {}
impl Fork for Anchor {}
impl Fork for ImageSize {}
impl Fork for EditPath {}

pub fn entries(dir: &dyn Dir, path: &VPath) -> Vec<VPathBuf> {
    query::<Entries>(dir, path).map(|e| e.0).unwrap_or_default()
}

pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) {
    for entry in entries(dir, path) {
        match f(&entry) {
            ControlFlow::Continue(_) => (),
            ControlFlow::Break(_) => return,
        }
        walk_dir_rec(dir, &entry, f);
    }
}

pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> {
    let anchor = query::<Anchor>(dir, path)?;
    if let Some(version) = query::<ContentVersion>(dir, path) {
        Some(format!("{}/{}?v={}", site, anchor.path, version.string))
    } else {
        Some(format!("{}/{}", site, anchor.path))
    }
}
