use std::{
    cmp::Ordering,
    time::{SystemTime, SystemTimeError},
};

use filetime::FileTime;

use crate::entry::Stat;

impl Stat {
    /// Detect whether this stat entry is racy if stored in a file index with `timestamp`.
    ///
    /// An index entry is considered racy if it's `mtime` is larger or equal to the index `timestamp`.
    /// The index `timestamp` marks the point in time before which we definitely resolved the racy git problem
    /// for all index entries so any index entries that changed afterwards will need to be examined for
    /// changes by actually reading the file from disk at least once.
    pub fn is_racy(
        &self,
        timestamp: FileTime,
        Options {
            check_stat, use_nsec, ..
        }: Options,
    ) -> bool {
        match timestamp.unix_seconds().cmp(&i64::from(self.mtime.secs)) {
            Ordering::Less => true,
            Ordering::Equal if use_nsec && check_stat => timestamp.nanoseconds() <= self.mtime.nsecs,
            Ordering::Equal => true,
            Ordering::Greater => false,
        }
    }

    /// Compares the stat information of two index entries.
    ///
    /// Intuitively this is basically equivalent to `self == other`.
    /// However there a lot of nobs in git that tweak whether certain stat information is used when checking
    /// equality, see [`Options`].
    /// This function respects those options while performing the stat comparison and may therefore ignore some fields.
    pub fn matches(
        &self,
        other: &Self,
        Options {
            trust_ctime,
            check_stat,
            use_nsec,
            use_stdev,
        }: Options,
    ) -> bool {
        if self.mtime.secs != other.mtime.secs {
            return false;
        }
        if check_stat && use_nsec && self.mtime.nsecs != other.mtime.nsecs {
            return false;
        }

        if self.size != other.size {
            return false;
        }

        if trust_ctime {
            if self.ctime.secs != other.ctime.secs {
                return false;
            }
            if check_stat && use_nsec && self.ctime.nsecs != other.ctime.nsecs {
                return false;
            }
        }

        if check_stat {
            if use_stdev && self.dev != other.dev {
                return false;
            }
            self.ino == other.ino && self.gid == other.gid && self.uid == other.uid
        } else {
            true
        }
    }

    /// Creates stat information from file metadata.
    ///
    /// The information passed to this function should originate from a function like
    /// `symlink_metadata`/`lstat` or `File::metadata`/`fstat`.
    ///
    /// The data are adjusted for use in the index, using default values of fields that are not
    /// meaningful on the target operating system or that are unavailable, and truncating data
    /// where doing so does not lose essential information for keeping track of file status.
    pub fn from_fs(stat: &crate::fs::Metadata) -> Result<Stat, SystemTimeError> {
        let mtime = stat.modified().unwrap_or(std::time::UNIX_EPOCH);
        let ctime = stat.created().unwrap_or(std::time::UNIX_EPOCH);

        #[cfg(windows)]
        let res = Stat {
            mtime: mtime.try_into()?,
            ctime: ctime.try_into()?,
            dev: 0,
            ino: 0,
            uid: 0,
            gid: 0,
            // Truncation to 32 bits is on purpose (git does the same).
            size: stat.len() as u32,
        };
        #[cfg(not(windows))]
        let res = {
            Stat {
                mtime: mtime.try_into().unwrap_or_default(),
                ctime: ctime.try_into().unwrap_or_default(),
                // Truncating the device and inode numbers to 32 bits should be fine even on
                // targets where they are represented as 64 bits, since we do not use them
                // precisely for tracking changes and we do not map them back to the inode.
                dev: stat.dev() as u32,
                ino: stat.ino() as u32,
                uid: stat.uid(),
                gid: stat.gid(),
                // Truncation to 32 bits is on purpose (git does the same).
                size: stat.len() as u32,
            }
        };

        Ok(res)
    }
}

impl TryFrom<SystemTime> for Time {
    type Error = SystemTimeError;
    fn try_from(s: SystemTime) -> Result<Self, SystemTimeError> {
        let d = s.duration_since(std::time::UNIX_EPOCH)?;
        Ok(Time {
            // truncation to 32 bits is on purpose (we only compare the low bits)
            secs: d.as_secs() as u32,
            nsecs: d.subsec_nanos(),
        })
    }
}

impl From<Time> for SystemTime {
    fn from(s: Time) -> Self {
        std::time::UNIX_EPOCH + std::time::Duration::new(s.secs.into(), s.nsecs)
    }
}

/// The time component in a [`Stat`] struct.
#[derive(Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Time {
    /// The amount of seconds elapsed since EPOCH.
    pub secs: u32,
    /// The amount of nanoseconds elapsed in the current second, ranging from 0 to 999.999.999 .
    pub nsecs: u32,
}

impl From<FileTime> for Time {
    fn from(value: FileTime) -> Self {
        Time {
            secs: value.unix_seconds().try_into().expect("can't represent non-unix times"),
            nsecs: value.nanoseconds(),
        }
    }
}

impl PartialEq<FileTime> for Time {
    fn eq(&self, other: &FileTime) -> bool {
        *self == Time::from(*other)
    }
}

impl PartialOrd<FileTime> for Time {
    fn partial_cmp(&self, other: &FileTime) -> Option<Ordering> {
        self.partial_cmp(&Time::from(*other))
    }
}

/// Configuration for comparing stat entries
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub struct Options {
    /// If true, a files creation time is taken into consideration when checking if a file changed.
    /// Can be set to false in case other tools alter the creation time in ways that interfere with our operation.
    ///
    /// Default `true`.
    pub trust_ctime: bool,
    /// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise
    /// nano-second parts of mtime and ctime,uid, gid, inode and device number _will not_ be used, leaving only
    /// the whole-second part of ctime and mtime and the file size to be checked.
    ///
    /// Default `true`.
    pub check_stat: bool,
    /// Whether to compare nano secs when comparing timestamps. This currently
    /// leads to many false positives on linux and is therefore disabled there.
    ///
    /// Default `false`
    pub use_nsec: bool,
    /// Whether to compare network devices secs when comparing timestamps.
    /// Disabled by default because this can cause many false positives on network
    /// devices where the device number is not stable
    ///
    /// Default `false`.
    pub use_stdev: bool,
}

impl Default for Options {
    fn default() -> Self {
        Self {
            trust_ctime: true,
            check_stat: true,
            use_nsec: false,
            use_stdev: false,
        }
    }
}
