//! Handle file attributes

use crate::consts::*;
use crate::file_name::FileName;
use fuser::FileType;
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};

/// File attributes
#[derive(Clone)]
pub struct Attr {
    name: FileName,
    version: u64,
    inner: fuser::FileAttr,
}

impl Attr {
    pub fn new(
        key: u64,
        r#type: u64,
        version: u64,
        name: FileName,
        size: u64,
        mtime: Option<SystemTime>,
    ) -> Attr {
        let ino = Ino::new(key, r#type);
        debug_assert!(!ino.is_reserved());
        Self::new_internal(ino, ino.fuse_type(), version, name, size, mtime)
    }

    pub fn new_reserved(
        ino: u64,
        r#type: FileType,
        name: FileName,
        size: u64,
        mtime: Option<SystemTime>,
    ) -> Attr {
        let ino = Ino::from_raw(ino);
        debug_assert!(ino.is_reserved());
        Self::new_internal(ino, r#type, 0, name, size, mtime)
    }

    fn new_internal(
        ino: Ino,
        r#type: FileType,
        version: u64,
        name: FileName,
        size: u64,
        mtime: Option<SystemTime>,
    ) -> Attr {
        let mtime = mtime.unwrap_or(UNIX_EPOCH);
        let perm = match r#type {
            FileType::RegularFile => 0o644,
            FileType::Directory => 0o755,
            _ => unreachable!(),
        };

        Attr {
            name,
            version,
            inner: fuser::FileAttr {
                ino: ino.raw(),
                size,
                blocks: size.div_ceil(512),
                atime: UNIX_EPOCH, // 1970-01-01 00:00:00
                mtime,
                ctime: mtime,
                crtime: UNIX_EPOCH,
                kind: r#type,
                perm,
                nlink: 1,
                uid: 0,
                gid: 0,
                rdev: 0,
                flags: 0,
                blksize: 512,
            },
        }
    }

    pub fn file_attr(&self) -> &fuser::FileAttr {
        &self.inner
    }

    pub fn ino(&self) -> Ino {
        Ino::from_raw(self.inner.ino)
    }

    pub fn name(&self) -> &FileName {
        &self.name
    }

    pub fn version(&self) -> u64 {
        self.version
    }
}

/// An inode
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Ino(u64);

impl Ino {
    pub fn new(key: u64, r#type: u64) -> Self {
        debug_assert!(key & !TYPE_MASK == key, "key {key:x} has type bits set");
        debug_assert!(
            r#type & TYPE_MASK == r#type,
            "type {type:x} has key bits set"
        );

        Ino::from_raw(key | r#type)
    }

    pub fn from_raw(ino: u64) -> Self {
        let r#type = ino & TYPE_MASK;
        // Most significant bit is unused to ease casting between u64 and i64.
        debug_assert!(
            ino & (!0 >> 1) == ino,
            "most significant bit set in inode: {ino:x}"
        );
        debug_assert!(
            [FILE_TYPE, FOLDER_TYPE, DOMAIN_TYPE, RESERVED_TYPE].contains(&r#type),
            "invalid type {type}"
        );
        Ino(ino)
    }

    pub fn raw(self) -> u64 {
        self.0
    }

    pub fn key(self) -> Option<u64> {
        match self.r#type() {
            FILE_TYPE | FOLDER_TYPE | DOMAIN_TYPE => Some(self.0 & !TYPE_MASK),
            RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn file_key(self) -> Option<u64> {
        match self.r#type() {
            FILE_TYPE => Some(self.0 & !TYPE_MASK),
            FOLDER_TYPE | DOMAIN_TYPE | RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn dir_key(self) -> Option<u64> {
        match self.r#type() {
            FOLDER_TYPE => Some(self.0 & !TYPE_MASK),
            FILE_TYPE | DOMAIN_TYPE | RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn domain_key(self) -> Option<u64> {
        match self.r#type() {
            DOMAIN_TYPE => Some(self.0 & !TYPE_MASK),
            FILE_TYPE | FOLDER_TYPE | RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn r#type(self) -> u64 {
        self.0 & TYPE_MASK
    }

    pub fn is_domain(self) -> bool {
        self.0 & TYPE_MASK == DOMAIN_TYPE
    }

    pub fn is_dir(self) -> bool {
        self.0 & TYPE_MASK == FOLDER_TYPE
    }

    pub fn is_file(self) -> bool {
        self.0 & TYPE_MASK == FILE_TYPE
    }

    pub fn is_lost_and_found(self) -> bool {
        self.0 == LOST_AND_FOUND_ID
    }

    pub fn is_reserved(self) -> bool {
        self.0 & TYPE_MASK == RESERVED_TYPE
    }

    pub fn is_root(self) -> bool {
        self.0 == ROOT_ID
    }

    pub fn fuse_type(self) -> FileType {
        match self.r#type() {
            FILE_TYPE => FileType::RegularFile,
            FOLDER_TYPE | DOMAIN_TYPE => FileType::Directory,
            _ => unreachable!("cannot determine type for {self:?}"),
        }
    }

    pub fn api_name(self) -> Option<&'static str> {
        match self.r#type() {
            FILE_TYPE => Some("Resource"),
            FOLDER_TYPE => Some("Folder"),
            DOMAIN_TYPE => Some("Domain"),
            RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn admin_url(self, base_url: &str) -> Option<String> {
        let key = self.key()?;
        match self.r#type() {
            FILE_TYPE => Some(format!("{base_url}/tocco/docs/doc/{key}/detail")),
            FOLDER_TYPE => Some(format!("{base_url}/tocco/docs/folder/{key}/list")),
            DOMAIN_TYPE => Some(format!("{base_url}/tocco/docs/domain/{key}/list")),
            RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }

    pub fn entity_api_url(self, base_url: &str) -> Option<String> {
        let key = self.key()?;
        match self.r#type() {
            FILE_TYPE => Some(format!("{base_url}/nice2/rest/entities/2.0/Resource/{key}")),
            FOLDER_TYPE => Some(format!("{base_url}/nice2/rest/entities/2.0/Folder/{key}")),
            DOMAIN_TYPE => Some(format!("{base_url}/nice2/rest/entities/2.0/Domain/{key}")),
            RESERVED_TYPE => None,
            _ => unreachable!(),
        }
    }
}

impl fmt::Display for Ino {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl fmt::Debug for Ino {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let r#type = self.r#type();
        let type_str = match self.0 {
            DOMAIN_TYPE | DOMAIN_TYPE_END => "domain offset",
            FOLDER_TYPE | FOLDER_TYPE_END => "dir offset",
            FILE_TYPE | FILE_TYPE_END => "file offset",
            ROOT_ID => "root",
            DOT_ID => r#""." offset"#,
            DOT_DOT_ID => r#"".." offset"#,
            LOST_AND_FOUND_ID => "lost+found",
            0 => "zero offset",
            _ => match r#type {
                DOMAIN_TYPE => "domain",
                FOLDER_TYPE => "dir",
                FILE_TYPE => "file",
                _ => "<invalid>",
            },
        };
        write!(f, "inode 0x{:x} (type: {type_str}", self.0)?;
        if let Some(key) = self.key() {
            if key == 0 {
                write!(f, ", key: <start marker>")?;
            } else if key == !TYPE_MASK {
                write!(f, ", key: <end marker>")?;
            } else {
                write!(f, ", key: {key}")?;
            }
        }
        write!(f, ")")
    }
}
