/*
 *  Copyright (C) 2002-2021  The DOSBox Team
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#ifndef DOSBOX_BIOS_DISK_H
#define DOSBOX_BIOS_DISK_H

#include "dos_inc.h"
#include "logging.h"
#include "../src/dos/cdrom.h"

/* The Section handling Bios Disk Access */
#define BIOS_MAX_DISK 10

#define MAX_SWAPPABLE_DISKS 20
struct diskGeo {
	uint32_t ksize;  /* Size in kilobytes */
	uint16_t secttrack; /* Sectors per track */
	uint16_t headscyl;  /* Heads per cylinder */
	uint16_t cylcount;  /* Cylinders per side */
	uint16_t biosval;   /* Type to return from BIOS */
    uint16_t bytespersect; /* Bytes per sector */
	uint16_t rootentries;  /* Root directory entries */
	uint8_t sectcluster;   /* Sectors per cluster */
	uint8_t mediaid;       /* Media ID */
};
extern diskGeo DiskGeometryList[];

extern const uint8_t freedos_mbr[];

class imageDisk {
	public:
		enum IMAGE_TYPE {
			ID_BASE=0,
			ID_EL_TORITO_FLOPPY,
			ID_VFD,
			ID_MEMORY,
			ID_VHD,
			ID_D88,
			ID_NFD
		};

		virtual uint8_t Read_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,void * data,unsigned int req_sector_size=0);
		virtual uint8_t Write_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,const void * data,unsigned int req_sector_size=0);
		virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
		virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);

		virtual void Set_Reserved_Cylinders(Bitu resCyl);
		virtual uint32_t Get_Reserved_Cylinders();
		virtual void Set_Geometry(uint32_t setHeads, uint32_t setCyl, uint32_t setSect, uint32_t setSectSize);
		virtual void Get_Geometry(uint32_t * getHeads, uint32_t *getCyl, uint32_t *getSect, uint32_t *getSectSize);
		virtual uint8_t GetBiosType(void);
		virtual uint32_t getSectSize(void);
		imageDisk(class DOS_Drive *useDrive, unsigned int letter, uint32_t freeMB, int timeout);
		imageDisk(FILE *imgFile, const char *imgName, uint32_t imgSizeK, bool isHardDisk);
		imageDisk(FILE* diskimg, const char* diskName, uint32_t cylinders, uint32_t heads, uint32_t sectors, uint32_t sector_size, bool hardDrive);
		virtual ~imageDisk();
		void Set_GeometryForHardDisk();
		struct fatFromDOSDrive* ffdd = NULL;
		unsigned int drvnum = DOS_DRIVES;

		IMAGE_TYPE class_id = ID_BASE;
		std::string diskname;
		bool active = false;
		uint32_t sector_size = 512;
		uint32_t heads = 0;
		uint32_t cylinders = 0;
		uint32_t sectors = 0;
		bool hardDrive = false;
		uint64_t diskSizeK = 0;
		FILE* diskimg = NULL;

	protected:
		imageDisk(IMAGE_TYPE class_id);
		uint8_t floppytype = 0;

		uint32_t reserved_cylinders = 0;
		uint64_t image_base = 0;
		uint64_t image_length = 0;

	private:
		volatile int refcount = 0;
		std::vector<bool> partition_in_use; /* used by FAT driver to prevent mounting a partition twice */
		uint64_t current_fpos = 0;

	public:
		int Addref() {
			return ++refcount;
		}
		int Release() {
			int ret = --refcount;
			if (ret < 0) {
				fprintf(stderr,"WARNING: imageDisk Release() changed refcount to %d\n",ret);
				abort();
			}
			if (ret == 0) delete this;
			return ret;
		}
		bool partitionInUse(const size_t i) {
			if (i < partition_in_use.size())
				return partition_in_use[i];

			return false;
		}
		bool partitionMarkUse(const size_t i,bool inUse) {
			if (i < 256) {
				if (partition_in_use.size() != 256) {
					partition_in_use.resize(256); // keep it simple, this is a bitfield, 256 bits = 32 bytes
					for (size_t i=0;i < partition_in_use.size();i++) partition_in_use[i] = false;
				}

				partition_in_use[i] = inUse;
			}

			return false;
		}
};

