#pragma once

#include "../../../cppruntime/runtime.hpp"
#include "../filesystem.hpp"
#include "../../../main.h"

namespace helos {

namespace filesystem {


// FAT is a FAT12/16/32 driver.
class FAT: public Filesystem {
private:
	// Type of the FAT filesystem (FAT12, FAT16 or FAT32)
	enum FATType {
		FAT12,
		FAT16,
		FAT32,
		exFAT,
	};


	// The 1st sector of the FAT filesystem, BIOS Parameter Block and Extended Boot Record
	// Same for FAT12/16 and FAT32.
	struct BIOSParamBlock {
		char     jumpcode[3];            // EB XX 90, a jmp:short XX; nop command
		char     oemid[8];               // OEM identifier, can be any string
		uint16_t numBytesPerSector;      // number of bytes per sector (512)
		uint8_t  numSectorsPerCluster;   // number of sectors per cluster (8, 512*8=4096)
		uint16_t numReservedSector;      // number of reserved sectors, including the BPB
		uint8_t  numFATs;                // number of FATs, often 2
		uint16_t numDirEntry;            // number of directory entries
		uint16_t numSectors_short;       // total sectors in the volume, 0 if the value is too large to fit and in sectorCount_large;
		uint8_t  mediaDescriptorType;    // Media Descriptor Type
		uint16_t numSectorsPerFAT_12_16; // number of sectors per FAT, set only for FAT12/16
		uint16_t numSectorsPerTrack;     // number of sectors per track
		uint16_t numSides;               // number of heads or sides of the media
		uint32_t offsetLBA;              // number of hidden sectors (i.e., the LBA of the beginning of the partition.)
		uint32_t numSectors_large;       // large sector count, set if sectorCount_short is 0
	} PACKED;
	static_assert(sizeof(BIOSParamBlock) == 36, "BIOS Parameter Block struct not packed");


	// Extended Boot Record for FAT12/16, comes right after BPB
	struct ExtBootRecord_12_16 {
		uint8_t  driveNumber; // drive number, i.e., 0x00 for floppy and 0x80 for harddisk. Useless.
		uint8_t  ntFlags;     // flags in Windows NT, reserved and zero.
		uint8_t  signature;   // signature, must be 0x28 or 0x29.
		uint32_t volumeID;    // volume ID (partition UUID)
		char     label[11];   // label, padded with spaces
		char     type[8];     // system identifier string, containing the FAT filesystem type. Not to be trusted.
	} PACKED;
	static_assert(sizeof(ExtBootRecord_12_16) == 62 - 36, "ExtBootRecord_12_16 not packed");


	// Extended Boot Record for FAT32, comes right after BPB
	struct ExtBootRecord_32 {
		uint32_t sectorsPerFAT;        // number of sectors in per FAT
		uint16_t flags;                // flags
		uint16_t version;              // FAT version number. High byte is the major version and low byte is the minor. Can be zero.
		uint32_t clusterRoot;          // the cluster number (offset) of the root directory. Often 2.
		uint16_t sectorFSinfo;         // the sector number of the FSInfo structure
		uint16_t sectorBackupBootsect; // the sector number of the backup boot sector
		char     reserved[12];         // reserved. Should be set to zero on format.

		uint8_t  driveNumber; // drive number, i.e., 0x00 for floppy and 0x80 for harddisk. Useless.
		uint8_t  ntFlags;     // flags in Windows NT, reserved and zero.
		uint8_t  signature;   // signature, must be 0x28 or 0x29.
		uint32_t volumeID;    // volume ID (partition UUID)
		char     label[11];   // label, padded with spaces
		char     type[8];     // system identifier string, always "FAT32   " with 3 spaces. Not to be trusted.
	} PACKED;
	static_assert(sizeof(ExtBootRecord_32) == 90 - 36, "ExtBootRecord_36 not packed");


	static constexpr int FSInfo_Offset = 484;
	// FSInfo structure for FAT32, starting at byte offset 484
	struct FSInfo {
		uint32_t signatureMid;      // = 0x61417272
		uint32_t freeClusterCount;  // last known free cluster count, might be incorrect. 0xFFFFFFFF means the value is unknown and must be computed.
		uint32_t freeClusterOffset; // the cluster number at which the driver should start looking for free clusters. 0xFFFFFFFF means there is no hint and the driver should start at 2. Also might be incorrect.
		char     reserved[12];      // reserved
		uint32_t signatureTrail;    // = 0xAA550000
	} PACKED;
	static_assert(sizeof(FSInfo) == 512 - 484, "FSInfo_off484 not packed");

