/**
  * UAE - The Un*x Amiga Emulator
  *
  * Hardfile emulation for *nix systems
  *
  * Copyright 2003-2006 Richard Drummond
  * Copyright 2008-2010 Mustafa TUFAN
  * Based on hardfile_win32.c
  */

#include "sysconfig.h"
#include "sysdeps.h"

#include "filesys.h"
#include "zfile.h"

#define hfd_log write_log

//#define HDF_DEBUG
#ifdef  HDF_DEBUG
#define DEBUG_LOG write_log ( "%s: ", __func__); write_log
#else
#define DEBUG_LOG(...) do ; while(0)
#endif

static int usefloppydrives = 0;

struct hardfilehandle
{
	int zfile;
	struct zfile *zf;
	FILE *h;
};

struct uae_driveinfo {
	char vendor_id[128];
	char product_id[128];
	char product_rev[128];
	char product_serial[128];
	char device_name[2048];
	char device_path[2048];
	uae_u64 size;
	uae_u64 offset;
	int bytespersector;
	int removablemedia;
	int nomedia;
	int dangerous;
	int readonly;
};

#define HDF_HANDLE_WIN32 1
#define HDF_HANDLE_ZFILE 2
#define HDF_HANDLE_LINUX 3
#define INVALID_HANDLE_VALUE NULL

#define CACHE_SIZE 16384
#define CACHE_FLUSH_TIME 5

/* safety check: only accept drives that:
* - contain RDSK in block 0
* - block 0 is zeroed
*/

int harddrive_dangerous, do_rdbdump;
static struct uae_driveinfo uae_drives[MAX_FILESYSTEM_UNITS];

static void rdbdump (FILE *h, uae_u64 offset, uae_u8 *buf, int blocksize)
{
	static int cnt = 1;
	int i, blocks;
	char name[100];
	FILE *f;

	blocks = (buf[132] << 24) | (buf[133] << 16) | (buf[134] << 8) | (buf[135] << 0);
	if (blocks < 0 || blocks > 100000)
		return;
	_stprintf (name, "rdb_dump_%d.rdb", cnt);
	f = fopen (name, "wb");
	if (!f)
		return;
	for (i = 0; i <= blocks; i++) {
		long outlen;
		if (fseek (h, (long)offset, SEEK_SET) != 0)
			break;
	   	outlen = fread (buf, 1, blocksize, h);
		fwrite (buf, 1, blocksize, f);
		offset += blocksize;
	}
	fclose (f);
	cnt++;
}

static int ismounted (int hd)
{
	int mounted;
	//mounted = 1;
	return mounted;
}

#define CA "Commodore\0Amiga\0"
static int safetycheck (FILE *h, const char *name, uae_u64 offset, uae_u8 *buf, int blocksize)
{
	int i, j, blocks = 63, empty = 1;
	long outlen;

	for (j = 0; j < blocks; j++) {
		if (fseek (h, (long)offset, SEEK_SET) != 0) {
			write_log ("hd ignored, SetFilePointer failed, error %d\n", errno);
			return 1;
		}
		memset (buf, 0xaa, blocksize);
	   	outlen = fread (buf, 1, blocksize, h);
		if (outlen != blocksize) {
			write_log ("hd ignored, read error %d!\n", errno);
			return 2;
		}
		if (j == 0 && offset > 0)
			return -5;
		if (j == 0 && buf[0] == 0x39 && buf[1] == 0x10 && buf[2] == 0xd3 && buf[3] == 0x12) {
			// ADIDE "CPRM" hidden block..
			if (do_rdbdump)
				rdbdump (h, offset, buf, blocksize);
			write_log ("hd accepted (adide rdb detected at block %d)\n", j);
			return -3;
		}
		if (!memcmp (buf, "RDSK", 4) || !memcmp (buf, "DRKS", 4)) {
			if (do_rdbdump)
				rdbdump (h, offset, buf, blocksize);
			write_log ("hd accepted (rdb detected at block %d)\n", j);
			return -1;
		}

		if (!memcmp (buf + 2, "CIS@", 4) && !memcmp (buf + 16, CA, strlen (CA))) {
			write_log ("hd accepted (PCMCIA RAM)\n");
			return -2;
		}
		if (j == 0) {
			for (i = 0; i < blocksize; i++) {
				if (buf[i])
					empty = 0;
			}
		}
		offset += blocksize;
	}
	if (!empty) {
		int mounted;
		mounted = ismounted (h);
		if (!mounted) {
			write_log ("hd accepted, not empty and not mounted in Windows\n");
			return -8;
		}
		if (mounted < 0) {
			write_log ("hd ignored, NTFS partitions\n");
			return 0;
		}
		if (harddrive_dangerous == 0x1234dead)
			return -6;
		write_log ("hd ignored, not empty and no RDB detected or Windows mounted\n");
		return 0;
	}
	write_log ("hd accepted (empty)\n");
	return -9;
}