class imageDiskD88 : public imageDisk {
public:
	virtual uint8_t Read_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,void * data,unsigned int req_sector_size=0);
	virtual uint8_t Write_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,const void * data,unsigned int req_sector_size=0);
	virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
	virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);

	imageDiskD88(FILE *imgFile, const char *imgName, uint32_t imgSizeK, bool isHardDisk);
	virtual ~imageDiskD88();

    unsigned char fd_type_major;
    unsigned char fd_type_minor;

    enum { /* major */
        DISKTYPE_2D			= 0,
        DISKTYPE_2DD,
        DISKTYPE_2HD
    };

    struct vfdentry {
        uint8_t         track = 0,head = 0,sector = 0;
        uint16_t        sector_size = 0;

        uint32_t        data_offset = 0;
        uint32_t        entry_offset = 0; // offset of the 12-byte entry this came from (if nonzero)

        vfdentry() {}

        uint16_t getSectorSize(void) const {
            return sector_size;
        }
    };

    vfdentry *findSector(uint8_t head,uint8_t track,uint8_t sector/*TODO: physical head?*/,unsigned int req_sector_size=0);

    std::vector<vfdentry> dents;
};

class imageDiskNFD : public imageDisk {
public:
	virtual uint8_t Read_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,void * data,unsigned int req_sector_size=0);
	virtual uint8_t Write_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,const void * data,unsigned int req_sector_size=0);
	virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
	virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);

	imageDiskNFD(FILE *imgFile, const char *imgName, uint32_t imgSizeK, bool isHardDisk, unsigned int revision);
	virtual ~imageDiskNFD();

    struct vfdentry {
        uint8_t         track = 0,head = 0,sector = 0;
        uint16_t        sector_size = 0;

        uint32_t        data_offset = 0;
        uint32_t        entry_offset = 0; // offset of the 12-byte entry this came from (if nonzero)

        vfdentry() {}

        uint16_t getSectorSize(void) const {
            return sector_size;
        }
    };

    vfdentry *findSector(uint8_t head,uint8_t track,uint8_t sector/*TODO: physical head?*/,unsigned int req_sector_size=0);

    std::vector<vfdentry> dents;
};

class imageDiskVFD : public imageDisk {
public:
	virtual uint8_t Read_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,void * data,unsigned int req_sector_size=0);
	virtual uint8_t Write_Sector(uint32_t head,uint32_t cylinder,uint32_t sector,const void * data,unsigned int req_sector_size=0);
	virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
	virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);

	imageDiskVFD(FILE *imgFile, const char *imgName, uint32_t imgSizeK, bool isHardDisk);
	virtual ~imageDiskVFD();

    struct vfdentry {
        uint8_t         track = 0,head = 0,sector = 0,sizebyte = 0;
        uint8_t         fillbyte = 0xFF;

        uint32_t        data_offset = 0;
        uint32_t        entry_offset = 0; // offset of the 12-byte entry this came from (if nonzero)

        vfdentry() {}

        bool hasSectorData(void) const {
            return fillbyte == 0xFF && data_offset != 0xFFFFFFFFUL;
        }

        bool hasFill(void) const {
            return fillbyte != 0xFF || data_offset == 0xFFFFFFFFUL;
        }

        uint16_t getSectorSize(void) const {
            return 128 << sizebyte;
        }
    };

    vfdentry *findSector(uint8_t head,uint8_t track,uint8_t sector/*TODO: physical head?*/,unsigned int req_sector_size=0);

    std::vector<vfdentry> dents;
};

class imageDiskMemory : public imageDisk {
public:
	virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
	virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);
	virtual uint8_t GetBiosType(void);
	virtual void Set_Geometry(uint32_t setHeads, uint32_t setCyl, uint32_t setSect, uint32_t setSectSize);
	// Parition and format the ramdrive
	virtual uint8_t Format();

	// Create a hard drive image of a specified size; automatically select c/h/s
	imageDiskMemory(uint32_t imgSizeK);
	// Create a hard drive image of a specified geometry
	imageDiskMemory(uint16_t cylinders, uint16_t heads, uint16_t sectors, uint16_t sector_size);
	// Create a floppy image of a specified geometry
    imageDiskMemory(const diskGeo& floppyGeometry);
	// Create a copy-on-write memory image of an existing image
	imageDiskMemory(imageDisk* underlyingImage);
	virtual ~imageDiskMemory();

