//! The on-disk layout of a FAT filesystem — the boot sector / BIOS Parameter
//! Block, directory entries, long-file-name entries, and the FAT32 FSInfo — as
//! `align(1)` extern structs that bit-cast straight out of a 512-byte sector
//! (multi-byte fields are little-endian, like usb-abi.zig). Pure data, plus the
//! cluster-count FAT-type detection. Host-testable.

const std = @import("std");

/// The BIOS Parameter Block, common to FAT12/16/32 (offset 0..36 of the boot
/// sector). The extended part that follows differs by FAT type.
pub const BiosParameterBlock = extern struct {
    jump: [3]u8,
    oem_name: [8]u8,
    bytes_per_sector: u16 align(1),
    sectors_per_cluster: u8,
    reserved_sector_count: u16 align(1),
    fat_count: u8,
    root_entry_count: u16 align(1),
    total_sectors_16: u16 align(1),
    media: u8,
    fat_size_16: u16 align(1),
    sectors_per_track: u16 align(1),
    head_count: u16 align(1),
    hidden_sectors: u32 align(1),
    total_sectors_32: u32 align(1),
};

/// The FAT12/16 extended boot record (offset 36).
pub const ExtendedBootRecord16 = extern struct {
    drive_number: u8,
    reserved: u8,
    boot_signature: u8,
    volume_id: u32 align(1),
    volume_label: [11]u8,
    filesystem_type: [8]u8,
};

/// The FAT32 extended boot record (offset 36).
pub const ExtendedBootRecord32 = extern struct {
    fat_size_32: u32 align(1),
    extended_flags: u16 align(1),
    filesystem_version: u16 align(1),
    root_cluster: u32 align(1),
    filesystem_information_sector: u16 align(1),
    backup_boot_sector: u16 align(1),
    reserved: [12]u8,
    drive_number: u8,
    reserved1: u8,
    boot_signature: u8,
    volume_id: u32 align(1),
    volume_label: [11]u8,
    filesystem_type: [8]u8,
};

/// A 32-byte directory entry (8.3 short name form).
pub const DirectoryEntry = extern struct {
    name: [11]u8, // 8 name + 3 extension, space-padded
    attributes: u8,
    reserved_nt: u8,
    creation_time_tenth: u8,
    creation_time: u16 align(1),
    creation_date: u16 align(1),
    last_access_date: u16 align(1),
    first_cluster_high: u16 align(1),
    write_time: u16 align(1),
    write_date: u16 align(1),
    first_cluster_low: u16 align(1),
    file_size: u32 align(1),

    pub fn firstCluster(self: DirectoryEntry) u32 {
        return (@as(u32, self.first_cluster_high) << 16) | self.first_cluster_low;
    }
    pub fn setFirstCluster(self: *DirectoryEntry, cluster: u32) void {
        self.first_cluster_low = @truncate(cluster);
        self.first_cluster_high = @truncate(cluster >> 16);
    }
    pub fn isFree(self: DirectoryEntry) bool {
        return self.name[0] == 0x00 or self.name[0] == 0xE5;
    }
    pub fn isEnd(self: DirectoryEntry) bool {
        return self.name[0] == 0x00;
    }
    pub fn isDirectory(self: DirectoryEntry) bool {
        return self.attributes & attribute_directory != 0;
    }
    pub fn isLongName(self: DirectoryEntry) bool {
        return self.attributes & attribute_long_name_mask == attribute_long_name;
    }
    pub fn isVolumeLabel(self: DirectoryEntry) bool {
        return self.attributes & attribute_volume_id != 0 and !self.isLongName();
    }
};

/// A 32-byte long-file-name entry (attributes == 0x0F). A sequence of these
/// precedes the 8.3 entry they name, each carrying 13 UTF-16 code units.
pub const LongNameEntry = extern struct {
    order: u8,
    name1: [5]u16 align(1),
    attributes: u8,
    kind: u8,
    checksum: u8,
    name2: [6]u16 align(1),
    first_cluster_low: u16 align(1),
    name3: [2]u16 align(1),
};

/// The FAT32 FSInfo sector (usually sector 1): advisory free-cluster bookkeeping.
pub const FileSystemInformation = extern struct {
    lead_signature: u32 align(1), // 0x41615252
    reserved1: [480]u8,
    struct_signature: u32 align(1), // 0x61417272
    free_count: u32 align(1),
    next_free: u32 align(1),
    reserved2: [12]u8,
    trail_signature: u32 align(1), // 0xAA550000
};

