/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

/*
 * https://paradroid.automac.se/diskimage/
 * Copyright (c) 2003-2006, Per Olofsson
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer. Redistributions in
 * binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "common/scummsys.h"
#include "common/str.h"
#include "common/textconsole.h"
#include "glk/scott/disk_image.h"

namespace Glk {
namespace Scott {

/* dos errors as used by the DOS internally (and as saves in the error info) */
struct DosError {
	signed int _number; /* dos error number */
	signed int _errnum; /* reported error number */
	const char *_string;      /* description */
};

DosError g_dosError[] = {
	/* non-errors */
	{0x01, 0, "ok"},
	/* errors */
	{0x02, 20, "Header descriptor byte not found (Seek)"},
	/*  { 0x02, 20, "Header block not found (Seek)" }, */
	{0x03, 21, "No SYNC sequence found (Seek)"},
	{0x04, 22, "Data descriptor byte not found (Read)"},
	{0x05, 23, "Checksum error in data block (Read)"},
	{0x06, 24, "Write verify (Write)"},
	{0x07, 25, "Write verify error (Write)"},
	{0x08, 26, "Write protect on (Write)"},
	{0x09, 27, "Checksum error in header block (Seek)"},
	{0x0A, 28, "Write error (Write)"},
	{0x0B, 29, "Disk sector ID mismatch (Seek)"},
	{0x0F, 74, "Drive Not Ready (Read)"},
	{-1, -1, nullptr}
};

int setStatus(DiskImage *di, int status, int track, int sector) {
	di->_status = status;
	di->_statusts._track = track;
	di->_statusts._sector = sector;
	return status;
}

/* check if given track/sector is within valid range */
int diTsIsValid(ImageType type, TrackSector ts) {
	if ((ts._track < 1) || (ts._track > diTracks(type))) {
		return 0; /* track out of range */
	}
	if (ts._sector > (diSectorsPerTrack(type, ts._track) - 1)) {
		return 0; /* sector out of range */
	}
	return 1;
}

int matchPattern(byte *rawPattern, byte *rawname) {
	int i;

	for (i = 0; i < 16; ++i) {
		if (rawPattern[i] == '*') {
			return 1;
		}
		if (rawname[i] == 0xa0) {
			if (rawPattern[i] == 0xa0) {
				return 1;
			} else {
				return 0;
			}
		} else {
			if (rawPattern[i] == '?' || rawPattern[i] == rawname[i]) {
			} else {
				return 0;
			}
		}
	}
	return 1;
}

/* return a pointer to the next block in the chain */
TrackSector nextTsInChain(DiskImage *di, TrackSector ts) {
	byte *p;
	TrackSector newTs;

	p = diGetTsAddr(di, ts);
	newTs._track = p[0];
	newTs._sector = p[1];

	return newTs;
}

RawDirEntry *findFileEntry(DiskImage *di, byte *rawPattern, int type) {
	byte *buffer;
	TrackSector ts;
	RawDirEntry *rde;
	int offset;

	ts = diGetDirTs(di);

	while (ts._track) {
		buffer = diGetTsAddr(di, ts);
		for (offset = 0; offset < 256; offset += 32) {
			rde = (RawDirEntry *)(buffer + offset);
			if (matchPattern(rawPattern, rde->_rawname)) {
				return rde;
			}
		}
		/* todo: add sanity checking */
		ts = nextTsInChain(di, ts);
	}
	return nullptr;
}

/* allocate next available directory block */
TrackSector allocNextDirTs(DiskImage *di) {
	byte *p;
	int spt;
	TrackSector ts, lastTs;

	if (diTrackBlocksFree(di, di->_bam._track)) {
		lastTs._track = di->_bam._track;
		lastTs._sector = 0;
		if ((di->_type == D64) || (di->_type == D71)) {
			ts._track = 18;
			ts._sector = 1;
		} else {
			ts = nextTsInChain(di, lastTs);
		}
		while (ts._track) {
			lastTs = ts;
			ts = nextTsInChain(di, ts);
		}
		ts._track = lastTs._track;
		ts._sector = lastTs._sector + 3;
		spt = diSectorsPerTrack(di->_type, ts._track);
		for (;; ts._sector = (ts._sector + 1) % spt) {
			if (diIsTsFree(di, ts)) {
				diAllocTs(di, ts);
				p = diGetTsAddr(di, lastTs);
				p[0] = ts._track;
				p[1] = ts._sector;
				p = diGetTsAddr(di, ts);
				memset(p, 0, 256);
				p[1] = 0xff;
				di->_modified = 1;
				return ts;
			}
		}
	} else {
		ts._track = 0;
		ts._sector = 0;
		return ts;
	}
}