private:
	void init(diskGeo diskParams, bool isHardDrive, imageDisk* underlyingImage);
	bool CalculateFAT(uint32_t partitionStartingSector, uint32_t partitionLength, bool isHardDrive, uint32_t rootEntries, uint32_t* rootSectors, uint32_t* sectorsPerCluster, bool* isFat16, uint32_t* fatSectors, uint32_t* reservedSectors);

	uint8_t * * ChunkMap;
	uint32_t sectors_per_chunk;
	uint32_t chunk_size;
	uint32_t total_chunks;
    uint32_t total_sectors = 0;
    imageDisk* underlyingImage = NULL;

	diskGeo floppyInfo;
};

class imageDiskVHD : public imageDisk {
public:
	enum ErrorCodes : int
	{
		OPEN_SUCCESS = 0,
		ERROR_OPENING = 1,
		INVALID_DATA = 2,
		UNSUPPORTED_TYPE = 3,
		INVALID_MATCH = 4,
		INVALID_DATE = 5,
		UNSUPPORTED_WRITE = 6,
        UNSUPPORTED_SIZE = 7,
        ERROR_WRITING = 8,
		PARENT_ERROR = 0x10,
		ERROR_OPENING_PARENT = 0x11,
		PARENT_INVALID_DATA = 0x12,
		PARENT_UNSUPPORTED_TYPE = 0x13,
		PARENT_INVALID_MATCH = 0x14,
		PARENT_INVALID_DATE = 0x15
	};
	enum VHDTypes : uint32_t
	{
		VHD_TYPE_NONE = 0,
		VHD_TYPE_FIXED = 2,
		VHD_TYPE_DYNAMIC = 3,
		VHD_TYPE_DIFFERENCING = 4
	};
    struct Geometry {
        uint16_t cylinders;
        uint8_t heads;
        uint8_t sectors;
    };
    struct VHDFooter {
        char cookie[8];
        uint32_t features;
        uint32_t fileFormatVersion;
        uint64_t dataOffset;
        uint32_t timeStamp;
        char creatorApp[4];
        uint32_t creatorVersion;
        uint32_t creatorHostOS;
        uint64_t originalSize;
        uint64_t currentSize;
        Geometry geometry;
        VHDTypes diskType;
        uint32_t checksum;
        char uniqueId[16];
        char savedState;
        char reserved[427];

        void SwapByteOrder();
        uint32_t CalculateChecksum();
        bool IsValid();
        void SetDefaults();
    };
    typedef struct _VHDInfo {
        uint32_t allocatedBlocks;
        uint32_t totalBlocks;
        uint32_t blockSize;
        float vhdSizeMB;
        uint32_t vhdType;
        _VHDInfo *parentInfo = NULL;
        std::string diskname;
    } VHDInfo;
    VHDTypes vhdType = VHD_TYPE_NONE;
	virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data);
	virtual uint8_t Write_AbsoluteSector(uint32_t sectnum, const void * data);
	static ErrorCodes Open(const char* fileName, const bool readOnly, imageDisk** disk);
	static VHDTypes GetVHDType(const char* fileName);
	VHDTypes GetVHDType(void) const;
    static uint32_t GetInfo(const char* filename, VHDInfo** info);
    uint32_t GetInfo(VHDInfo* info);
    static uint32_t CreateFixed(const char* filename, uint64_t size); //dummy
    static uint32_t ConvertFixed(const char* filename);
    static uint32_t CreateDynamic(const char* filename, uint64_t size);
    static uint32_t CreateDifferencing(const char* filename, const char* basename);
    uint32_t CreateSnapshot();
    bool MergeSnapshot(uint32_t* totalSectorsMerged, uint32_t* totalBlocksUpdated);
    static void SizeToCHS(uint64_t size, uint16_t* c, uint8_t* h, uint8_t* s);
    bool UpdateUUID();
    static void mk_uuid(uint8_t* buf);
    virtual ~imageDiskVHD();