static void trim (TCHAR *s)
{
	while(_tcslen(s) > 0 && s[_tcslen(s) - 1] == ' ')
		s[_tcslen(s) - 1] = 0;
}

static int isharddrive (const TCHAR *name)
{
	int i;

	for (i = 0; i < hdf_getnumharddrives (); i++) {
		if (!_tcscmp (uae_drives[i].device_name, name))
			return i;
	}
	return -1;
}

static TCHAR *hdz[] = { "hdz", "zip", "rar", "7z", NULL };

int hdf_open_target (struct hardfiledata *hfd, const char *pname)
{
	FILE *h = INVALID_HANDLE_VALUE;
	int i;
	struct uae_driveinfo *udi;
	char *name = strdup (pname);

	hfd->flags = 0;
	hfd->drive_empty = 0;
	hdf_close (hfd);
	hfd->cache = (uae_u8*)xmalloc (uae_u8, CACHE_SIZE);
	hfd->cache_valid = 0;
	hfd->virtual_size = 0;
	hfd->virtual_rdb = NULL;
	if (!hfd->cache) {
		write_log ("VirtualAlloc(%d) failed, error %d\n", CACHE_SIZE, errno);
		goto end;
	}
	hfd->handle = xcalloc (struct hardfilehandle, 1);
	hfd->handle->h = INVALID_HANDLE_VALUE;
	hfd_log ("hfd open: '%s'\n", name);
	if (_tcslen (name) > 4 && !_tcsncmp (name,"HD_", 3)) {
		hdf_init_target ();
		i = isharddrive (name);
		if (i >= 0) {
			long r;
			udi = &uae_drives[i];
			hfd->flags = HFD_FLAGS_REALDRIVE;
			if (udi->nomedia)
				hfd->drive_empty = -1;
			if (udi->readonly)
				hfd->readonly = 1;
			h = fopen (udi->device_path, hfd->readonly ? "rb" : "r+b");
			hfd->handle->h = h;
			if (h == INVALID_HANDLE_VALUE)
				goto end;
			_tcsncpy (hfd->vendor_id, udi->vendor_id, 8);
			_tcsncpy (hfd->product_id, udi->product_id, 16);
			_tcsncpy (hfd->product_rev, udi->product_rev, 4);
			hfd->offset = udi->offset;
			hfd->physsize = hfd->virtsize = udi->size;
			hfd->blocksize = udi->bytespersector;
			if (hfd->offset == 0 && !hfd->drive_empty) {
				int sf = safetycheck (hfd->handle->h, udi->device_path, 0, hfd->cache, hfd->blocksize);
				if (sf > 0)
					goto end;
				if (sf == 0 && !hfd->readonly && harddrive_dangerous != 0x1234dead) {
					write_log ("'%s' forced read-only, safetycheck enabled\n", udi->device_path);
					hfd->dangerous = 1;
					// clear GENERIC_WRITE
					fclose (h);
					h = fopen (udi->device_path, "r+b");
					hfd->handle->h = h;
					if (h == INVALID_HANDLE_VALUE)
						goto end;
				}
			}
			hfd->handle_valid = HDF_HANDLE_LINUX;
			hfd->emptyname = strdup (name);
		} else {
			hfd->flags = HFD_FLAGS_REALDRIVE;
			hfd->drive_empty = -1;
			hfd->emptyname = strdup (name);
		}
	} else {
		int zmode = 0;
		char *ext = _tcsrchr (name, '.');
		if (ext != NULL) {
			ext++;
			for (i = 0; hdz[i]; i++) {
				if (!_tcsicmp (ext, hdz[i]))
					zmode = 1;
			}
		}
		h = fopen (name, hfd->readonly ? "rb" : "r+b");
		if (h == INVALID_HANDLE_VALUE)
			goto end;
		hfd->handle->h = h;
		i = _tcslen (name) - 1;
		while (i >= 0) {
			if ((i > 0 && (name[i - 1] == '/' || name[i - 1] == '\\')) || i == 0) {
				_tcscpy (hfd->vendor_id, "UAE");
				_tcsncpy (hfd->product_id, name + i, 15);
				_tcscpy (hfd->product_rev, "0.3");
				break;
			}
			i--;
		}
		if (h != INVALID_HANDLE_VALUE) {
			size_t ret;
			int low;
			ret = fseek (h, 0, SEEK_END);
			if (ret)
				goto end;
			low = ftell (h);
			if (low == -1)
				goto end;
			low &= ~(hfd->blocksize - 1);
			hfd->physsize = hfd->virtsize = low;
			hfd->handle_valid = HDF_HANDLE_LINUX;
			if (hfd->physsize < 64 * 1024 * 1024 && zmode) {
				write_log ("HDF '%s' re-opened in zfile-mode\n", name);
				fclose (h);
				hfd->handle->h = INVALID_HANDLE_VALUE;
				hfd->handle->zf = zfile_fopen(name, hfd->readonly ? "rb" : "r+b", ZFD_NORMAL);
				hfd->handle->zfile = 1;
				if (!h)
					goto end;
				zfile_fseek (hfd->handle->zf, 0, SEEK_END);
				hfd->physsize = hfd->virtsize = zfile_ftell (hfd->handle->zf);
				zfile_fseek (hfd->handle->zf, 0, SEEK_SET);
				hfd->handle_valid = HDF_HANDLE_ZFILE;
			}
		} else {
			write_log ("HDF '%s' failed to open. error = %d\n", name, errno);
		}
	}
	if (hfd->handle_valid || hfd->drive_empty) {
		hfd_log ("HDF '%s' opened, size=%dK mode=%d empty=%d\n",
			name, hfd->physsize / 1024, hfd->handle_valid, hfd->drive_empty);
		return 1;
	}
end:
	hdf_close (hfd);
	xfree (name);
	return 0;
}

