// Fat12ImageLib.cs
// Minimal C# library for creating a FAT12 disk image and adding files (root directory only)
// Target: .NET 6+
// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction.

using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Fat12Lib
{
    public class Fat12ImageOptions
    {
        // Total size of the image in bytes. Common sizes: 360 KiB, 720 KiB, 1.2 MiB, 1.44 MiB, 2.88 MiB, etc.
        public long TotalSizeBytes { get; set; } = 1440L * 1024; // 1.44 MiB default
        public ushort BytesPerSector { get; set; } = 512;
        public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size
        public byte NumFats { get; set; } = 2;
        public ushort ReservedSectors { get; set; } = 1; // boot sector
        public ushort MaxRootDirEntries { get; set; } = 224; // typical for 1.44MB floppy
        public byte MediaDescriptor { get; set; } = 0xF0; // floppy
        public ushort SectorsPerTrack { get; set; } = 18; // 1.44MB geometry (not critical)
        public ushort NumberOfHeads { get; set; } = 2; // geometry (not critical)
        public uint HiddenSectors { get; set; } = 0;
        public string OemId { get; set; } = "MSDOS5.0";
        public string VolumeLabel { get; set; } = "NO NAME    "; // exactly 11 chars; will be padded/trimmed
        public string FsTypeLabel { get; set; } = "FAT12   "; // 8 chars in boot sector label
        public ushort? VolumeId { get; set; } = null; // null => random
    }

    public class Fat12Image
    {
        private readonly Fat12ImageOptions _opt;

        // Computed layout
        private uint _totalSectors; // 16-bit or 32-bit depending on size
        private ushort _rootDirSectors;
        private ushort _fatSectors; // sectors per FAT
        private uint _firstFatSectorLba;
        private uint _firstRootDirSectorLba;
        private uint _firstDataSectorLba;
        private uint _clusterCount; // data clusters (not counting 0 and 1)
        private byte _sectorsPerCluster;

        // Runtime buffers
        private byte[] _bootSector = Array.Empty<byte>();
        private byte[] _fat; // a single FAT table; will be duplicated NumFats times
        private byte[] _rootDir; // root directory area (fixed size)
        private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps)

        // Allocation state
        private uint _nextFreeCluster = 2; // FAT clusters start at 2
        private int _nextFreeRootDirEntry = 0;

        public Fat12Image(Fat12ImageOptions options)
        {
            _opt = options;
            if (_opt.BytesPerSector != 512)
                throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation.");
            if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0)
                throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector.");

            InitializeLayout();
            BuildBootSector();
            InitializeFatAndRoot();
        }

        // Adds a file into the root directory. Uses 8.3 name rules.
        public void AddFile(string fileName, byte[] content, DateTime? timestamp = null)
        {
            if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName");
            if (content == null) throw new ArgumentNullException(nameof(content));

            var (name83, ext83) = ToShortName(fileName);
            if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries)
                throw new IOException("Root directory is full.");

            // Compute how many clusters needed
            uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster);
            uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster);
            if (clustersNeeded == 0) clustersNeeded = 1; // allocate 1 cluster to store 0-byte file metadata/simple handling

            if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount)
                throw new IOException("Not enough free space for file.");

            // Allocate cluster chain
            List<uint> chain = new();
            for (int i = 0; i < clustersNeeded; i++)
            {
                uint c = _nextFreeCluster++;
                chain.Add(c);
            }

            // Write data to clusters
            int srcOffset = 0;
            foreach (var c in chain)
            {
                uint lba = ClusterToLba(c);
                uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector;
                int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset));
                if (toCopy > 0)
                    Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy);
                srcOffset += toCopy;
            }

            // Link FAT entries (12-bit values)
            for (int i = 0; i < chain.Count - 1; i++)
                SetFat12Entry(chain[i], (ushort)chain[i + 1]);
            SetFat12Entry(chain[^1], 0xFFF); // EOF for FAT12

            // Build directory entry
            var dt = timestamp ?? DateTime.Now;
            ushort time = ToFatTime(dt);
            ushort date = ToFatDate(dt);

            Span<byte> entry = stackalloc byte[32];
            entry.Clear();
            Encoding.ASCII.GetBytes(name83, entry);
            Encoding.ASCII.GetBytes(ext83, entry.Slice(8));
            entry[11] = 0x20; // Archive
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx)
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date
            BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster
            BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size

            // Write entry into root dir
            int rootOffset = _nextFreeRootDirEntry * 32;
            _rootDir.AsSpan(rootOffset, 32).Clear();
            entry.CopyTo(_rootDir.AsSpan(rootOffset));
            _nextFreeRootDirEntry++;
        }

        public byte[] GetImage()
        {
            using var ms = new MemoryStream((int)(_opt.TotalSizeBytes));

            // Boot sector
            ms.Write(_bootSector, 0, _bootSector.Length);

            // FATs
            for (int f = 0; f < _opt.NumFats; f++)
                ms.Write(_fat, 0, _fat.Length);

            // Root directory
            ms.Write(_rootDir, 0, _rootDir.Length);

            // Data region
            ms.Write(_data, 0, _data.Length);

            // Pad to exact size if necessary (should be exact)
            if (ms.Length < _opt.TotalSizeBytes)
                ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length));

            return ms.ToArray();
        }

        public void SaveToFile(string path)
        {
            File.WriteAllBytes(path, GetImage());
        }

        private void InitializeLayout()
        {
            _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector);

            // Decide sectors per cluster if not specified
            _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector);

            _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);

            // Iteratively solve for FAT size and cluster count (FAT12 uses 12-bit entries => 1.5 bytes per entry)
            ushort fatSectors = 1;
            while (true)
            {
                uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors;
                uint clusterCount = dataSectors / _sectorsPerCluster;
                if (clusterCount < 1 || clusterCount > 4084)
                    throw new NotSupportedException($"Cluster count {clusterCount} out of FAT12 bounds (needs 1..4,084). Adjust size or sectors/cluster.");

                // FAT12 entries: (clusterCount + 2) entries, each 12 bits => total bytes = ceil(3 * entries / 2)
                uint entries = clusterCount + 2;
                uint fatBytes = (entries * 3 + 1) / 2; // ceil(1.5 * entries)
                ushort requiredFatSectors = (ushort)((fatBytes + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector);
                if (requiredFatSectors == fatSectors)
                {
                    _fatSectors = fatSectors;
                    _clusterCount = clusterCount;
                    break;
                }
                fatSectors = requiredFatSectors;
            }

            _firstFatSectorLba = _opt.ReservedSectors;
            _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors);
            _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors;

            // Buffers
            _bootSector = new byte[_opt.BytesPerSector];
            _fat = new byte[_fatSectors * _opt.BytesPerSector];
            _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector];
            uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors;
            _data = new byte[dataSectorsFinal * _opt.BytesPerSector];
        }

        private void BuildBootSector()
        {
            Span<byte> bs = _bootSector;
            bs.Clear();

            // Jump + OEM
            bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short
            Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8));

            // BIOS Parameter Block (BPB)
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector);
            bs[13] = _sectorsPerCluster;
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors);
            bs[16] = _opt.NumFats;
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries);
            ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0;
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16);
            bs[21] = _opt.MediaDescriptor;
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors);
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack);
            BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads);
            BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors);
            BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors);

            // Extended BPB for FAT12/FAT16
            ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF);
            BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId);
            string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant();
            label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray());
            label = label.PadRight(11).Substring(0, 11);
            Encoding.ASCII.GetBytes(label, bs.Slice(43, 11));
            string fstype = (_opt.FsTypeLabel ?? "FAT12").PadRight(8).Substring(0, 8);
            Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8));

            // Minimal bootstrap code area (ignored for data disks)
            bs[_opt.BytesPerSector - 2] = 0x55; // signature
            bs[_opt.BytesPerSector - 1] = 0xAA;
        }

        private void InitializeFatAndRoot()
        {
            // FAT initial entries for FAT12
            // First three bytes: media descriptor and end markers -> usually F0 FF FF for 0xF0 media
            Array.Clear(_fat, 0, _fat.Length);
            _fat[0] = _opt.MediaDescriptor; // e.g., 0xF0
            _fat[1] = 0xFF;
            _fat[2] = 0xFF; // cluster 1 reserved/end
        }

        private uint ClusterToLba(uint cluster)
        {
            if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster));
            return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster;
        }

        private void SetFat12Entry(uint cluster, ushort value)
        {
            if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster));
            // value is 12-bit
            value &= 0x0FFF;
            int index = (int)(cluster + (cluster / 2)); // floor(1.5 * cluster)
            if ((cluster & 1) == 0)
            {
                // even cluster: low 12 bits starting at index
                _fat[index] = (byte)((_fat[index] & 0x00) | (value & 0xFF));
                _fat[index + 1] = (byte)((_fat[index + 1] & 0xF0) | ((value >> 8) & 0x0F));
            }
            else
            {
                // odd cluster: high 12 bits overlapping starting at index
                _fat[index] = (byte)((_fat[index] & 0x0F) | ((value << 4) & 0xF0));
                _fat[index + 1] = (byte)((_fat[index + 1] & 0x00) | ((value >> 4) & 0xFF));
            }
        }

        private static (string name, string ext) ToShortName(string fileName)
        {
            // Extract base and extension, sanitize to 8.3 uppercase ASCII
            string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE";
            string ext = Path.GetExtension(fileName);
            if (ext.StartsWith('.')) ext = ext.Substring(1);

            string Sanitize(string s) => new string(s.ToUpperInvariant()
                .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_')
                .ToArray());

            string n = Sanitize(name);
            string e = Sanitize(ext);
            if (n.Length > 8) n = n.Substring(0, 8);
            if (e.Length > 3) e = e.Substring(0, 3);
            n = n.PadRight(8);
            e = e.PadRight(3);
            return (n, e);
        }

        private static ushort ToFatDate(DateTime dt)
        {
            int year = Math.Clamp(dt.Year - 1980, 0, 127);
            int month = Math.Clamp(dt.Month, 1, 12);
            int day = Math.Clamp(dt.Day, 1, 31);
            return (ushort)((year << 9) | (month << 5) | day);
        }

        private static ushort ToFatTime(DateTime dt)
        {
            int hour = Math.Clamp(dt.Hour, 0, 23);
            int minute = Math.Clamp(dt.Minute, 0, 59);
            int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution
            return (ushort)((hour << 11) | (minute << 5) | second);
        }

        private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector)
        {
            // Simple mapping aimed at floppy-like sizes with 512B sectors
            long sizeKiB = (long)totalSectors * bytesPerSector / 1024;
            if (sizeKiB <= 720) return 1;       // 360K/720K
            if (sizeKiB <= 1440) return 1;      // 1.44MB
            if (sizeKiB <= 2400) return 1;      // 2.4MB
            if (sizeKiB <= 2880) return 1;      // 2.88MB
            if (sizeKiB <= 4096) return 2;      // up to 4MB
            if (sizeKiB <= 8192) return 4;      // up to 8MB
            return 8;                            // larger volumes still FAT12 but uncommon
        }
    }
}

// --------- Example usage (not part of the library) ---------
// using System.Text;
// var img = new Fat12Lib.Fat12Image(new Fat12Lib.Fat12ImageOptions { TotalSizeBytes = 1440L * 1024, VolumeLabel = "MY12DISK" });
// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT12!"));
// img.SaveToFile("fat12.img");