private:
	struct ParentLocatorEntry {
		uint32_t platformCode;
		uint32_t platformDataSpace;
		uint32_t platformDataLength;
		uint32_t reserved;
		uint64_t platformDataOffset;
	};
	struct DynamicHeader {
		char cookie[8];
		uint64_t dataOffset;
		uint64_t tableOffset;
		uint32_t headerVersion;
		uint32_t maxTableEntries;
		uint32_t blockSize;
		uint32_t checksum;
		uint8_t parentUniqueId[16];
		uint32_t parentTimeStamp;
		uint32_t reserved;
		uint16_t parentUnicodeName[256];
		ParentLocatorEntry parentLocatorEntry[8];
		char reserved2[256];

		void SwapByteOrder();
		uint32_t CalculateChecksum();
		bool IsValid();
        void SetDefaults();
    };

	imageDiskVHD() : imageDisk(ID_VHD) { }
    static ErrorCodes TryOpenParent(const char* childFileName, const ParentLocatorEntry& entry, const uint8_t* data, const uint32_t dataLength, imageDisk** disk, const uint8_t* uniqueId);
	static ErrorCodes Open(const char* fileName, const bool readOnly, imageDisk** disk, const uint8_t* matchUniqueId);
	virtual bool loadBlock(const uint32_t blockNumber);
	static bool convert_UTF16_for_fopen(std::string &string, const void* data, const uint32_t dataLength);
    bool is_zeroed_sector(const void* data);

    imageDisk* parentDisk = NULL;
    imageDisk* fixedDisk = NULL;
	uint64_t footerPosition = 0;
    VHDFooter footer = {};
    VHDFooter originalFooter = {};
    bool copiedFooter = false;
    DynamicHeader dynamicHeader = {};
	uint32_t sectorsPerBlock = 0;
	uint32_t blockMapSectors = 0;
	uint32_t blockMapSize = 0;
	uint32_t currentBlock = 0xFFFFFFFF;
    bool currentBlockAllocated = false;
	uint32_t currentBlockSectorOffset = 0;
	uint8_t* currentBlockDirtyMap = 0;
};

/* C++ class implementing El Torito floppy emulation */
class imageDiskElToritoFloppy : public imageDisk {
public:
    /* Read_Sector and Write_Sector take care of geometry translation for us,
     * then call the absolute versions. So, we override the absolute versions only */
    virtual uint8_t Read_AbsoluteSector(uint32_t sectnum, void * data) {
        unsigned char buffer[2048];

        bool GetMSCDEXDrive(unsigned char drive_letter,CDROM_Interface **_cdrom);

        CDROM_Interface *src_drive=NULL;
        if (!GetMSCDEXDrive(CDROM_drive-'A',&src_drive)) return 0x05;

        if (!src_drive->ReadSectorsHost(buffer,false,cdrom_sector_offset+(sectnum>>2)/*512 byte/sector to 2048 byte/sector conversion*/,1))
            return 0x05;

        memcpy(data,buffer+((sectnum&3)*512),512);
        return 0x00;
    }
    virtual uint8_t Write_AbsoluteSector(uint32_t sectnum,const void * data) {
        (void)sectnum;//UNUSED
        (void)data;//UNUSED
        return 0x05; /* fail, read only */
    }
    imageDiskElToritoFloppy(unsigned char new_CDROM_drive,unsigned long new_cdrom_sector_offset,unsigned char floppy_emu_type) : imageDisk((FILE *)NULL,NULL,0,false), CDROM_drive(new_CDROM_drive), cdrom_sector_offset(new_cdrom_sector_offset), floppy_type(floppy_emu_type) {
        diskimg = NULL;
        sector_size = 512;
        class_id = ID_EL_TORITO_FLOPPY;

        if (floppy_emu_type == 1) { /* 1.2MB */
            heads = 2;
            cylinders = 80;
            sectors = 15;
        }
        else if (floppy_emu_type == 2) { /* 1.44MB */
            heads = 2;
            cylinders = 80;
            sectors = 18;
        }
        else if (floppy_emu_type == 3) { /* 2.88MB */
            heads = 2;
            cylinders = 80;
            sectors = 36; /* FIXME: right? */
        }
        else {
            heads = 2;
            cylinders = 69;
            sectors = 14;
            LOG_MSG("BUG! unsupported floppy_emu_type in El Torito floppy object\n");
        }

        diskSizeK = ((uint64_t)heads * cylinders * sectors * sector_size) / 1024;
        active = true;
    }
    virtual ~imageDiskElToritoFloppy() {
    }