RawDirEntry *allocFileEntry(DiskImage *di, byte *rawName, int type) {
	byte *buffer;
	TrackSector ts;
	RawDirEntry *rde;
	int offset;

	/* check if file already exists */
	ts = nextTsInChain(di, di->_bam);
	while (ts._track) {
		buffer = diGetTsAddr(di, ts);
		for (offset = 0; offset < 256; offset += 32) {
			rde = (RawDirEntry *)(buffer + offset);
			if (rde->_type) {
				if (scumm_strnicmp((char *)rawName, (char *)rde->_rawname, 16) == 0) {
					setStatus(di, 63, 0, 0);
					return nullptr;
				}
			}
		}
		ts = nextTsInChain(di, ts);
	}

	/* allocate empty slot */
	ts = nextTsInChain(di, di->_bam);
	while (ts._track) {
		buffer = diGetTsAddr(di, ts);
		for (offset = 0; offset < 256; offset += 32) {
			rde = (RawDirEntry *)(buffer + offset);
			if (rde->_type == 0) {
				memset((byte *)rde + 2, 0, 30);
				memcpy(rde->_rawname, rawName, 16);
				rde->_type = type;
				di->_modified = 1;
				return rde;
			}
		}
		ts = nextTsInChain(di, ts);
	}

	/* allocate new dir block */
	ts = allocNextDirTs(di);
	if (ts._track) {
		rde = (RawDirEntry *)diGetTsAddr(di, ts);
		memset((byte *)rde + 2, 0, 30);
		memcpy(rde->_rawname, rawName, 16);
		rde->_type = type;
		di->_modified = 1;
		return rde;
	} else {
		setStatus(di, 72, 0, 0);
		return nullptr;
	}
}

ImageFile *diOpen(DiskImage *di, byte *rawname, byte type, const char *mode) {
	ImageFile *imgFile;
	RawDirEntry *rde;
	byte *p;

	setStatus(di, 255, 0, 0);

	if (scumm_stricmp("rb", mode) == 0) {

		if ((imgFile = new ImageFile) == nullptr) {
			return nullptr;
		}

		memset(imgFile->_visited, 0, sizeof(imgFile->_visited));

		if (scumm_stricmp("$", (char *)rawname) == 0) {
			imgFile->_mode = 'r';

			imgFile->_ts = di->_dir;

			p = diGetTsAddr(di, di->_dir);
			imgFile->_buffer = p + 2;

			imgFile->_nextts = diGetDirTs(di);

			imgFile->_buflen = 254;

			if (!diTsIsValid(di->_type, imgFile->_nextts)) {
				setStatus(di, 66, imgFile->_nextts._track, imgFile->_nextts._sector);
				delete imgFile;
				return nullptr;
			}
			rde = nullptr;

		} else {
			if ((rde = findFileEntry(di, rawname, type)) == nullptr) {
				setStatus(di, 62, 0, 0);
				delete imgFile;
				return nullptr;
			}
			imgFile->_mode = 'r';
			imgFile->_ts = rde->_startts;

			if (!diTsIsValid(di->_type, imgFile->_ts)) {
				setStatus(di, 66, imgFile->_ts._track, imgFile->_ts._sector);
				delete imgFile;
				return nullptr;
			}

			p = diGetTsAddr(di, rde->_startts);
			imgFile->_buffer = p + 2;
			imgFile->_nextts._track = p[0];
			imgFile->_nextts._sector = p[1];

			if (imgFile->_nextts._track == 0) {
				if (imgFile->_nextts._sector != 0) {
					imgFile->_buflen = imgFile->_nextts._sector - 1;
				} else {
					imgFile->_buflen = 254;
				}
			} else {
				if (!diTsIsValid(di->_type, imgFile->_nextts)) {
					setStatus(di, 66, imgFile->_nextts._track, imgFile->_nextts._sector);
					delete imgFile;
					return nullptr;
				}
				imgFile->_buflen = 254;
			}
		}

	} else if (strcmp("wb", mode) == 0) {

		if ((rde = allocFileEntry(di, rawname, type)) == nullptr) {
			return nullptr;
		}
		if ((imgFile = new ImageFile) == nullptr) {
			return nullptr;
		}
		imgFile->_mode = 'w';
		imgFile->_ts._track = 0;
		imgFile->_ts._sector = 0;
		if ((imgFile->_buffer = new byte[254]) == nullptr) {
			delete imgFile;
			return nullptr;
		}
		imgFile->_buflen = 254;
		di->_modified = 1;

	} else {
		return nullptr;
	}

	imgFile->_diskimage = di;
	imgFile->_rawdirentry = rde;
	imgFile->_position = 0;
	imgFile->_bufptr = 0;

	++(di->_openfiles);
	setStatus(di, 0, 0, 0);
	return imgFile;
}

