#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
#![feature(let_chains, decl_macro)]

use std::{
    cmp::Ordering,
    ffi::OsString,
    fmt,
    fs::{read_dir, FileType, Metadata},
    io::{self, stdout, IsTerminal},
    path::{Path, PathBuf},
    process::ExitCode,
    time::SystemTime,
};

#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(target_os = "yggdrasil")]
use std::os::yggdrasil::fs::MetadataExt;

use chrono::{Datelike, Timelike};
use clap::Parser;
use libutil::fmt::FormatSize;

#[derive(Parser)]
#[clap(disable_help_flag = true)]
pub struct Args {
    #[arg(long, default_value_t = true)]
    color: bool,
    #[arg(short)]
    long: bool,
    #[arg(short)]
    inodes: bool,
    #[arg(short, long)]
    human_readable: bool,
    #[arg(short)]
    all: bool,

    paths: Vec<PathBuf>,
}

trait DisplayBit {
    fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;

    fn display_with<'a, 'b>(&'a self, opts: &'b Args) -> DisplayWith<'a, 'b, Self>
    where
        Self: Sized,
    {
        DisplayWith { item: self, opts }
    }
}

trait DisplaySizeBit: Sized {
    fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;

    fn display_size_with(self, opts: &Args) -> DisplaySizeWith<'_, Self> {
        DisplaySizeWith { size: self, opts }
    }
}

trait MetadataImpl {
    type Mode: DisplayBit;

    fn size(&self) -> u64;
    fn inode(&self) -> Option<u32>;
    fn mode(&self) -> Self::Mode;
    fn mtime(&self) -> SystemTime;
}

trait FileTypeImpl {
    fn is_directory(&self) -> bool;
    fn is_block_device(&self) -> bool;
    fn is_char_device(&self) -> bool;
}

impl DisplaySizeBit for u64 {
    fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if opts.human_readable {
            fmt::Display::fmt(&FormatSize::default(self), f)
        } else {
            fmt::Display::fmt(&self, f)
        }
    }
}

struct DisplaySizeWith<'a, T> {
    size: T,
    opts: &'a Args,
}

struct DisplayWith<'a, 'b, T: DisplayBit> {
    item: &'a T,
    opts: &'b Args,
}

impl<T: DisplayBit> fmt::Display for DisplayWith<'_, '_, T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.item.display_bit(self.opts, f)
    }
}

impl<T: DisplaySizeBit + Copy> fmt::Display for DisplaySizeWith<'_, T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.size.display_size_bit(self.opts, f)
    }
}

struct Entry {
    name: String,
    target: Option<String>,
    ty: Option<FileType>,
    attrs: Option<Metadata>,
}

impl DisplayBit for Option<FileType> {
    fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(ty) = self {
            if ty.is_char_device() {
                f.write_str("c")
            } else if ty.is_block_device() {
                f.write_str("b")
            } else if ty.is_directory() {
                f.write_str("d")
            } else if ty.is_symlink() {
                f.write_str("l")
            } else {
                f.write_str("-")
            }
        } else {
            f.write_str("?")
        }
    }
}

#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
impl FileTypeImpl for FileType {
    fn is_directory(&self) -> bool {
        FileType::is_dir(self)
    }

    fn is_char_device(&self) -> bool {
        use std::os::yggdrasil::fs::FileTypeExt;
        FileTypeExt::is_char_device(self)
    }

    fn is_block_device(&self) -> bool {
        use std::os::yggdrasil::fs::FileTypeExt;
        FileTypeExt::is_block_device(self)
    }
}

#[cfg(any(unix, rust_analyzer))]
impl FileTypeImpl for FileType {
    fn is_directory(&self) -> bool {
        FileType::is_dir(self)
    }

    fn is_char_device(&self) -> bool {
        use std::os::unix::fs::FileTypeExt;
        FileTypeExt::is_char_device(self)
    }

    fn is_block_device(&self) -> bool {
        use std::os::unix::fs::FileTypeExt;
        FileTypeExt::is_block_device(self)
    }
}

#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
impl DisplayBit for std::os::yggdrasil::io::RawFileMode {
    fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self}")
    }
}

#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
impl MetadataImpl for Metadata {
    type Mode = std::os::yggdrasil::io::RawFileMode;

    fn size(&self) -> u64 {
        self.len()
    }

    fn mode(&self) -> Self::Mode {
        self.mode_ext()
    }

    fn inode(&self) -> Option<u32> {
        MetadataExt::inode(self)
    }

    fn mtime(&self) -> SystemTime {
        self.modified().unwrap()
    }
}