// Directory-entry attribute bits.
pub const attribute_read_only: u8 = 0x01;
pub const attribute_hidden: u8 = 0x02;
pub const attribute_system: u8 = 0x04;
pub const attribute_volume_id: u8 = 0x08;
pub const attribute_directory: u8 = 0x10;
pub const attribute_archive: u8 = 0x20;
pub const attribute_long_name: u8 = 0x0F; // read_only|hidden|system|volume_id
pub const attribute_long_name_mask: u8 = 0x3F;

// FSInfo signatures.
pub const fsinfo_lead_signature: u32 = 0x41615252;
pub const fsinfo_struct_signature: u32 = 0x61417272;
pub const fsinfo_trail_signature: u32 = 0xAA550000;

/// End-of-chain markers (a cluster value >= these ends a chain).
pub const end_of_chain_12: u32 = 0xFF8;
pub const end_of_chain_16: u32 = 0xFFF8;
pub const end_of_chain_32: u32 = 0x0FFFFFF8;
pub const bad_cluster_32: u32 = 0x0FFFFFF7;

pub const free_cluster: u32 = 0;
pub const boot_signature_offset: usize = 510; // 0x55 0xAA at the end of the boot sector

pub const FatType = enum { fat12, fat16, fat32 };

/// The geometry derived from the BPB, plus the FAT type (by the Microsoft
/// cluster-count rule: <4085 FAT12, <65525 FAT16, else FAT32).
pub const Geometry = struct {
    fat_type: FatType,
    bytes_per_sector: u32,
    sectors_per_cluster: u32,
    reserved_sector_count: u32,
    fat_count: u32,
    fat_size_sectors: u32, // per FAT
    root_entry_count: u32, // FAT12/16
    root_cluster: u32, // FAT32
    first_data_sector: u32,
    total_sectors: u32,
    cluster_count: u32,
    fsinfo_sector: u32, // FAT32
};

/// Derive the geometry (and FAT type) from a boot sector's first 512 bytes.
/// Returns null if the sector is not a plausible FAT boot sector.
pub fn geometryOf(sector: []const u8) ?Geometry {
    if (sector.len < 512) return null;
    if (sector[boot_signature_offset] != 0x55 or sector[boot_signature_offset + 1] != 0xAA) return null;
    const bpb = std.mem.bytesToValue(BiosParameterBlock, sector[0..@sizeOf(BiosParameterBlock)]);
    if (bpb.bytes_per_sector == 0 or bpb.sectors_per_cluster == 0 or bpb.fat_count == 0) return null;

    const fat_size_16: u32 = bpb.fat_size_16;
    var fat_size: u32 = fat_size_16;
    var root_cluster: u32 = 0;
    var fsinfo_sector: u32 = 0;
    if (fat_size_16 == 0) {
        const ebr = std.mem.bytesToValue(ExtendedBootRecord32, sector[36 .. 36 + @sizeOf(ExtendedBootRecord32)]);
        fat_size = ebr.fat_size_32;
        root_cluster = ebr.root_cluster;
        fsinfo_sector = ebr.filesystem_information_sector;
    }

    const total_sectors: u32 = if (bpb.total_sectors_16 != 0) bpb.total_sectors_16 else bpb.total_sectors_32;
    const root_dir_sectors = (@as(u32, bpb.root_entry_count) * 32 + bpb.bytes_per_sector - 1) / bpb.bytes_per_sector;
    const first_data_sector = bpb.reserved_sector_count + bpb.fat_count * fat_size + root_dir_sectors;
    if (total_sectors < first_data_sector) return null;
    const data_sectors = total_sectors - first_data_sector;
    const cluster_count = data_sectors / bpb.sectors_per_cluster;

    const fat_type: FatType = if (cluster_count < 4085) .fat12 else if (cluster_count < 65525) .fat16 else .fat32;

    return .{
        .fat_type = fat_type,
        .bytes_per_sector = bpb.bytes_per_sector,
        .sectors_per_cluster = bpb.sectors_per_cluster,
        .reserved_sector_count = bpb.reserved_sector_count,
        .fat_count = bpb.fat_count,
        .fat_size_sectors = fat_size,
        .root_entry_count = bpb.root_entry_count,
        .root_cluster = root_cluster,
        .first_data_sector = first_data_sector,
        .total_sectors = total_sectors,
        .cluster_count = cluster_count,
        .fsinfo_sector = fsinfo_sector,
    };
}

// --- DOS date/time <-> Unix epoch --------------------------------------------
//
// FAT stamps a file's modification time as two 16-bit DOS fields. There is no
// timezone, so danos treats them as UTC. `date`: year-1980(7)|month(4)|day(5);
// `time`: hour(5)|minute(6)|(second/2)(5).

fn isLeapYear(year: u32) bool {
    return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0);
}

const days_in_month = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