int diRead(ImageFile *imgFile, byte *buffer, int len) {
	byte *p;
	int bytesLeft;
	int counter = 0;
	int err;

	while (len) {
		bytesLeft = imgFile->_buflen - imgFile->_bufptr;

		err = diGetTsErr(imgFile->_diskimage, imgFile->_ts);
		if (err) {
			setStatus(imgFile->_diskimage, err, imgFile->_ts._track, imgFile->_ts._sector);
			return counter;
		}

		if (bytesLeft == 0) {
			if (imgFile->_nextts._track == 0) {
				return counter;
			}
			if (((imgFile->_diskimage->_type == D64) || (imgFile->_diskimage->_type == D71)) && imgFile->_ts._track == 18 && imgFile->_ts._sector == 0) {
				imgFile->_ts._track = 18;
				imgFile->_ts._sector = 1;
			} else {
				imgFile->_ts = nextTsInChain(imgFile->_diskimage, imgFile->_ts);
			}
			if (imgFile->_ts._track == 0) {
				return counter;
			}

			/* check for cyclic files */
			if (imgFile->_visited[imgFile->_ts._track][imgFile->_ts._sector]) {
				/* return 52, file too long error */
				setStatus(imgFile->_diskimage, 52, imgFile->_ts._track, imgFile->_ts._sector);
			} else {
				imgFile->_visited[imgFile->_ts._track][imgFile->_ts._sector] = 1;
			}

			err = diGetTsErr(imgFile->_diskimage, imgFile->_ts);
			if (err) {
				setStatus(imgFile->_diskimage, err, imgFile->_ts._track, imgFile->_ts._sector);
				return counter;
			}

			p = diGetTsAddr(imgFile->_diskimage, imgFile->_ts);
			imgFile->_buffer = p + 2;
			imgFile->_nextts._track = p[0];
			imgFile->_nextts._sector = p[1];

			if (imgFile->_nextts._track == 0) {
				if (imgFile->_nextts._sector == 0) {
					/* fixme, something is wrong if this happens, should be a proper error */
					imgFile->_buflen = 0;
					setStatus(imgFile->_diskimage, -1, imgFile->_ts._track, imgFile->_ts._sector);
				} else {
					imgFile->_buflen = imgFile->_nextts._sector - 1;
				}
			} else {
				if (!diTsIsValid(imgFile->_diskimage->_type, imgFile->_nextts)) {
					setStatus(imgFile->_diskimage, 66, imgFile->_nextts._track, imgFile->_nextts._sector);
					return counter;
				}
				imgFile->_buflen = 254;
			}
			imgFile->_bufptr = 0;
		} else {
			if (len >= bytesLeft) {
				while (bytesLeft) {
					*buffer++ = imgFile->_buffer[imgFile->_bufptr++];
					--len;
					--bytesLeft;
					++counter;
					++(imgFile->_position);
				}
			} else {
				while (len) {
					*buffer++ = imgFile->_buffer[imgFile->_bufptr++];
					--len;
					--bytesLeft;
					++counter;
					++(imgFile->_position);
				}
			}
		}
	}
	return counter;
}

// get a pointer to block data 
byte *diGetTsAddr(DiskImage *di, TrackSector ts) {
	return di->_image + diGetBlockNum(di->_type, ts) * 256;
}

/* get error info for a sector */
int getTsDosErr(DiskImage *di, TrackSector ts) {
	//	return 1;
	if (di->_errinfo == nullptr) {
		return 1; /* return OK if image has no error info */
	}

	return di->_errinfo[diGetBlockNum(di->_type, ts)];
}

int diGetTsErr(DiskImage *di, TrackSector ts) {
	int errnum;
	DosError *err = g_dosError;

	errnum = getTsDosErr(di, ts);
	while (err->_number >= 0) {
		if (errnum == err->_number) {
			return err->_errnum;
		}
		++err;
	}
	return -1; /* unknown error */
}