#[cfg(any(unix, rust_analyzer))]
struct UnixFileMode(u32);

#[cfg(any(unix, rust_analyzer))]
impl DisplayBit for UnixFileMode {
    fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use std::fmt::Write;

        macro display_bit($self:expr, $letter:expr, $bit:expr) {
            if $self.0 & (1 << $bit) != 0 {
                $letter
            } else {
                '-'
            }
        }

        for i in (0..3).rev() {
            let r = i * 3 + 2;
            let w = i * 3 + 1;
            let x = i * 3;
            f.write_char(display_bit!(self, 'r', r))?;
            f.write_char(display_bit!(self, 'w', w))?;
            f.write_char(display_bit!(self, 'x', x))?;
        }

        Ok(())
    }
}

impl Entry {
    pub fn is_symlink(&self) -> bool {
        self.ty.map_or(false, |d| d.is_symlink())
    }

    pub fn is_directory(&self) -> bool {
        self.ty.map_or(false, |d| d.is_dir())
    }
}

#[cfg(any(unix, rust_analyzer))]
impl Entry {
    pub fn is_device(&self) -> bool {
        self.ty
            .map_or(false, |d| d.is_block_device() || d.is_char_device())
    }

    pub fn is_executable(&self) -> bool {
        self.attrs
            .as_ref()
            .map(MetadataImpl::mode)
            .map_or(false, |d| d.0 & 0o100 != 0)
    }
}

#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
impl Entry {
    pub fn is_device(&self) -> bool {
        self.ty
            .map_or(false, |d| d.is_block_device() || d.is_char_device())
    }

    pub fn is_executable(&self) -> bool {
        self.attrs
            .as_ref()
            .map(MetadataImpl::mode)
            .map_or(false, |d| {
                d.contains(std::os::yggdrasil::io::RawFileMode::USER_EXEC)
            })
    }
}

#[cfg(any(unix, rust_analyzer))]
impl MetadataImpl for Metadata {
    type Mode = UnixFileMode;

    fn size(&self) -> u64 {
        self.len()
    }

    fn inode(&self) -> Option<u32> {
        self.ino().try_into().ok()
    }

    fn mode(&self) -> Self::Mode {
        UnixFileMode(MetadataExt::mode(self))
    }

    fn mtime(&self) -> SystemTime {
        self.modified().unwrap()
    }
}

fn convert_file_time(time: SystemTime) -> chrono::DateTime<chrono::Utc> {
    let timestamp = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
    chrono::DateTime::from_timestamp(timestamp.as_secs() as _, 0).unwrap()
}

fn time_now() -> chrono::DateTime<chrono::Utc> {
    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap();
    chrono::DateTime::from_timestamp(now.as_secs() as _, 0).unwrap()
}

impl DisplayBit for Option<Metadata> {
    fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        const MONTHS: &[&str] = &[
            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
        ];

        let now = time_now();
        let this = self.as_ref();
        let ino = this.and_then(MetadataImpl::inode);
        let mode = this.map(MetadataImpl::mode);
        let size = this.map(MetadataImpl::size);
        let mtime = this
            .map(MetadataImpl::mtime)
            .map(convert_file_time)
            .map(|time| {
                if time.year() == now.year() {
                    format!(
                        "{} {:02} {:02}:{:02}",
                        MONTHS[time.month0() as usize],
                        time.day(),
                        time.hour(),
                        time.minute()
                    )
                } else {
                    format!(
                        "{} {:02}, {:04}",
                        MONTHS[time.month0() as usize],
                        time.day(),
                        time.year()
                    )
                }
            });

        if let Some(mode) = mode {
            mode.display_bit(opts, f)?;
        } else {
            write!(f, "---------")?;
        }

        if let Some(size) = size {
            write!(f, " {:>12}", size.display_size_with(opts))?;
        } else {
            write!(f, " {:>12}", "???")?;
        }

        if let Some(mtime) = mtime {
            write!(f, " {:>14}", mtime)?;
        } else {
            write!(f, " {:<14}", "???")?;
        }

        if opts.inodes {
            if let Some(ino) = ino {
                write!(f, " {ino:>8}")?;
            } else {
                write!(f, " {:<8}", "---")?;
            }
        }

        Ok(())
    }
}

fn display_filename(f: &mut fmt::Formatter<'_>, entry: &Entry, color: bool) -> fmt::Result {
    if !color {
        return f.write_str(&entry.name);
    }

    let color = if entry.is_device() {
        // Yellow
        Some(3)
    } else if entry.is_symlink() {
        // Purple
        Some(5)
    } else if entry.is_directory() {
        // Cyan
        Some(6)
    } else if entry.is_executable() {
        // Green
        Some(2)
    } else {
        None
    };

    if let Some(color) = color {
        write!(f, "\x1B[3{color}m{}\x1B[0m", &entry.name)
    } else {
        f.write_str(&entry.name)
    }
}