/// Convert a FAT date+time to Unix epoch seconds (UTC). Returns 0 for an unset
/// (zero) date.
pub fn fatToEpoch(date: u16, time: u16) u64 {
    if (date == 0) return 0;
    const day: u32 = date & 0x1F;
    const month: u32 = (date >> 5) & 0x0F;
    const year: u32 = 1980 + (date >> 9);
    if (month < 1 or month > 12 or day < 1) return 0;
    const second: u32 = @as(u32, time & 0x1F) * 2;
    const minute: u32 = (time >> 5) & 0x3F;
    const hour: u32 = (time >> 11) & 0x1F;

    var days: u64 = 0;
    var y: u32 = 1970;
    while (y < year) : (y += 1) days += if (isLeapYear(y)) 366 else 365;
    var m: u32 = 1;
    while (m < month) : (m += 1) {
        days += days_in_month[m - 1];
        if (m == 2 and isLeapYear(year)) days += 1;
    }
    days += day - 1;
    return ((days * 24 + hour) * 60 + minute) * 60 + second;
}

pub const FatDateTime = struct { date: u16, time: u16 };

/// Convert Unix epoch seconds (UTC) to a FAT date+time. Returns {0,0} for epoch 0 or
/// any time before 1980 (which DOS cannot represent).
pub fn epochToFatDateTime(epoch: u64) FatDateTime {
    if (epoch == 0) return .{ .date = 0, .time = 0 };
    var remaining = epoch;
    const second: u32 = @intCast(remaining % 60);
    remaining /= 60;
    const minute: u32 = @intCast(remaining % 60);
    remaining /= 60;
    const hour: u32 = @intCast(remaining % 24);
    remaining /= 24;
    var days: u32 = @intCast(remaining); // whole days since 1970-01-01

    var year: u32 = 1970;
    while (true) {
        const y_days: u32 = if (isLeapYear(year)) 366 else 365;
        if (days < y_days) break;
        days -= y_days;
        year += 1;
    }
    if (year < 1980) return .{ .date = 0, .time = 0 };
    var month: u32 = 1;
    while (true) {
        var m_days: u32 = days_in_month[month - 1];
        if (month == 2 and isLeapYear(year)) m_days += 1;
        if (days < m_days) break;
        days -= m_days;
        month += 1;
    }
    const day = days + 1;
    return .{
        .date = @intCast(((year - 1980) << 9) | (month << 5) | day),
        .time = @intCast((hour << 11) | (minute << 5) | (second / 2)),
    };
}

test "FAT date/time <-> Unix epoch round trip" {
    // Even-second UTC times (FAT stores seconds/2, so even seconds round-trip exactly).
    for ([_]u64{ 1_577_836_800, 1_700_000_000, 1_262_304_000, 1_783_971_244 }) |epoch| {
        const fat = epochToFatDateTime(epoch);
        try std.testing.expectEqual(epoch, fatToEpoch(fat.date, fat.time));
    }
    // Absolute check: 1577836800 is 2020-01-01 00:00:00 UTC.
    const y2020 = epochToFatDateTime(1_577_836_800);
    try std.testing.expectEqual(@as(u16, 2020), 1980 + (y2020.date >> 9));
    try std.testing.expectEqual(@as(u16, 1), (y2020.date >> 5) & 0x0F); // month
    try std.testing.expectEqual(@as(u16, 1), y2020.date & 0x1F); // day
    // 0 is "unset" both ways.
    try std.testing.expectEqual(@as(u64, 0), fatToEpoch(0, 0));
    try std.testing.expectEqual(@as(u16, 0), epochToFatDateTime(0).date);
}

test "on-disk struct sizes match the specification" {
    try std.testing.expectEqual(@as(usize, 36), @sizeOf(BiosParameterBlock));
    try std.testing.expectEqual(@as(usize, 26), @sizeOf(ExtendedBootRecord16));
    try std.testing.expectEqual(@as(usize, 54), @sizeOf(ExtendedBootRecord32));
    try std.testing.expectEqual(@as(usize, 32), @sizeOf(DirectoryEntry));
    try std.testing.expectEqual(@as(usize, 32), @sizeOf(LongNameEntry));
    try std.testing.expectEqual(@as(usize, 512), @sizeOf(FileSystemInformation));
}

test "directory entry cluster split/join" {
    var entry = std.mem.zeroes(DirectoryEntry);
    entry.setFirstCluster(0x01234567);
    try std.testing.expectEqual(@as(u16, 0x4567), entry.first_cluster_low);
    try std.testing.expectEqual(@as(u16, 0x0123), entry.first_cluster_high);
    try std.testing.expectEqual(@as(u32, 0x01234567), entry.firstCluster());
}
