// Take a look at the license at the top of the repository in the LICENSE file.

use crate::sys::ffi;
use crate::sys::{
    disk::{get_str_value, DictKey},
    macos::utils::IOReleaser,
    utils::CFReleaser,
};
use crate::DiskType;

use core_foundation_sys::base::{kCFAllocatorDefault, kCFAllocatorNull};
use core_foundation_sys::string as cfs;

use std::ffi::CStr;

pub(crate) fn get_disk_type(disk: &libc::statfs) -> Option<DiskType> {
    let characteristics_string = unsafe {
        CFReleaser::new(cfs::CFStringCreateWithBytesNoCopy(
            kCFAllocatorDefault,
            ffi::kIOPropertyDeviceCharacteristicsKey.as_ptr(),
            ffi::kIOPropertyDeviceCharacteristicsKey.len() as _,
            cfs::kCFStringEncodingUTF8,
            false as _,
            kCFAllocatorNull,
        ))?
    };

    // Removes `/dev/` from the value.
    let bsd_name = unsafe {
        CStr::from_ptr(disk.f_mntfromname.as_ptr())
            .to_bytes()
            .strip_prefix(b"/dev/")
            .or_else(|| {
                sysinfo_debug!("unknown disk mount path format");
                None
            })?
    };

    // We don't need to wrap this in an auto-releaser because the following call to `IOServiceGetMatchingServices`
    // will take ownership of one retain reference.
    let matching =
        unsafe { ffi::IOBSDNameMatching(ffi::kIOMasterPortDefault, 0, bsd_name.as_ptr().cast()) };

    if matching.is_null() {
        return None;
    }

    let mut service_iterator: ffi::io_iterator_t = 0;

    if unsafe {
        ffi::IOServiceGetMatchingServices(
            ffi::kIOMasterPortDefault,
            matching.cast(),
            &mut service_iterator,
        )
    } != libc::KERN_SUCCESS
    {
        return None;
    }

    // Safety: We checked for success, so there is always a valid iterator, even if its empty.
    let service_iterator = unsafe { IOReleaser::new_unchecked(service_iterator) };

    let mut parent_entry: ffi::io_registry_entry_t = 0;

    while let Some(mut current_service_entry) =
        IOReleaser::new(unsafe { ffi::IOIteratorNext(service_iterator.inner()) })
    {
        // Note: This loop is required in a non-obvious way. Due to device properties existing as a tree
        // in IOKit, we may need an arbitrary number of calls to `IORegistryEntryCreateCFProperty` in order to find
        // the values we are looking for. The function may return nothing if we aren't deep enough into the registry
        // tree, so we need to continue going from child->parent node until its found.
        loop {
            if unsafe {
                ffi::IORegistryEntryGetParentEntry(
                    current_service_entry.inner(),
                    ffi::kIOServicePlane.as_ptr().cast(),
                    &mut parent_entry,
                )
            } != libc::KERN_SUCCESS
            {
                break;
            }

            current_service_entry = match IOReleaser::new(parent_entry) {
                Some(service) => service,
                // There were no more parents left
                None => break,
            };

            let properties_result = unsafe {
                CFReleaser::new(ffi::IORegistryEntryCreateCFProperty(
                    current_service_entry.inner(),
                    characteristics_string.inner(),
                    kCFAllocatorDefault,
                    0,
                ))
            };

            if let Some(device_properties) = properties_result {
                let disk_type = unsafe {
                    super::disk::get_str_value(
                        device_properties.inner(),
                        DictKey::Defined(ffi::kIOPropertyMediumTypeKey),
                    )
                };

                if let Some(disk_type) = disk_type.and_then(|medium| match medium.as_str() {
                    _ if medium == ffi::kIOPropertyMediumTypeSolidStateKey => Some(DiskType::SSD),
                    _ if medium == ffi::kIOPropertyMediumTypeRotationalKey => Some(DiskType::HDD),
                    _ => None,
                }) {
                    return Some(disk_type);
                } else {
                    // Many external drive vendors do not advertise their device's storage medium.
                    //
                    // In these cases, assuming that there were _any_ properties about them registered, we fallback
                    // to `HDD` when no storage medium is provided by the device instead of `Unknown`.
                    return Some(DiskType::HDD);
                }
            }
        }
    }

    None
}