impl DisplayBit for Entry {
    fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // let ino = self.attrs.as_ref().and_then(<Metadata as MetadataExt>::inode);

        if opts.long {
            write!(
                f,
                "{}{} ",
                self.ty.display_with(opts),
                self.attrs.display_with(opts),
            )?;

            display_filename(f, self, opts.color)?;

            if let Some(target) = self.target.as_ref() {
                write!(f, " -> {}", target)?;
            }

            Ok(())
        } else {
            let ino = self
                .attrs
                .as_ref()
                .and_then(<Metadata as MetadataImpl>::inode);
            if opts.inodes {
                if let Some(ino) = ino {
                    write!(f, "{ino:<8} ")?;
                } else {
                    write!(f, "{:<8} ", "---")?;
                }
            }

            display_filename(f, self, opts.color)?;

            Ok(())
        }
    }
}

impl Entry {
    fn invalid() -> Self {
        Self {
            name: "???".to_owned(),
            target: None,
            ty: None,
            attrs: None,
        }
    }
}

fn sort_dirs_first(a: &Entry, b: &Entry) -> Ordering {
    let a_is_dir = a.ty.map(|t| t.is_dir()).unwrap_or(false);
    let b_is_dir = b.ty.map(|t| t.is_dir()).unwrap_or(false);

    let by_type = Ord::cmp(&(b_is_dir as usize), &(a_is_dir as usize));
    let by_name = Ord::cmp(&a.name, &b.name);

    by_type.then(by_name)
}

fn include(filename: &OsString, all: bool) -> bool {
    if all {
        return true;
    }
    let Some(filename) = filename.to_str() else {
        return true;
    };
    !filename.starts_with(".")
}

fn list_directory(path: &Path, all: bool) -> io::Result<Vec<Entry>> {
    let mut entries = vec![];
    for entry in read_dir(path)? {
        let Ok(entry) = entry else {
            entries.push(Entry::invalid());
            continue;
        };

        let os_filename = entry.file_name();

        if !include(&os_filename, all) {
            continue;
        }

        let ty = entry.file_type().ok();
        let attrs = entry.path().symlink_metadata().ok();

        let target = if let Some(attrs) = attrs.as_ref()
            && attrs.is_symlink()
        {
            Some(match entry.path().read_link() {
                Ok(res) => res.to_string_lossy().to_string(),
                Err(_) => "???".into(),
            })
        } else {
            None
        };

        entries.push(Entry {
            name: os_filename.to_string_lossy().to_string(),
            target,
            ty,
            attrs,
        });
    }

    entries.sort_by(sort_dirs_first);

    Ok(entries)
}

fn list(opts: &Args, path: &Path) -> io::Result<()> {
    if path.is_dir() {
        let entries = list_directory(path, opts.all)?;

        for entry in entries {
            println!("{}", entry.display_with(opts));
        }

        Ok(())
    } else {
        let attrs = path.symlink_metadata()?;

        let target = attrs.is_symlink().then(|| match path.read_link() {
            Ok(res) => res.display().to_string(),
            Err(_) => "???".into(),
        });

        let entry = Entry {
            name: path.display().to_string(),
            ty: Some(attrs.file_type()),
            attrs: Some(attrs),
            target,
        };

        println!("{}", entry.display_with(opts));

        Ok(())
    }
}

fn run_inner(opts: &Args, paths: &[PathBuf], print_names: bool) -> Vec<Result<(), io::Error>> {
    let mut results = vec![];
    for path in paths {
        if print_names {
            println!("{}: ", path.display());
        }
        results.push(list(opts, path).inspect_err(|error| {
            eprintln!("{}: {error}", path.display());
        }));
    }
    results
}

fn run(opts: &Args) -> Vec<Result<(), io::Error>> {
    if opts.paths.is_empty() {
        run_inner(opts, &[".".into()], false)
    } else {
        run_inner(opts, &opts.paths, opts.paths.len() > 1)
    }
}

pub fn main() -> ExitCode {
    logsink::setup_logging(false);
    let mut args = Args::parse();

    if !stdout().is_terminal() {
        args.color = false;
    }

    let results = run(&args);
    let code = match results.iter().any(|e| e.is_err()) {
        false => ExitCode::SUCCESS,
        true => ExitCode::FAILURE,
    };

    code
}
