use std::num::ParseIntError;

use clap::Parser;
use libc::{
    S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH,
    S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR,
    mode_t,
};
use serde::{Serialize, Serializer};

#[cfg_attr(test, mutants::skip)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    let mode = parse_mode(&args.mode)?;
    let explained = Explained::new(mode);
    println!("{}", serde_json::to_string_pretty(&explained)?);
    Ok(())
}

/// Explain a Unix file mode.
#[derive(Debug, Parser)]
struct Args {
    /// The mode.
    mode: String,
}

#[derive(Debug, Serialize)]
struct Explained {
    mode_decimal: mode_t,
    #[serde(serialize_with = "octal")]
    mode_octal: mode_t,
    #[serde(serialize_with = "hex")]
    mode_hex: mode_t,

    file_kind_decimal: mode_t,
    #[serde(serialize_with = "octal")]
    file_kind_octal: mode_t,
    #[serde(serialize_with = "hex")]
    file_kind_hex: mode_t,

    is_regular: bool,
    is_dir: bool,
    is_socket: bool,
    is_symlink: bool,
    is_blk: bool,
    is_chr: bool,
    is_fifo: bool,

    is_setuid: bool,
    is_setgid: bool,
    is_sticky: bool,

    read_owner: bool,
    write_owner: bool,
    exec_owner: bool,

    read_group: bool,
    write_group: bool,
    exec_group: bool,

    read_other: bool,
    write_other: bool,
    exec_other: bool,
}

fn octal<S: Serializer>(v: &mode_t, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.serialize_str(&format!("0o{v:o}"))
}

fn hex<S: Serializer>(v: &mode_t, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.serialize_str(&format!("0x{v:x}"))
}

impl Explained {
    fn new(mode: mode_t) -> Self {
        fn is_set(mode: mode_t, mask: mode_t) -> bool {
            (mode & mask) == mask
        }

        let file_kind = mode & S_IFMT;

        Self {
            mode_decimal: mode,
            mode_octal: mode,
            mode_hex: mode,

            file_kind_decimal: file_kind,
            file_kind_octal: file_kind,
            file_kind_hex: file_kind,

            is_regular: is_set(mode, S_IFREG),
            is_dir: is_set(mode, S_IFDIR),
            is_socket: is_set(mode, S_IFSOCK),
            is_symlink: is_set(mode, S_IFLNK),
            is_blk: is_set(mode, S_IFBLK),
            is_chr: is_set(mode, S_IFCHR),
            is_fifo: is_set(mode, S_IFIFO),

            is_setuid: is_set(mode, S_ISUID),
            is_setgid: is_set(mode, S_ISGID),
            is_sticky: is_set(mode, S_ISVTX),

            read_owner: is_set(mode, S_IRUSR),
            write_owner: is_set(mode, S_IWUSR),
            exec_owner: is_set(mode, S_IXUSR),

            read_group: is_set(mode, S_IRGRP),
            write_group: is_set(mode, S_IWGRP),
            exec_group: is_set(mode, S_IXGRP),

            read_other: is_set(mode, S_IROTH),
            write_other: is_set(mode, S_IWOTH),
            exec_other: is_set(mode, S_IXOTH),
        }
    }
}

fn parse_mode(mode: &str) -> Result<mode_t, ParseIntError> {
    if let Some(s) = mode.strip_prefix("0x") {
        mode_t::from_str_radix(s, 16)
    } else if let Some(s) = mode.strip_prefix("0o") {
        mode_t::from_str_radix(s, 8)
    } else if let Some(s) = mode.strip_prefix("0") {
        mode_t::from_str_radix(s, 8)
    } else {
        mode.parse()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    type AnyResult<T> = Result<T, Box<dyn std::error::Error>>;

    #[test]
    fn hex() -> AnyResult<()> {
        assert_eq!(parse_mode("0xff")?, 255);
        Ok(())
    }

    #[test]
    fn octal_0() -> AnyResult<()> {
        assert_eq!(parse_mode("0644")?, 420);
        Ok(())
    }

    #[test]
    fn octal_0o() -> AnyResult<()> {
        assert_eq!(parse_mode("0o644")?, 420);
        Ok(())
    }

    #[test]
    fn explain_regular() {
        let ex = Explained::new(0o100664);
        assert_eq!(ex.mode_octal, 0o100664);
        assert!(ex.is_regular);
        assert!(ex.read_owner);
        assert!(ex.write_owner);
        assert!(!ex.exec_owner);

        assert!(ex.read_group);
        assert!(ex.write_group);
        assert!(!ex.exec_group);

        assert!(ex.read_other);
        assert!(!ex.write_other);
        assert!(!ex.exec_other);

        assert!(!ex.is_dir);
        assert!(!ex.is_socket);
        assert!(!ex.is_symlink);
        assert!(!ex.is_blk);
        assert!(!ex.is_chr);
        assert!(!ex.is_fifo);
    }
}