	static constexpr uint32_t
		FSInfo_SignatureLead  = 0x41615252, // Lead signature at the beginning of the FSInfo sector
		FSInto_SignatureMid   = 0x61417272, // Signature at offset 0x1e4
		FSInfo_SignatureTrail = 0xaa550000; // Trail signature at the end of the sector


	// Time in a Directory Entry
	struct DirEntry_Time {
		uint16_t hours : 5;        // Hours in wall time.
		uint16_t minutes : 6;      // Minutes in wall time.
		uint16_t seconds_div2 : 5; // Seconds in wall time divided by 2 (sec = div2*2 + 10th/100)
	};

	// Date in a Directory Entry
	struct DirEntry_Date {
		uint16_t year_1980 : 7; // Calendar year, offset since 1980
		uint16_t month : 4;     // Calendar month, 1-12
		uint16_t day : 5;       // Calendar day, 1-31
	};

	// Directory Entry
	struct DirEntry {
		char          name[8], ext[3];         // File name and extension (standard 8.3 format)
		uint8_t       flags;                   // Flags of the file.
		char          reserved;                // Reserved by Windows NT.
		uint8_t       createTime_Seconds_10th; // Tenths of a second, range 0-199 inclusive.
		DirEntry_Time createTime;              // Create time
		DirEntry_Date createDate;              // Create time
		DirEntry_Date lastAccessDate;          // Last access date. there are no last-access time.
		uint16_t      firstCluster_high;       // High 16 bits of the first cluster number.
		DirEntry_Time modifyTime;              // Last modify time
		DirEntry_Date modifyDate;              // Last modify date
		uint16_t      firstCluster_low;        // Low 16 bits of the first cluster number.
		uint32_t      size;                    // Size of the file in bytes
	} PACKED;
	static_assert(sizeof(DirEntry) == 32, "DirEntry not packed");

	static constexpr uint8_t
		DirEntry_Flags_ReadOnly              = 0x01, // Read-Only
		DirEntry_Flags_ReadOnly_Hidden       = 0x02, // Hidden
		DirEntry_Flags_ReadOnly_System       = 0x04, // System
		DirEntry_Flags_ReadOnly_VolumeID     = 0x08, // A Volume-ID entry, only in the root folder
		DirEntry_Flags_ReadOnly_Directory    = 0x10, // Directory
		DirEntry_Flags_ReadOnly_Archive      = 0x20, // Archive
		DirEntry_Flags_ReadOnly_LongFileName = 0x0f; // Part of a Long File Name entry

public:
	~FAT();

	// AllocateBlock allocates a new FAT driver from a block device.
	//
	// It only supports 512-byte-block devices.
	virtual Filesystem *AllocateBlock(BlockDevice *block, Config *config) override;

public:
	// Opendir opens a new directory.
	//
	// It places the cluster number of the directory into file->handle.
	virtual int Opendir(const char *path, OpenFile *file) override;

	// Readdir reads a whole directory in one go, calling the callback on each element.
	virtual int Readdir(const char *path, void *user, Readdir_Callback callback, OpenFile *file) override;

	// Closedir closes a open directory.
	//
	// Currently it does nothing.
	virtual int Closedir(const char *path, OpenFile *file) override;

private:
	// returns the next cluster number from this one, or 0 if end-of-chain
	int __NextCluster(int cluster);

	// reads a directory from a cluster number
	virtual int __Readdir(int cluster, void *user, Readdir_Callback callback);

private:
	FATType type;

	// A copy of the BPB and EBR
	BIOSParamBlock *bpb;
	union {
		ExtBootRecord_12_16 *ebr;
		ExtBootRecord_32 *   ebr32;
	};

	// A copy of the entire FAT
	// TODO change this! this takes about 1/128 of the disk size in memory (in FAT32)
	void *fat_table;
	void *sector; // A buffer at the size of a sector. Allocated on creation

	int numSectors; // Number of sectors in the entire partition.
};


} // namespace filesystem
} // namespace helos