#include "physicaldisk.h"
#include "common/io.h"
#include "common/windows/unicode.h"
#include "common/mallocHelper.h"
#include "common/debug.h"

#include <stdalign.h>
#include <windows.h>
#include <winioctl.h>
#include <cfgmgr32.h>

static const char* detectPhysicalDisk(const char* physicalType, const wchar_t* szDevice, FFlist* result, FFPhysicalDiskOptions* options) {
    FF_AUTO_CLOSE_FD HANDLE hDevice = CreateFileW(szDevice, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
    if (hDevice == INVALID_HANDLE_VALUE) {
        return "CreateFileW() failed";
    }

    DWORD retSize;
    FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE;
    FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate();

    const char* interconnect = nullptr;
    uint64_t size = 0;
    {
        alignas(DISK_GEOMETRY_EX) uint8_t dgeBuffer[4096];
        if (DeviceIoControl(
                hDevice,
                IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
                nullptr,
                0,
                dgeBuffer,
                sizeof(dgeBuffer),
                &retSize,
                nullptr)) {
            const DISK_GEOMETRY_EX* dge = (const DISK_GEOMETRY_EX*) dgeBuffer;
            size = (uint64_t) dge->DiskSize.QuadPart;
        } else if (DeviceIoControl(
                       hDevice,
                       IOCTL_DISK_GET_DRIVE_GEOMETRY,
                       nullptr,
                       0,
                       dgeBuffer,
                       sizeof(dgeBuffer),
                       &retSize,
                       nullptr) &&
            retSize >= sizeof(DISK_GEOMETRY)) {
            const DISK_GEOMETRY* dg = (const DISK_GEOMETRY*) dgeBuffer;
            size = (uint64_t) dg->BytesPerSector * dg->SectorsPerTrack * dg->TracksPerCylinder * (uint64_t) dg->Cylinders.QuadPart;
            switch (dg->MediaType) {
                case F3_1Pt44_512:
                case F3_2Pt88_512:
                case F3_20Pt8_512:
                case F3_720_512:
                case F3_120M_512:
                case F3_640_512:
                case F3_1Pt2_512:
                case F3_1Pt23_1024:
                case F3_128Mb_512:
                case F3_230Mb_512:
                case F3_200Mb_512:
                case F3_240M_512:
                case F3_32M_512:
                    ffStrbufSetStatic(&name, "3.5-inch Floppy Disk");
                    break;
                case F5_1Pt2_512:
                case F5_360_512:
                case F5_320_512:
                case F5_320_1024:
                case F5_180_512:
                case F5_160_512:
                case F5_640_512:
                case F5_720_512:
                case F5_1Pt23_1024:
                    ffStrbufSetStatic(&name, "5.25-inch Floppy Disk");
                    break;
                case F8_256_128:
                    ffStrbufSetStatic(&name, "8-inch Floppy Disk");
                    break;
                default:
                    return "Unsupported media type";
            }
            interconnect = "Floppy Controller";
            type |= FF_PHYSICALDISK_TYPE_HDD | FF_PHYSICALDISK_TYPE_REMOVABLE;
        }
    }
    if (size == 0) {
        if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) {
            return "Skipping unknown disk with size 0";
        }

        type |= FF_PHYSICALDISK_TYPE_UNUSED;
    }

    const STORAGE_DEVICE_DESCRIPTOR* sdd = nullptr;
    alignas(STORAGE_DEVICE_DESCRIPTOR) uint8_t sddBuffer[4096];
    if (!interconnect) {
        if (DeviceIoControl(
                hDevice,
                IOCTL_STORAGE_QUERY_PROPERTY,
                &(STORAGE_PROPERTY_QUERY) {
                    .PropertyId = StorageDeviceProperty,
                    .QueryType = PropertyStandardQuery,
                },
                sizeof(STORAGE_PROPERTY_QUERY),
                &sddBuffer,
                sizeof(sddBuffer),
                &retSize,
                nullptr) ||
            retSize == 0) {
            sdd = (const STORAGE_DEVICE_DESCRIPTOR*) sddBuffer;

            switch (sdd->BusType) {
                case BusTypeScsi:
                    interconnect = "SCSI";
                    break;
                case BusTypeAtapi:
                    interconnect = "ATAPI";
                    break;
                case BusTypeAta:
                    interconnect = "ATA";
                    break;
                case BusType1394:
                    interconnect = "IEEE 1394";
                    break;
                case BusTypeSsa:
                    interconnect = "SSA";
                    break;
                case BusTypeFibre:
                    interconnect = "Fibre";
                    break;
                case BusTypeUsb:
                    interconnect = "USB";
                    break;
                case BusTypeRAID:
                    interconnect = "RAID";
                    break;
                case BusTypeiScsi:
                    interconnect = "iSCSI";
                    break;
                case BusTypeSas:
                    interconnect = "SAS";
                    break;
                case BusTypeSata:
                    interconnect = "SATA";
                    break;
                case BusTypeSd:
                    interconnect = "SD";
                    break;
                case BusTypeMmc:
                    interconnect = "MMC";
                    break;
                case BusTypeVirtual:
                    interconnect = "Virtual";
                    type |= FF_PHYSICALDISK_TYPE_VIRTUAL;
                    break;
                case BusTypeFileBackedVirtual:
                    interconnect = "File Backed Virtual";
                    type |= FF_PHYSICALDISK_TYPE_VIRTUAL;
                    break;
                case BusTypeSpaces:
                    interconnect = "Storage Spaces";
                    type |= FF_PHYSICALDISK_TYPE_VIRTUAL;
                    break;
                case BusTypeNvme:
                    interconnect = "NVMe";
                    break;
                case BusTypeSCM:
                    interconnect = "SCM";
                    break;
                case BusTypeUfs:
                    interconnect = "UFS";
                    break;
                case 0x14 /*BusTypeNvmeof*/:
                    interconnect = "NVMe-oF";
                    break;
                default:
                    interconnect = "Unknown";
                    break;
            }

            if (type & FF_PHYSICALDISK_TYPE_VIRTUAL && options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) {
                return "Skipping virtual disk";
            }

            if (sdd->VendorIdOffset != 0) {
                ffStrbufSetS(&name, (const char*) sdd + sdd->VendorIdOffset);
                ffStrbufTrim(&name, ' ');
            }
            if (sdd->ProductIdOffset != 0) {
                if (name.length) {
                    ffStrbufAppendC(&name, ' ');
                }

                ffStrbufAppendS(&name, (const char*) sdd + sdd->ProductIdOffset);
                ffStrbufTrimRight(&name, ' ');
            }
        }
        if (!name.length) {
            ffStrbufSetStatic(&name, physicalType);
        }
    }

    if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) {
        return "Name prefix mismatch";
    }

    FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result);
    ffStrbufInit(&device->serial);
    ffStrbufInit(&device->revision);
    ffStrbufInitMove(&device->name, &name);
    ffStrbufInit(&device->devPath);
    ffStrbufInitStatic(&device->interconnect, interconnect);
    device->type = type;
    device->size = size;
    device->temperature = FF_PHYSICALDISK_TEMP_UNSET;

    ffStrbufSetWS(&device->devPath, szDevice);

    if (sdd) {
        if (sdd->SerialNumberOffset != 0) {
            ffStrbufSetS(&device->serial, (const char*) sdd + sdd->SerialNumberOffset);
            ffStrbufTrimSpace(&device->serial);
        }

        if (sdd->ProductRevisionOffset != 0) {
            ffStrbufSetS(&device->revision, (const char*) sdd + sdd->ProductRevisionOffset);
            ffStrbufTrimRightSpace(&device->revision);
        }

        device->type |= sdd->RemovableMedia ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED;
    }

    {
        alignas(GET_MEDIA_TYPES) uint8_t buffer[4096];
        GET_MEDIA_TYPES* gmt = (GET_MEDIA_TYPES*) buffer;
        if (DeviceIoControl(
                hDevice,
                IOCTL_STORAGE_GET_MEDIA_TYPES_EX,
                nullptr,
                0,
                gmt,
                sizeof(buffer),
                &retSize,
                nullptr) &&
            gmt->MediaInfoCount > 0) {
            // DiskInfo and RemovableDiskInfo have the same structures. TapeInfo doesn't.
            if (gmt->DeviceType != FILE_DEVICE_TAPE) {
                auto diskInfo = &gmt->MediaInfo[0].DeviceSpecific.DiskInfo;
                if (diskInfo->MediaCharacteristics & MEDIA_READ_ONLY) {
                    device->type |= FF_PHYSICALDISK_TYPE_READONLY;
                } else if (diskInfo->MediaCharacteristics & MEDIA_READ_WRITE) {
                    device->type |= FF_PHYSICALDISK_TYPE_READWRITE;
                }
            } else {
                auto tapeInfo = &gmt->MediaInfo[0].DeviceSpecific.TapeInfo;
                if (tapeInfo->MediaCharacteristics & MEDIA_READ_ONLY) {
                    device->type |= FF_PHYSICALDISK_TYPE_READONLY;
                } else if (tapeInfo->MediaCharacteristics & MEDIA_READ_WRITE) {
                    device->type |= FF_PHYSICALDISK_TYPE_READWRITE;
                }
            }
        }
    }

    if (!(device->type & FF_PHYSICALDISK_TYPE_VIRTUAL) && !(device->type & FF_PHYSICALDISK_TYPE_HDD)) {
        DEVICE_SEEK_PENALTY_DESCRIPTOR dspd = {};
        if (DeviceIoControl(
                hDevice,
                IOCTL_STORAGE_QUERY_PROPERTY,
                &(STORAGE_PROPERTY_QUERY) {
                    .PropertyId = StorageDeviceSeekPenaltyProperty,
                    .QueryType = PropertyStandardQuery,
                },
                sizeof(STORAGE_PROPERTY_QUERY),
                &dspd,
                sizeof(dspd),
                &retSize,
                nullptr) &&
            retSize == sizeof(dspd)) {
            device->type |= dspd.IncursSeekPenalty ? FF_PHYSICALDISK_TYPE_HDD : FF_PHYSICALDISK_TYPE_SSD;
        }

        if (options->temp) {
            STORAGE_TEMPERATURE_DATA_DESCRIPTOR stdd = {};
            if (DeviceIoControl(
                    hDevice,
                    IOCTL_STORAGE_QUERY_PROPERTY,
                    &(STORAGE_PROPERTY_QUERY) {
                        .PropertyId = StorageDeviceTemperatureProperty,
                        .QueryType = PropertyStandardQuery,
                    },
                    sizeof(STORAGE_PROPERTY_QUERY),
                    &stdd,
                    sizeof(stdd),
                    &retSize,
                    nullptr) &&
                retSize == sizeof(stdd)) {
                device->temperature = stdd.TemperatureInfo[0].Temperature;
            }
        }
    }

    return nullptr;
}