static void freehandle (struct hardfilehandle *h)
{
	if (!h)
		return;
	if (!h->zfile && h->h != INVALID_HANDLE_VALUE)
		fclose (h->h);
	if (h->zfile && h->zf)
		zfile_fclose (h->zf);
	h->zf = NULL;
	h->h = INVALID_HANDLE_VALUE;
	h->zfile = 0;
}

void hdf_close_target (struct hardfiledata *hfd)
{
 	//freehandle (hfd->handle);
	xfree (hfd->handle);
	xfree (hfd->emptyname);
	hfd->emptyname = NULL;
	hfd->handle = NULL;
	hfd->handle_valid = 0;
	if (hfd->cache)
		xfree (hfd->cache);
	xfree(hfd->virtual_rdb);
	hfd->virtual_rdb = 0;
	hfd->virtual_size = 0;
	hfd->cache = 0;
	hfd->cache_valid = 0;
	hfd->drive_empty = 0;
	hfd->dangerous = 0;
}

int hdf_dup_target (struct hardfiledata *dhfd, const struct hardfiledata *shfd)
{
	if (!shfd->handle_valid)
		return 0;

    return 0;
}

static int hdf_seek (struct hardfiledata *hfd, uae_u64 offset)
{
    size_t ret;

	if (hfd->handle_valid == 0) {
		gui_message ("hd: hdf handle is not valid. bug.");
		abort();
	}
	if (offset >= hfd->physsize - hfd->virtual_size) {
		gui_message ("hd: tried to seek out of bounds! (0x%llx >= 0x%llx)\n", offset, hfd->physsize);
		abort ();
    }
    offset += hfd->offset;
    if (offset & (hfd->blocksize - 1)) {
		gui_message ("hd: poscheck failed, offset=0x%llx not aligned to blocksize=%d! (0x%llx & 0x%04.4x = 0x%04.4x)\n",
			offset, hfd->blocksize, offset, hfd->blocksize, offset & (hfd->blocksize - 1));
		abort ();
    }
	if (hfd->handle_valid == HDF_HANDLE_LINUX) {
		ret = fseek (hfd->handle->h, (long)offset, SEEK_SET);
		if (ret)
			return -1;
	} else if (hfd->handle_valid == HDF_HANDLE_ZFILE) {
		zfile_fseek (hfd->handle->zf, (long)offset, SEEK_SET);
	}
    return 0;
}