    unsigned char CDROM_drive;
    unsigned long cdrom_sector_offset;
    unsigned char floppy_type;
/*
    int class_id;

    bool hardDrive;
    bool active;
    FILE *diskimg;
    std::string diskname;
    uint8_t floppytype;

    uint32_t sector_size;
    uint32_t heads,cylinders,sectors;
    uint32_t reserved_cylinders;
    uint64_t current_fpos; */
};

/* PC-98 IPL1 partition table entry.
 * Taken from GNU Parted source code.
 * Maximum 16 entries. */
#pragma pack(push,1)
struct _PC98RawPartition {
	uint8_t		mid;		/* 0x80 - boot */
	uint8_t		sid;		/* 0x80 - active */
	uint8_t		dum1;		/* dummy for padding */
	uint8_t		dum2;		/* dummy for padding */
	uint8_t		ipl_sect;	/* IPL sector */
	uint8_t		ipl_head;	/* IPL head */
	uint16_t	ipl_cyl;	/* IPL cylinder */
	uint8_t		sector;		/* starting sector */
	uint8_t		head;		/* starting head */
	uint16_t	cyl;		/* starting cylinder */
	uint8_t		end_sector;	/* end sector */
	uint8_t		end_head;	/* end head */
	uint16_t	end_cyl;	/* end cylinder */
	char		name[16];
};

struct partTable {
	uint8_t booter[446];
	struct partentry_t {
		uint8_t bootflag;
		uint8_t beginchs[3];
		uint8_t parttype;
		uint8_t endchs[3];
		uint32_t absSectStart;
		uint32_t partSize;
	} pentry[4];
	uint8_t  magic1; /* 0x55 */
	uint8_t  magic2; /* 0xaa */
#ifndef SECTOR_SIZE_MAX
# pragma warning SECTOR_SIZE_MAX not defined
#endif
#if SECTOR_SIZE_MAX > 512
    uint8_t  extra[SECTOR_SIZE_MAX - 512];
#endif
};
#pragma pack(pop)

void updateDPT(void);
void incrementFDD(void);

//in order to attach to the virtual IDE controllers, the disk must be mounted
//  in the BIOS first (the imageDiskList array), so the IDE controller can obtain
//  a reference to the drive in the imageDiskList array
#define MAX_HDD_IMAGES 4
#define MAX_DISK_IMAGES 6 //MAX_HDD_IMAGES + 2

extern bool imageDiskChange[MAX_DISK_IMAGES];
extern imageDisk *imageDiskList[MAX_DISK_IMAGES];
extern imageDisk *diskSwap[MAX_SWAPPABLE_DISKS];
extern int32_t swapPosition;
extern uint16_t imgDTASeg; /* Real memory location of temporary DTA pointer for fat image disk access */
extern RealPt imgDTAPtr; /* Real memory location of temporary DTA pointer for fat image disk access */
extern DOS_DTA *imgDTA;

void swapInDisks(int drive);
bool getSwapRequest(void);
imageDisk *GetINT13HardDrive(unsigned char drv);
imageDisk *GetINT13FloppyDrive(unsigned char drv);

bool PartitionLoadMBR(std::vector<partTable::partentry_t> &parts,imageDisk *loadedDisk);
bool PartitionLoadIPL1(std::vector<_PC98RawPartition> &parts,imageDisk *loadedDisk);
std::string PartitionIdentifyType(imageDisk *loadedDisk);

void LogPrintPartitionTable(const std::vector<_PC98RawPartition> &parts);
void LogPrintPartitionTable(const std::vector<partTable::partentry_t> &parts);

#endif
