#define _LARGEFILE_SOURCE
#define _LARGEFILE64_SOURCE
#define _GNU_SOURCE
#include <errno.h>

#include "ibm.h"
#include "hdd_file.h"
#include "minivhd/minivhd.h"
#include "minivhd/minivhd_util.h"

void hdd_load_ext(hdd_file_t *hdd, const char *fn, int spt, int hpc, int tracks, int read_only) {
        if (hdd->f == NULL) {
                /* Try to open existing hard disk image */
                if (read_only)
                        hdd->f = (void *)fopen64(fn, "rb");
                else
                        hdd->f = (void *)fopen64(fn, "rb+");
                if (hdd->f != NULL) {
                        hdd->img_type = HDD_IMG_RAW;
                        /* Check if the file we opened is a VHD */
                        if (mvhd_file_is_vhd((FILE *)hdd->f)) {
                                int err;
                                fclose((FILE *)hdd->f);
                                MVHDMeta *vhdm = mvhd_open(fn, (bool)read_only, &err);
                                if (vhdm == NULL) {
                                        hdd->f = NULL;
                                        if (err == MVHD_ERR_FILE) {
                                                pclog("Cannot open VHD file '%s' : %s", fn, strerror(mvhd_errno));
                                        } else {
                                                pclog("Cannot open VHD file '%s' : %s", fn, mvhd_strerr(err));
                                        }
                                        return;
                                }
                                /*else if (err == MVHD_ERR_TIMESTAMP)
                                {
                                        pclog("VHD: Parent/child timestamp mismatch");
                                        mvhd_close(vhdm);
                                        hdd->f = NULL;
                                        return;
                                }*/ // FIX: This is to see if this resolves Issue #59
                                hdd->f = (void *)vhdm;
                                hdd->img_type = HDD_IMG_VHD;
                        }
                } else {
                        /* Failed to open existing hard disk image */
                        if (errno == ENOENT && !read_only) {
                                /* Failed because it does not exist,
                                   so try to create new file */
                                hdd->f = (void *)fopen64(fn, "wb+");
                                if (hdd->f == NULL) {
                                        pclog("Cannot create file '%s': %s", fn, strerror(errno));
                                        return;
                                }
                        } else {
                                /* Failed for another reason */
                                pclog("Cannot open file '%s': %s", fn, strerror(errno));
                                return;
                        }
                        hdd->img_type = HDD_IMG_RAW;
                }
        }
        if (hdd->img_type == HDD_IMG_VHD) {
                MVHDMeta *vhdm = (MVHDMeta *)hdd->f;
                MVHDGeom geom = mvhd_get_geometry(vhdm);
                hdd->spt = geom.spt;
                hdd->hpc = geom.heads;
                hdd->tracks = geom.cyl;
        } else if (hdd->img_type == HDD_IMG_RAW) {
                hdd->spt = spt;
                hdd->hpc = hpc;
                hdd->tracks = tracks;
        }
        hdd->sectors = hdd->spt * hdd->hpc * hdd->tracks;
        hdd->read_only = read_only;
}

void hdd_load(hdd_file_t *hdd, int d, const char *fn) { hdd_load_ext(hdd, fn, hdc[d].spt, hdc[d].hpc, hdc[d].tracks, 0); }

void hdd_close(hdd_file_t *hdd) {
        if (hdd->f) {
                if (hdd->img_type == HDD_IMG_VHD)
                        mvhd_close((MVHDMeta *)hdd->f);
                else if (hdd->img_type == HDD_IMG_RAW)
                        fclose((FILE *)hdd->f);
        }
        hdd->img_type = HDD_IMG_RAW;
        hdd->f = NULL;
}

int hdd_read_sectors(hdd_file_t *hdd, int offset, int nr_sectors, void *buffer) {
        if (hdd->img_type == HDD_IMG_VHD) {
                return mvhd_read_sectors((MVHDMeta *)hdd->f, offset, nr_sectors, buffer);
        } else if (hdd->img_type == HDD_IMG_RAW) {
                off64_t addr;
                int transfer_sectors = nr_sectors;

                if ((hdd->sectors - offset) < transfer_sectors)
                        transfer_sectors = hdd->sectors - offset;
                addr = (uint64_t)offset * 512;

                fseeko64((FILE *)hdd->f, addr, SEEK_SET);
                fread(buffer, transfer_sectors * 512, 1, (FILE *)hdd->f);

                if (nr_sectors != transfer_sectors)
                        return 1;
                return 0;
        }
        /* Keep the compiler happy */
        return 1;
}

int hdd_write_sectors(hdd_file_t *hdd, int offset, int nr_sectors, void *buffer) {
        if (hdd->img_type == HDD_IMG_VHD) {
                return mvhd_write_sectors((MVHDMeta *)hdd->f, offset, nr_sectors, buffer);
        } else if (hdd->img_type == HDD_IMG_RAW) {
                off64_t addr;
                int transfer_sectors = nr_sectors;

                if (hdd->read_only)
                        return 1;

                if ((hdd->sectors - offset) < transfer_sectors)
                        transfer_sectors = hdd->sectors - offset;
                addr = (uint64_t)offset * 512;

                fseeko64((FILE *)hdd->f, addr, SEEK_SET);
                fwrite(buffer, transfer_sectors * 512, 1, (FILE *)hdd->f);

                if (nr_sectors != transfer_sectors)
                        return 1;
                return 0;
        }
        /* Keep the compiler happy */
        return 1;
}

int hdd_format_sectors(hdd_file_t *hdd, int offset, int nr_sectors) {
        if (hdd->img_type == HDD_IMG_VHD) {
                return mvhd_format_sectors((MVHDMeta *)hdd->f, offset, nr_sectors);
        } else if (hdd->img_type == HDD_IMG_RAW) {
                off64_t addr;
                int c;
                uint8_t zero_buffer[512];
                int transfer_sectors = nr_sectors;

                if (hdd->read_only)
                        return 1;

                memset(zero_buffer, 0, 512);

                if ((hdd->sectors - offset) < transfer_sectors)
                        transfer_sectors = hdd->sectors - offset;
                addr = (uint64_t)offset * 512;
                fseeko64((FILE *)hdd->f, addr, SEEK_SET);
                for (c = 0; c < transfer_sectors; c++)
                        fwrite(zero_buffer, 512, 1, (FILE *)hdd->f);

                if (nr_sectors != transfer_sectors)
                        return 1;
                return 0;
        }
        /* Keep the compiler happy */
        return 1;
}