static void poscheck (struct hardfiledata *hfd, int len)
{
	int ret, err;
	uae_u64 pos;

	if (hfd->handle_valid == HDF_HANDLE_LINUX) {
		ret = fseek (hfd->handle->h, 0, SEEK_CUR);
		if (ret) {
			gui_message ("hd: poscheck failed. seek failure, error %d", err);
			abort ();
		}
		pos = ftell (hfd->handle->h);
	} else if (hfd->handle_valid == HDF_HANDLE_ZFILE) {
		pos = zfile_ftell (hfd->handle->zf);
	}
	if (len < 0) {
		gui_message ("hd: poscheck failed, negative length! (%d)", len);
		abort ();
    }
	if (pos < hfd->offset) {
		gui_message ("hd: poscheck failed, offset out of bounds! (0x%llx < 0x%llx)", pos, hfd->offset);
		abort ();
    }
	if (pos >= hfd->offset + hfd->physsize - hfd->virtual_size || pos >= hfd->offset + hfd->physsize + len - hfd->virtual_size) {
		gui_message ("hd: poscheck failed, offset out of bounds! (0x%llx >= 0x%llx, LEN=%d)", pos, hfd->offset + hfd->physsize, len);
		abort ();
    }
    if (pos & (hfd->blocksize - 1)) {
		gui_message ("hd: poscheck failed, offset not aligned to blocksize! (0x%llx & 0x%04.4x = 0x%04.4x\n", pos, hfd->blocksize, pos & hfd->blocksize);
		abort ();
    }
}

static int isincache (struct hardfiledata *hfd, uae_u64 offset, int len)
{
	if (!hfd->cache_valid)
		return -1;
	if (offset >= hfd->cache_offset && offset + len <= hfd->cache_offset + CACHE_SIZE)
		return (int)(offset - hfd->cache_offset);
	return -1;
}

#if 0
void hfd_flush_cache (struct hardfiledata *hfd, int now)
{
	DWORD outlen = 0;
	if (!hfd->cache_needs_flush || !hfd->cache_valid)
		return;
	if (now || time (NULL) > hfd->cache_needs_flush + CACHE_FLUSH_TIME) {
		hdf_log ("flushed %d %d %d\n", now, time(NULL), hfd->cache_needs_flush);
		hdf_seek (hfd, hfd->cache_offset);
		poscheck (hfd, CACHE_SIZE);
		WriteFile (hfd->handle, hfd->cache, CACHE_SIZE, &outlen, NULL);
		hfd->cache_needs_flush = 0;
	}
}
#endif