/* return disk geometry for track */
int diSectorsPerTrack(ImageType type, int track) {
	switch (type) {
	case D71:
		if (track > 35) {
			track -= 35;
		}
		/* fall through */
	case D64:
		if (track < 18) {
			return 21;
		} else if (track < 25) {
			return 19;
		} else if (track < 31) {
			return 18;
		} else {
			return 17;
		}
		break;
	case D81:
		return 40;
		break;
	}
	return 0;
}

/* return number of tracks for image type */
int diTracks(ImageType type) {
	switch (type) {
	case D64:
		return 35;
		break;
	case D71:
		return 70;
		break;
	case D81:
		return 80;
		break;
	}
	return 0;
}

int diGetBlockNum(ImageType type, TrackSector ts) {
	int block;

	/* assertion, should never happen (indicates bad error handling elsewhere) */
	if (!diTsIsValid(type, ts)) {
		error("diGetBlockNum: internal error, track/sector out of range");
	}

	switch (type) {
	case D64:
		if (ts._track < 18) {
			block = (ts._track - 1) * 21;
		} else if (ts._track < 25) {
 			block = (ts._track - 18) * 19 + 17 * 21;
		} else if (ts._track < 31) {
			block = (ts._track - 25) * 18 + 17 * 21 + 7 * 19;
		} else {
			block = (ts._track - 31) * 17 + 17 * 21 + 7 * 19 + 6 * 18;
		}
        return block + ts._sector;
		break;
	case D71:
		if (ts._track > 35) {
			block = 683;
			ts._track -= 35;
		} else {
			block = 0;
		}
		if (ts._track < 18) {
			block += (ts._track - 1) * 21;
		} else if (ts._track < 25) {
			block += (ts._track - 18) * 19 + 17 * 21;
		} else if (ts._track < 31) {
			block += (ts._track - 25) * 18 + 17 * 21 + 7 * 19;
		} else {
			block += (ts._track - 31) * 17 + 17 * 21 + 7 * 19 + 6 * 18;
		}
		return block + ts._sector;
		break;
	case D81:
		return (ts._track - 1) * 40 + ts._sector;
		break;
	}
	return 0;
}

/* return t/s of first directory sector */
TrackSector diGetDirTs(DiskImage *di) {
	TrackSector newTs;
	byte *p;

	p = diGetTsAddr(di, di->_dir);
	if ((di->_type == D64) || (di->_type == D71)) {
		newTs._track = 18; /* 1541/71 ignores bam t/s link */
		newTs._sector = 1;
	} else {
		newTs._track = p[0];
		newTs._sector = p[1];
	}

	return newTs;
}

/* return number of free blocks in track */
int diTrackBlocksFree(DiskImage *di, int track) {
	byte *bam;

	switch (di->_type) {
	default:
	case D64:
		bam = diGetTsAddr(di, di->_bam);
		break;
	case D71:
		bam = diGetTsAddr(di, di->_bam);
		if (track >= 36) {
			return bam[track + 185];
		}
		break;
	case D81:
		if (track <= 40) {
			bam = diGetTsAddr(di, di->_bam);
		} else {
			bam = diGetTsAddr(di, di->_bam2);
			track -= 40;
		}
		return bam[track * 6 + 10];
		break;
	}
	return bam[track * 4];
}

/* check if track, sector is free in BAM */
int diIsTsFree(DiskImage *di, TrackSector ts) {
	byte mask;
	byte *bam;

	switch (di->_type) {
	case D64:
		bam = diGetTsAddr(di, di->_bam);
		if (bam[ts._track * 4]) {
			mask = 1 << (ts._sector & 7);
			return bam[ts._track * 4 + ts._sector / 8 + 1] & mask ? 1 : 0;
		} else {
			return 0;
		}
		break;
	case D71:
		mask = 1 << (ts._sector & 7);
		if (ts._track < 36) {
			bam = diGetTsAddr(di, di->_bam);
			return bam[ts._track * 4 + ts._sector / 8 + 1] & mask ? 1 : 0;
		} else {
			bam = diGetTsAddr(di, di->_bam2);
			return bam[(ts._track - 35) * 3 + ts._sector / 8 - 3] & mask ? 1 : 0;
		}
		break;
	case D81:
		mask = 1 << (ts._sector & 7);
		if (ts._track < 41) {
			bam = diGetTsAddr(di, di->_bam);
		} else {
			bam = diGetTsAddr(di, di->_bam2);
			ts._track -= 40;
		}
		return bam[ts._track * 6 + ts._sector / 8 + 11] & mask ? 1 : 0;
		break;
	}
	return 0;
}

