use petgraph::graph::NodeIndex;

use crate::{
    ItemPathBuf,
    note::{Note, NoteError},
};

static AUDIO_EXTS: &[&str] = &["3gp", "flac", "m4a", "mp3", "ogg", "opus", "wav"];
static DATA_EXTS: &[&str] = &["json", "toml", "yaml"];
static IMAGE_EXTS: &[&str] = &["bmp", "gif", "png", "jpeg", "jpg", "svg", "webp"];
static VIDEO_EXTS: &[&str] = &["avi", "mkv", "mov", "mp4", "ogv", "webm"];

static BASE_EXTS: &[&str] = &["base"];
static CANVAS_EXTS: &[&str] = &["canvas"];
static NOTE_EXTS: &[&str] = &["adoc", "gem", "md", "org"];

static HTML_EXTS: &[&str] = &["html", "htm"];

/// All the supported types of files that can exist in a Vault.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum FileKind {
    /// An audio file.
    ///
    /// Supported file extensions: `3gp`, `flac`, `m4a`, `mp3`, `ogg`, `opus`, `wav`.
    Audio,
    /// A generic data file.
    ///
    /// Supported file extensions: `json`, `toml`, `yaml`.
    Data,
    /// An image file.
    ///
    /// Supported file extensions: `bmp`, `gif`, `png`, `jpeg`, `jpg`, `svg`, `webp`.
    Image,
    /// A video file.
    ///
    /// Supported file extensions: `avi`, `mkv`, `mov`, `mp4`, `ogv`, `webm`.
    Video,

    /// A Obsidian Base.
    Base,
    /// A Obsidian Canvas.
    Canvas,
    /// A Obsidian Note.
    Note,

    /// An HTML file.
    ///
    /// Supported file extensions: `html`, `htm`.
    Html,

    /// An unknown (unsupport) file type.
    Unknown(Option<String>),
}

impl FileKind {
    /// Returns `true` if the file kind is [`FileKind::Audio`], [`FileKind::Image`], or [`FileKind::Video`].
    #[must_use]
    pub const fn is_asset(&self) -> bool {
        self.is_audio() || self.is_image() || self.is_video()
    }

    /// Returns `true` if the file kind is [`FileKind::Audio`].
    #[must_use]
    pub const fn is_audio(&self) -> bool {
        matches!(self, Self::Audio)
    }

    /// Returns `true` if the file kind is [`FileKind::Data`].
    #[must_use]
    pub const fn is_data(&self) -> bool {
        matches!(self, Self::Data)
    }

    /// Returns `true` if the file kind is [`FileKind::Image`].
    #[must_use]
    pub const fn is_image(&self) -> bool {
        matches!(self, Self::Image)
    }

    /// Returns `true` if the file kind is [`FileKind::Video`].
    #[must_use]
    pub const fn is_video(&self) -> bool {
        matches!(self, Self::Video)
    }

    /// Returns `true` if the file kind is [`FileKind::Base`].
    #[must_use]
    pub const fn is_base(&self) -> bool {
        matches!(self, Self::Base)
    }

    /// Returns `true` if the file kind is [`FileKind::Canvas`].
    #[must_use]
    pub const fn is_canvas(&self) -> bool {
        matches!(self, Self::Canvas)
    }

    /// Returns `true` if the file kind is [`FileKind::Note`].
    #[must_use]
    pub const fn is_note(&self) -> bool {
        matches!(self, Self::Note)
    }

    /// Returns `true` if the file kind is [`FileKind::Html`].
    #[must_use]
    pub const fn is_html(&self) -> bool {
        matches!(self, Self::Html)
    }
}

/// A Vault file.
///
/// This is the base entry of all files in a Vault.
/// Bases, Canvases, and Note are all converted from this.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct File {
    /// The path of the file.
    pub path: ItemPathBuf,

    /// The kind of file this is.
    ///
    /// [`FileKind::Base`], [`FileKind::Canvas`], and [`FileKind::Note`] are all converted to their respected structs.
    /// As such any accessable file will never have those as a kind.
    pub kind: FileKind,
}

impl File {
    /// Create a new [`File`], getting the [`FileKind`] from the path extension.
    #[must_use]
    pub fn from_path(path: ItemPathBuf) -> Self {
        let kind = path
            .extension()
            .map_or(FileKind::Unknown(None), |ext| match ext {
                _ if AUDIO_EXTS.contains(&ext) => FileKind::Audio,
                _ if DATA_EXTS.contains(&ext) => FileKind::Data,
                _ if IMAGE_EXTS.contains(&ext) => FileKind::Image,
                _ if VIDEO_EXTS.contains(&ext) => FileKind::Video,

                _ if BASE_EXTS.contains(&ext) => FileKind::Base,
                _ if CANVAS_EXTS.contains(&ext) => FileKind::Canvas,
                _ if NOTE_EXTS.contains(&ext) => FileKind::Note,

                _ if HTML_EXTS.contains(&ext) => FileKind::Html,

                _ => FileKind::Unknown(Some(ext.to_string())),
            });

        if matches!(&kind, FileKind::Unknown(_)) {
            tracing::warn!(path=%path.relative, "unknown file type");
        }

        Self { path, kind }
    }

    /// Convert this file referencce into a [`Note`].
    ///
    /// # Errors
    ///
    /// This will error if it failed to read the note from the file system,
    ///  its unable to deserialize the frontmatter,
    /// or it was unable to get file system metadata of the note.
    pub fn into_note(self, index: NodeIndex) -> Result<Note, NoteError> {
        Note::from_path(self.path, index)
    }
}