static int hdf_read_2 (struct hardfiledata *hfd, void *buffer, uae_u64 offset, int len)
{
	long outlen = 0;
	int coffset;

	if (offset == 0)
		hfd->cache_valid = 0;
	coffset = isincache (hfd, offset, len);
	if (coffset >= 0) {
		memcpy (buffer, hfd->cache + coffset, len);
		return len;
	}
	hfd->cache_offset = offset;
	if (offset + CACHE_SIZE > hfd->offset + (hfd->physsize - hfd->virtual_size))
		hfd->cache_offset = hfd->offset + (hfd->physsize - hfd->virtual_size) - CACHE_SIZE;
	hdf_seek (hfd, hfd->cache_offset);
	poscheck (hfd, CACHE_SIZE);
	if (hfd->handle_valid == HDF_HANDLE_LINUX)
	   	outlen = fread (hfd->cache, 1, CACHE_SIZE, hfd->handle->h);
	else if (hfd->handle_valid == HDF_HANDLE_ZFILE)
		outlen = zfile_fread (hfd->cache, 1, CACHE_SIZE, hfd->handle->zf);
	hfd->cache_valid = 0;
	if (outlen != CACHE_SIZE)
		return 0;
	hfd->cache_valid = 1;
	coffset = isincache (hfd, offset, len);
	if (coffset >= 0) {
		memcpy (buffer, hfd->cache + coffset, len);
		return len;
	}
	write_log ("hdf_read: cache bug! offset=0x%llx len=%d\n", offset, len);
	hfd->cache_valid = 0;
	return 0;
}

int hdf_read_target (struct hardfiledata *hfd, void *buffer, uae_u64 offset, int len)
{
    int got = 0;
	uae_u8 *p = (uae_u8*)buffer;

	if (hfd->drive_empty)
		return 0;
	if (offset < hfd->virtual_size) {
		uae_u64 len2 = offset + (unsigned)len <= hfd->virtual_size ? (unsigned)len : hfd->virtual_size - offset;
		if (!hfd->virtual_rdb)
			return 0;
		memcpy (buffer, hfd->virtual_rdb + offset, len2);
		return len2;
	}
	offset -= hfd->virtual_size;
	while (len > 0) {
		unsigned int maxlen;
		size_t ret;
		if (hfd->physsize < CACHE_SIZE) {
		    hfd->cache_valid = 0;
		    hdf_seek (hfd, offset);
		    poscheck (hfd, len);
			if (hfd->handle_valid == HDF_HANDLE_LINUX) {
				ret = fread (hfd->cache, 1, len, hfd->handle->h);
				memcpy (buffer, hfd->cache, ret);
			} else if (hfd->handle_valid == HDF_HANDLE_ZFILE) {
				ret = zfile_fread (buffer, 1, len, hfd->handle->zf);
			}
			maxlen = len;
		} else {
			maxlen = len > CACHE_SIZE ? CACHE_SIZE : len;
			ret = hdf_read_2 (hfd, p, offset, maxlen);
		}
		got += ret;
		if (ret != maxlen)
			return got;
		offset += maxlen;
		p += maxlen;
		len -= maxlen;
	}
	return got;
}