/* allocate track, sector in BAM */
void diAllocTs(DiskImage *di, TrackSector ts) {
	byte mask;
	byte *bam;

	di->_modified = 1;
	switch (di->_type) {
	case D64:
		bam = diGetTsAddr(di, di->_bam);
		bam[ts._track * 4] -= 1;
		mask = 1 << (ts._sector & 7);
		bam[ts._track * 4 + ts._sector / 8 + 1] &= ~mask;
		break;
	case D71:
		mask = 1 << (ts._sector & 7);
		if (ts._track < 36) {
			bam = diGetTsAddr(di, di->_bam);
			bam[ts._track * 4] -= 1;
			bam[ts._track * 4 + ts._sector / 8 + 1] &= ~mask;
		} else {
			bam = diGetTsAddr(di, di->_bam);
			bam[ts._track + 185] -= 1;
			bam = diGetTsAddr(di, di->_bam2);
			bam[(ts._track - 35) * 3 + ts._sector / 8 - 3] &= ~mask;
		}
		break;
	case D81:
		if (ts._track < 41) {
			bam = diGetTsAddr(di, di->_bam);
		} else {
			bam = diGetTsAddr(di, di->_bam2);
			ts._track -= 40;
		}
		bam[ts._track * 6 + 10] -= 1;
		mask = 1 << (ts._sector & 7);
		bam[ts._track * 6 + ts._sector / 8 + 11] &= ~mask;
		break;
	}
}

/* convert to rawname */
int diRawnameFromName(byte *rawname, const char *name) {
	int i;

	memset(rawname, 0xa0, 16);
	for (i = 0; i < 16 && name[i]; ++i)
		rawname[i] = name[i];
	return i;
}

/* count number of free blocks */
int blocksFree(DiskImage *di) {
	int blocks = 0;

	for (int track = 1; track <= diTracks(di->_type); ++track) {
		if (track != di->_dir._track) {
			blocks += diTrackBlocksFree(di, track);
		}
	}
	return blocks;
}

/* return write interleave */
int interleave(ImageType type) {
	switch (type) {
	case D64:
		return 10;
		break;
	case D71:
		return 6;
		break;
	default:
		return 1;
		break;
	}
}

RawDirEntry *findLargestFileEntry(DiskImage *di) {
	byte *buffer;
	TrackSector ts;
	RawDirEntry *rde, *largest = nullptr;
	int offset, largestSize = 0;

	ts = diGetDirTs(di);

	while (ts._track) {
		buffer = diGetTsAddr(di, ts);
		for (offset = 0; offset < 256; offset += 32) {
			rde = (RawDirEntry *)(buffer + offset);
			int size = rde->_sizelo + rde->_sizehi * 0x100;
			if (size > largestSize) {
				largest = rde;
				largestSize = size;
			}
		}
		/* todo: add sanity checking */
		ts = nextTsInChain(di, ts);
	}
	return largest;
}

DiskImage *diCreateFromData(uint8_t *data, int length) {
	DiskImage *di;

	if (data == nullptr)
		return nullptr;

	if ((di = new DiskImage) == nullptr) {
		return nullptr;
	}

	di->_size = length;
	di->_image = data;

	di->_errinfo = nullptr;

	/* check image type */
	switch (length) {
	case D64ERRSIZE: /* D64 with error info */
		// di->_errinfo = &(di->_error_);
		// fallthrough
	case D64SIZE: /* standard D64 */
		di->_type = D64;
		di->_bam._track = 18;
		di->_bam._sector = 0;
		di->_dir = di->_bam;
		break;

	case D71ERRSIZE: /* D71 with error info */
		di->_errinfo = &(di->_image[D71SIZE]);
		// fallthrough
	case D71SIZE:
		di->_type = D71;
		di->_bam._track = 18;
		di->_bam._sector = 0;
		di->_bam2._track = 53;
		di->_bam2._sector = 0;
		di->_dir = di->_bam;
		break;

	case D81ERRSIZE: /* D81 with error info */
		di->_errinfo = &(di->_image[D81SIZE]);
		// fallthrough
	case D81SIZE:
		di->_type = D81;
		di->_bam._track = 40;
		di->_bam._sector = 1;
		di->_bam2._track = 40;
		di->_bam2._sector = 2;
		di->_dir._track = 40;
		di->_dir._sector = 0;
		break;

	default:
		delete di;
		return nullptr;
	}

	di->_filename = nullptr;
	di->_openfiles = 0;
	di->_blocksfree = blocksFree(di);
	di->_modified = 0;
	di->_interleave = interleave(di->_type);
	setStatus(di, 254, 0, 0);
	return di;
}

} // End of namespace Scott
} // End of namespace Glk