static void detectPhysicalDisksByInterfaceClass(const char* type, const GUID* interfaceClassGuid, FFlist* result, FFPhysicalDiskOptions* options) {
    ULONG cchDeviceInterfaces = 0;
    if (CM_Get_Device_Interface_List_SizeW(
            &cchDeviceInterfaces,
            (LPGUID) interfaceClassGuid,
            nullptr,
            CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CR_SUCCESS ||
        cchDeviceInterfaces <= 1) {
        return;
    }

    FF_AUTO_FREE wchar_t* mszDeviceInterfaces = (wchar_t*) malloc(cchDeviceInterfaces * sizeof(wchar_t));
    if (!mszDeviceInterfaces) {
        return;
    }

    if (CM_Get_Device_Interface_ListW(
            (LPGUID) interfaceClassGuid,
            nullptr,
            mszDeviceInterfaces,
            cchDeviceInterfaces,
            CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CR_SUCCESS) {
        return;
    }

    // MULTI_SZ: "str1\0str2\0...\0\0"
    for (const wchar_t* p = mszDeviceInterfaces; *p; p += wcslen(p) + 1) {
        FF_DEBUG("Probing %s: %ls", type, p);
        [[maybe_unused]] const char* error = detectPhysicalDisk(type, p, result, options);
        if (error == nullptr) {
            FF_DEBUG("Detected device \"%s\"", FF_LIST_LAST(FFPhysicalDiskResult, *result)->name.chars);
        } else {
            FF_DEBUG("Failed to detect device %s: %s", type, error);
        }
    }
}

const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) {
    detectPhysicalDisksByInterfaceClass("Floppy", &GUID_DEVINTERFACE_FLOPPY, result, options);
    detectPhysicalDisksByInterfaceClass("Disk", &GUID_DEVINTERFACE_DISK, result, options);
    detectPhysicalDisksByInterfaceClass("CD-ROM", &GUID_DEVINTERFACE_CDROM, result, options);
    detectPhysicalDisksByInterfaceClass("Tape", &GUID_DEVINTERFACE_TAPE, result, options);
    return nullptr;
}