static int hdf_write_2 (struct hardfiledata *hfd, void *buffer, uae_u64 offset, int len)
{
	long outlen = 0;

	if (hfd->readonly)
		return 0;
	if (hfd->dangerous)
		return 0;
	hfd->cache_valid = 0;
	hdf_seek (hfd, offset);
	poscheck (hfd, len);
	memcpy (hfd->cache, buffer, len);
	if (hfd->handle_valid == HDF_HANDLE_LINUX) {
	    outlen = fwrite (hfd->cache, 1, len, hfd->handle->h);
		if (offset == 0) {
			long outlen2;
			uae_u8 *tmp;
			int tmplen = 512;
			tmp = (uae_u8*)xmalloc (uae_u8, tmplen);
			if (tmp) {
				memset (tmp, 0xa1, tmplen);
				hdf_seek (hfd, offset);
				outlen2 = fread (tmp, 1, tmplen, hfd->handle->h);
				if (memcmp (hfd->cache, tmp, tmplen) != 0 || outlen != len)
					gui_message ("Harddrive\n%s\nblock zero write failed!", hfd->device_name);
				xfree (tmp);
			}
		}
	} else if (hfd->handle_valid == HDF_HANDLE_ZFILE) {
		outlen = zfile_fwrite (hfd->cache, 1, len, hfd->handle->zf);
	}
	return outlen;
}
int hdf_write_target (struct hardfiledata *hfd, void *buffer, uae_u64 offset, int len)
{
	int got = 0;
	uae_u8 *p = (uae_u8*)buffer;

	if (hfd->drive_empty)
		return 0;
	if (offset < hfd->virtual_size)
		return len;
	offset -= hfd->virtual_size;
	while (len > 0) {
		int maxlen = len > CACHE_SIZE ? CACHE_SIZE : len;
		int ret = hdf_write_2 (hfd, p, offset, maxlen);
		if (ret < 0)
			return ret;
		got += ret;
		if (ret != maxlen)
			return got;
		offset += maxlen;
		p += maxlen;
		len -= maxlen;
	}
	return got;
}

int hdf_resize_target (struct hardfiledata *hfd, uae_u64 newsize)
{
	int err = 0;

	write_log ("hdf_resize_target: SetEndOfFile() %d\n", err);
	return 0;
}

static int num_drives;

static int hdf_init2 (int force)
{
	int index = 0, index2 = 0, drive;
	uae_u8 *buffer;
	int errormode;
	int dwDriveMask;
	static int done;

	if (done && !force)
		return num_drives;
	done = 1;
	num_drives = 0;
	return num_drives;
}

int hdf_init_target (void)
{
	return hdf_init2 (0);
}

int hdf_getnumharddrives (void)
{
	return num_drives;
}

TCHAR *hdf_getnameharddrive (int index, int flags, int *sectorsize, int *dangerousdrive)
{
	static char name[512];
	char tmp[32];
	uae_u64 size = uae_drives[index].size;
	int nomedia = uae_drives[index].nomedia;
	char *dang = "?";
	char *rw = "RW";

	if (dangerousdrive)
		*dangerousdrive = 0;
	switch (uae_drives[index].dangerous)
	{
	case -5:
		dang = "[PART]";
		break;
	case -6:
		dang = "[MBR]";
		break;
	case -7:
		dang = "[!]";
		break;
	case -8:
		dang = "[UNK]";
		break;
	case -9:
		dang = "[EMPTY]";
		break;
	case -3:
		dang = "(CPRM)";
		break;
	case -2:
		dang = "(SRAM)";
		break;
	case -1:
		dang = "(RDB)";
		break;
	case 0:
		dang = "[OS]";
		if (dangerousdrive)
			*dangerousdrive |= 1;
		break;
	}
	if (nomedia) {
		dang = "[NO MEDIA]";
		if (dangerousdrive)
			*dangerousdrive &= ~1;
	}
	if (uae_drives[index].readonly) {
		rw = "RO";
		if (dangerousdrive && !nomedia)
			*dangerousdrive |= 2;
	}

	if (sectorsize)
		*sectorsize = uae_drives[index].bytespersector;
	if (flags & 1) {
		if (nomedia) {
			_tcscpy (tmp, "N/A");
		} else {
			if (size >= 1024 * 1024 * 1024)
				_stprintf (tmp, "%.1fG", ((double)(uae_u32)(size / (1024 * 1024))) / 1024.0);
			else if (size < 10 * 1024 * 1024)
				_stprintf (tmp, "%dK", size / 1024);
			else
				_stprintf (tmp, "%.1fM", ((double)(uae_u32)(size / (1024))) / 1024.0);
		}
		_stprintf (name, "%10s [%s,%s] %s", dang, tmp, rw, uae_drives[index].device_name + 3);
		return name;
	}
	if (flags & 2)
		return uae_drives[index].device_path;
	return uae_drives[index].device_name;
}