/*

  SjASMPlus Z80 Cross Compiler - modified - SAVECPCSNA extension

  Copyright (c) 2006 Sjoerd Mastijn (original SW)

  This software is provided 'as-is', without any express or implied warranty.
  In no event will the authors be held liable for any damages arising from the
  use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it freely,
  subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not claim
	 that you wrote the original software. If you use this software in a product,
	 an acknowledgment in the product documentation would be appreciated but is
	 not required.

  2. Altered source versions must be plainly marked as such, and must not be
	 misrepresented as being the original software.

  3. This notice may not be removed or altered from any source distribution.

*/

// io_cpc.cpp

#include "sjdefs.h"
#include "io_cpc_ldrs.h"

//
// Amstrad CPC snapshot file saving (SNA)
//

namespace
{
	// report error and close the file
	static int writeError(const std::filesystem::path & fname, FILE*& fileToClose) {
		Error("[SAVECPCSNA] Write error (disk full?)", fname.string().c_str(), IF_FIRST);
		fclose(fileToClose);
		return 0;
	}

	static bool isCPC6128() {
		return strcmp(DeviceID, "AMSTRADCPC464");
	}

	static word getCPCMemoryDepth() {
		return Device->PagesCount * 0x10;
	}
}

static int SaveSNA_CPC(const std::filesystem::path & fname, word start) {
	// for Lua
	if (!DeviceID) {
		Error("SAVECPCSNA only allowed in real device emulation mode (See DEVICE)"); return 0;
	}
	else if (!IsAmstradCPCDevice(DeviceID)) {
		Error("[SAVECPCSNA] Device must be AMSTRADCPC464 or AMSTRADCPC6128."); return 0;
	}

	FILE* ff;
	if (!FOPEN_ISOK(ff, fname, "wb")) {
		Error("[SAVECPCSNA] Error opening file for write", fname.string().c_str());
		return 0;
	}
	AddDeleteOnError(fname);

	// format:  http://cpctech.cpc-live.com/docs/snapshot.html
	constexpr int SNA_HEADER_SIZE = 256;
	const char magic[8] = { 'M', 'V', ' ', '-', ' ', 'S', 'N', 'A' };
	// basic rom initialized pens
	const byte ga_pens[17] = { 0x04, 0x0A, 0x13, 0x0C, 0x0B, 0x14, 0x15, 0x0D, 0x06, 0x1E, 0x1F, 0x07, 0x12, 0x19, 0x04, 0x17, 0x04 };
	// crtc set to standard mode 1 screen @ $C000
	const byte crtc_defaults[18] = { 
		0x3F,		// h total
		0x28,		// h displayed
		0x2E,		// h sync pos
		0x8E,		// h/v sync widths
		0x26,		// v total (height)
		0x00,		// v adjust
		0x19,		// v displayed (height)
		0x1E,		// v sync pos
		0x00,		// interlace & skew
		0x07,		// max raster
		0x00, 0x00, // cursor start
		0x30, 0x00, // display (xxPPSSOO) -> 0xC000 based screen
		0x00, 0x00, // cursor addr
		0x00, 0x00  // light pen
	};
	// psg defaults as initialized by ROM
	const byte psg_defaults[16] = { 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

	// init header
	byte snbuf[SNA_HEADER_SIZE];
	memset(snbuf, 0, SNA_HEADER_SIZE);
	// copy over the magic marker
	memcpy(snbuf, magic, sizeof(magic));
	snbuf[0x10] = 2; // v2 file format

	// v1 format fields
	snbuf[0x1B] = 0; snbuf[0x1C] = 0; // ensure interrupts are disabled
	snbuf[0x21] = 0xF0; snbuf[0x22] = 0xBF; // set the sp to $BFF0
	snbuf[0x23] = start & 0xFF; snbuf[0x24] = start >> 8; // pc set to start addr
	snbuf[0x25] = 1; // im = 1
	// set the pens to the defaults
	memcpy(snbuf + 0x2F, ga_pens, sizeof(ga_pens));
	// multi-config (RMR: 100I ULVM)
	snbuf[0x40] = 0b1000'01'01; // Upper ROM paged out, Lower ROM paged in, Mode 1
	// RAM config (MMR see https://www.grimware.org/doku.php/documentations/devices/gatearray#mmr)
	snbuf[0x41] = 0; // default RAM paging
	// set the crtc registers to the default values
	snbuf[0x42] = 0x0D;	// selected crtc reg
	memcpy(snbuf + 0x43, crtc_defaults, sizeof(crtc_defaults));
	// PPI
	snbuf[0x59] = 0x82;	// PPI control port default
	// Set the PSG registers to sensible defaults
	memcpy(snbuf + 0x5B, psg_defaults, sizeof(psg_defaults));

	word memdepth = getCPCMemoryDepth();
	snbuf[0x6B] = memdepth & 0xFF;
	snbuf[0x6C] = memdepth >> 8;

	// v2 format fields
	snbuf[0x6D] = isCPC6128() ? 2 : 0;	// machine type (0 = 464, 1 = 664, 2 = 6128)

	if (fwrite(snbuf, 1, SNA_HEADER_SIZE, ff) != SNA_HEADER_SIZE) {
		return writeError(fname, ff);
	}

	// Write the pages out in order
	for (int page = 0; page < Device->PagesCount; ++page) {
		if ((aint)fwrite(Device->GetPage(page)->RAM, 1, Device->GetPage(page)->Size, ff) != Device->GetPage(page)->Size) {
			return writeError(fname, ff);
		}
	}

	fclose(ff);
	return 1;
}

void dirSAVECPCSNA() {
	if (pass != LASTPASS) {
		SkipToEol(lp);
		return;
	}
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	int start = StartAddress;
	if (anyComma(lp)) {
		aint val;
		if (ParseExpression(lp, val)) {
			if (0 <= start) Warning("[SAVECPCSNA] Start address was also defined by END, SAVECPCSNA argument used instead");
			if (0 <= val) {
				start = val;
			}
			else {
				Error("[SAVECPCSNA] Negative values are not allowed", bp, SUPPRESS); return;
			}
		}
		else {
			return;
		}
	}
	if (start < 0) {
		Error("[SAVECPCSNA] No start address defined", bp, SUPPRESS); return;
	}

	if (!SaveSNA_CPC(fnaam, start))
		Error("[SAVECPCSNA] Error writing file (Disk full?)", bp, IF_FIRST);
}

//
// Amstrad CPC tape file saving (CDT)
//

enum ECDTHeadlessFormat { AMSTRAD, SPECTRUM };

namespace CDTUtil {

	static constexpr word DefaultPause = 1000;
// 	static constexpr byte BlockTypeReg = 0x0A;			//unused currently, TODO ask Oli about it?
	static constexpr byte BlockTypeHeader = 0x2C;
	static constexpr byte BlockTypeData = 0x16;

	static constexpr byte FileTypeBASIC = (0 << 1);
	static constexpr byte FileTypeBINARY = (1 << 1);
// 	static constexpr byte FileTypeSCREEN = (2 << 1);	//unused currently, TODO ask Oli about it?

	/* CRC polynomial: X^16+X^12+X^5+1 */
	static unsigned int crcupdate(word c, byte b) {
		constexpr unsigned int poly = 4129;
		unsigned int aux = c ^ (b << 8);
		for (aint i = 0; i < 8; i++) {
			if (aux & 0x8000) {
				aux = (aux << 1) ^ poly;
			}
			else {
				aux <<= 1;
			}
		}
		return aux;
	}

	static void writeChunkedData(const std::filesystem::path & fname, const byte* buf, const aint buflen, word pauseAfter, byte sync) {
		constexpr aint chunkLen = 256;

		const aint chunkCount = (buflen + 255) >> 8;
		const aint dataLen = (chunkCount * chunkLen)	// data size (in chunks)
			+ (chunkCount * 2)	// crcs
			+ 5; // sync byte + trailer

		std::unique_ptr<byte[]> chunkedData(new byte[dataLen]);
		byte* wptr = chunkedData.get(); // write ptr
		const byte* rptr = buf; // read ptr
		// build the buffer
		*(wptr++) = sync; // sync pattern

		aint remaining = buflen;
	
		// N chunks of 256 bytes with a 2 byte checksum at the end
		for (aint i = 0; i < chunkCount; ++i) {		
			if (remaining < chunkLen) {
				memcpy(wptr, rptr, remaining);
				memset(wptr + remaining, 0, chunkLen - remaining);
				rptr += remaining;
				remaining = 0;
			}
			else {
				memcpy(wptr, rptr, chunkLen);
				rptr += chunkLen;
				remaining -= chunkLen;
			}

			unsigned int check = 0xFFFF;
			for (aint n = 0; n < chunkLen; ++n) {
				check = crcupdate(check, wptr[n]);
			}

			wptr += chunkLen;
			// append block crc
			check ^= 0xFFFF;
			*(wptr++) = check >> 8;
			*(wptr++) = check & 0xFF;
		}

		// 4 trailer bytes of 0xFF
		const byte trailer[] = { 0xFF, 0xFF, 0xFF, 0xFF };
		memcpy(wptr, trailer, sizeof(trailer));

		// save block
		STZXTurboBlock turbo;
		turbo.PilotPulseLen = 0x091A;
		turbo.FirstSyncLen = 0x048D;
		turbo.SecondSyncLen = 0x048D;
		turbo.ZeroBitLen = 0x048D;
		turbo.OneBitLen = 0x091A;
		turbo.PilotToneLen = 0x1000;
		turbo.LastByteUsedBits = 0x08;
		turbo.PauseAfterMs = pauseAfter;

		TZX_AppendTurboBlock(fname, chunkedData.get(), dataLen, turbo);
	}

	static void writeTapeFile(
		const std::filesystem::path & fname, const std::string & tfname,
		byte fileType, const byte* buf, aint buflen, word memaddr, word startaddr, word pause)
	{
		constexpr aint blocksize = 2048;
		constexpr aint headerlen = 64;

		byte hbuf[headerlen];
		memset(hbuf, 0, headerlen);
		/*
		   0   16  Filename       Name of the file, padded with nulls
		  16    1  Block number   The first block is 1, numbers are consecutive
		  17    1  Last block     A non-zero value means that this is the last block of a file
		  18    1  File type      A value recording  the type of the file
		  19    2  Data length    The number of data bytes in the data record
		  21    2  Data location  Where the data was written from originally
		  23    1  First block    A non-zero value means that this is the first block of a file
		  24    2  Logical length This is the total length of the file in bytes
		  26    2  Entry address  The execution address for machine code programs
		*/
		
		// copy tape file name (16 bytes)
		memcpy(hbuf, tfname.data(), std::min(static_cast<std::string::size_type>(16), tfname.size()));

		// init header
		hbuf[16] = 1; // block 1
		hbuf[18] = fileType;
		hbuf[24] = buflen & 0xFF;	// logical len (size of whole file)
		hbuf[25] = buflen >> 8;
		hbuf[26] = startaddr & 0xFF;
		hbuf[27] = startaddr >> 8; // entry addr

		word memloc = memaddr;
		aint remaining = buflen;
		byte block = 1;
		const byte* rptr = buf;

		// split the file into blocks of up to 2048 bytes, each with a header and a payload
		while (remaining) {
			hbuf[16] = block;	// write block num
			hbuf[21] = memloc & 0xFF; // where's this block going in memory?
			hbuf[22] = memloc >> 8;
			hbuf[23] = block == 1 ? 0xFF : 0x00;	// first block flag

			aint dlen = remaining;
			if (remaining > blocksize) {
				dlen = blocksize;
				hbuf[17] = 0x00;	// more blocks to come
			}
			else {
				hbuf[17] = 0xFF;	// last block
			}

			// write data len (size of block)
			hbuf[19] = dlen & 0xFF;
			hbuf[20] = dlen >> 8;

			writeChunkedData(fname, hbuf, headerlen, 10, BlockTypeHeader); // header
			writeChunkedData(fname, rptr, dlen, pause, BlockTypeData);	// data
			rptr += dlen;
			memloc += dlen;
			remaining -= dlen;
			++block;
		}
	}

	static void writeBASICLoader(const std::filesystem::path & fname, byte screenMode, const byte* palette) {
		constexpr byte mode_values[] = { 0x0E, 0x0F, 0x10 };
		byte border = 0;
		border = *palette;
		// Border is always the first entry in the palette
		++palette;

		// BASIC number encoding format for the palette entries
		byte p[32];
		for (aint i = 0, n = 0; i < 16; ++i, n+=2) {
			p[n] = (palette[i] / 10) + '0';
			p[n+1] = (palette[i] % 10) + '0';
		}

		const word callad = SaveCDT_AmstradCPC464_ORG;
		const word himem = callad - 1; // himem is one byte lower than the program we load

		// BASIC loader to set the screen mode, border color, palette, and then load the asm loader and execute it
		const byte basic[] = {
			// Line format <Line Len (int16)> <line no (int16)> <tokens...> <0x00>
			//
			//		10		MODE		N						:		CLS  :     BORDER		N
			15, 00, 10, 00, 0xAD, 0x20, mode_values[screenMode], 0x01, 0x8A, 0x01, 0x82, 0x20, 0x19, border, 0x00,
			//		20		DATA [16 bytes of int8]
			54, 00, 20, 00, 0x8C, 0x20, 
				p[0], p[1], ',', p[2], p[3], ',', p[4], p[5], ',', p[6], p[7], ',',
				p[8], p[9], ',', p[10], p[11], ',', p[12], p[13], ',', p[14], p[15], ',',
				p[16], p[17], ',', p[18], p[19], ',', p[20], p[21], ',', p[22], p[23], ',',
				p[24], p[25], ',', p[26], p[27], ',', p[28], p[29], ',', p[30], p[31], 0x00,
			//		30		FOR			i						=	  0			  TO		  15
			18, 00,	30, 00, 0x9E, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0xEF, 0x0E, 0x20, 0xEC, 0x20, 0x19, 0x0F, 0x00,
			//		40		READ		v						:	  INK		  	i					  ,           v                       ,                             v
			30, 00, 40, 00, 0xC3, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x01, 0xA2, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0x2C, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x2C, 0x20, 0x0D, 0x00, 0x00, 0xF6, 0x00,
			//		50		NEXT		i
			11, 00,	50, 00, 0xB0, 0x20, 0x0D, 0x00, 0x00, 0xE9, 0x00,
			//		60		MEMORY		&NNNN			  :		LOAD 
			20, 00, 60, 00, 0xAA, 0x20, 0x1C, himem & 0xFF, himem >> 8, 0x01,	0xA8, 0x20, 0x22, '!', 'c', 'o', 'd', 'e', 0x22, 0x00,
			//		70		CALL
			10, 00, 70, 00, 0x83, 0x20, 0x1C, callad & 0xFF, callad >> 8, 0x00,
			// EOF
			00, 00 
		};
		constexpr aint basiclen = sizeof(basic);
		writeTapeFile(fname, "LOADER", FileTypeBASIC, basic, basiclen, 0x0170, 0x0000, DefaultPause);
	}

	static void writeUserProgram(
		const std::filesystem::path & fname, const std::string & tapefname,
		const byte* buf, aint buflen, word baseAddr, word startAddr)
	{
		writeTapeFile(fname, tapefname, FileTypeBINARY, buf, buflen, baseAddr, startAddr, DefaultPause);
	}

	static bool hasScreen() {
		const CDevicePage* page = Device->GetPage(3);
		const byte* ptr = page->RAM;
		for (int i = 0; i < page->Size; ++i) {
			if (*ptr != 0) {
				return true;
			}
			++ptr;
		}
		return false;
	}

	static aint calcRAMStart(const byte* ram, aint ramlen) {
		for (int i = 0; i < ramlen; ++i) {
			if (ram[i]) return i;
		}
		return 0x10000;
	}

	static aint calcRAMLength(const byte* ram, aint ramlen) {
		while (ramlen--) {
			if (ram[ramlen]) return 1 + ramlen;
		}
		return 0x0000;
	}

	static std::unique_ptr<byte[]> getContigRAM(aint startAddr, aint length) {
		assert(0 <= startAddr && 1 <= length && (startAddr+length) <= 0x10000);
		std::unique_ptr<byte[]> data(new byte[length]);
		byte* bptr = data.get();
		// copy the currently mapped device memory into new continuous buffer
		while (0 < length) {
			const int slotId = Device->GetSlotOfA16(startAddr);
			assert(-1 != slotId);
			CDeviceSlot* const S = Device->GetSlot(slotId);
			const aint offset = startAddr - S->Address;
			const aint slotLength = std::min(S->Size - offset, length);
			memcpy(bptr, S->Page->RAM+offset, slotLength);
			bptr += slotLength;
			startAddr += slotLength;
			length -= slotLength;
		}
		assert(0 == length);
		return data;
	}

/* //unused currently, TODO ask Oli about it?
	static constexpr byte basicToHWColor[] = {
		0x54, 0x44, 0x55, 0x5C,	0x58, 0x5D,	0x4C, 0x45,
		0x4D, 0x56,	0x46, 0x57,	0x5E, 0x40,	0x5F, 0x4E,
		0x47, 0x4F,	0x52, 0x42,	0x53, 0x5A,	0x59, 0x5B,
		0x4A, 0x43,	0x4B, 0x41,	0x48, 0x49,	0x50, 0x51,
	};
*/
}

static void createCDTDump464(const std::filesystem::path & fname, aint startAddr, byte screenMode, const byte* palette) {
	byte* ramptr;
	aint ram_size = 0xC000; // 3 x 16K pages (eg: excl screen)
	std::unique_ptr<byte[]> ram(new byte[ram_size]);
	ramptr = ram.get();
	memcpy(ramptr + 0x0000, Device->GetPage(0)->RAM, 0x4000);
	memcpy(ramptr + 0x4000, Device->GetPage(1)->RAM, 0x4000);
	memcpy(ramptr + 0x8000, Device->GetPage(2)->RAM, 0x4000);

	if (screenMode > 2) {
		screenMode = 0xFF; // Turn mode & palette select off
	}
	bool hasScreen = CDTUtil::hasScreen();
	aint ramBase = CDTUtil::calcRAMStart(ramptr, ram_size);
	aint ramEnd = CDTUtil::calcRAMLength(ramptr, ram_size);

	if (0x10000 == ramBase || 0x0000 == ramEnd) {
		Error("[SAVECDT] Could not determine the start and end of the program", nullptr, SUPPRESS); return;
	}

	aint ramUsed = ramEnd - ramBase;

	if (startAddr < 0) {
		startAddr = ramBase;
	}

	// construct the asm loader
	byte loader[SaveCDT_AmstradCPC464_Len];
	memcpy(loader, SaveCDT_AmstradCPC464, SaveCDT_AmstradCPC464_Len);

	// loader settings
	loader[SaveCDT_AmstradCPC464_Settings + 0x0] = hasScreen;
	loader[SaveCDT_AmstradCPC464_Settings + 0x1] = startAddr & 0xFF;
	loader[SaveCDT_AmstradCPC464_Settings + 0x2] = startAddr >> 8;

	loader[SaveCDT_AmstradCPC464_Settings + 0x3] = ramBase & 0xFF;
	loader[SaveCDT_AmstradCPC464_Settings + 0x4] = ramBase >> 8;

	loader[SaveCDT_AmstradCPC464_Settings + 0x5] = ramUsed & 0xFF;
	loader[SaveCDT_AmstradCPC464_Settings + 0x6] = ramUsed >> 8;

	// Create an empty file with a 2s pause to start with
	TZX_CreateEmpty(fname);
	TZX_AppendPauseBlock(fname, 2000);

	// append a CPC basic loader which will run the asm loader
	CDTUtil::writeBASICLoader(fname, screenMode, palette);

	// append the asm loader program
	CDTUtil::writeUserProgram(fname, "CODE", loader, SaveCDT_AmstradCPC464_Len, SaveCDT_AmstradCPC464_ORG, SaveCDT_AmstradCPC464_ORG);

	// append screen if we have one
	if (hasScreen) {
		CDTUtil::writeChunkedData(fname, Device->GetPage(3)->RAM, Device->GetPage(3)->Size, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
	}
	// finally write the main code
	CDTUtil::writeChunkedData(fname, ramptr + ramBase, ramUsed, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
}

static void createCDTDump6128(const std::filesystem::path & fname, aint startAddr, byte screenMode, const byte* palette) {
	byte* ramptr;
	aint ram_size = 0xC000; // 3 x 16K pages (eg: excl screen)
	std::unique_ptr<byte[]> ram(new byte[ram_size]);
	ramptr = ram.get();
	memcpy(ramptr + 0x0000, Device->GetPage(0)->RAM, 0x4000);
	memcpy(ramptr + 0x4000, Device->GetPage(1)->RAM, 0x4000);
	memcpy(ramptr + 0x8000, Device->GetPage(2)->RAM, 0x4000);

	if (screenMode > 2) {
		screenMode = 0xFF; // Turn mode & palette select off
	}
	bool hasScreen = CDTUtil::hasScreen();
	aint ramBase = CDTUtil::calcRAMStart(ramptr, ram_size);
	aint ramEnd = CDTUtil::calcRAMLength(ramptr, ram_size);

	if (0x10000 == ramBase || 0x0000 == ramEnd) {
		Error("[SAVECDT] Could not determine the start and end of the program", nullptr, SUPPRESS); return;
	}

	aint ramUsed = ramEnd - ramBase;

	if (startAddr < 0) {
		startAddr = ramBase;
	}

	// the max possible length for the loader
	constexpr aint max_loader_len = SaveCDT_AmstradCPC6128_Len + (SaveCDT_AmstradCPC6128_PageEntrySize * 4);
	// construct the asm loader
	byte loader[max_loader_len];
	memcpy(loader, SaveCDT_AmstradCPC6128, SaveCDT_AmstradCPC6128_Len);
	aint loader_actual_len = SaveCDT_AmstradCPC6128_Len;

	// loader settings
	loader[SaveCDT_AmstradCPC6128_Settings + 0x0] = hasScreen;
	loader[SaveCDT_AmstradCPC6128_Settings + 0x1] = startAddr & 0xFF;
	loader[SaveCDT_AmstradCPC6128_Settings + 0x2] = startAddr >> 8;

	loader[SaveCDT_AmstradCPC6128_Settings + 0x3] = ramBase & 0xFF;
	loader[SaveCDT_AmstradCPC6128_Settings + 0x4] = ramBase >> 8;

	loader[SaveCDT_AmstradCPC6128_Settings + 0x5] = ramUsed & 0xFF;
	loader[SaveCDT_AmstradCPC6128_Settings + 0x6] = ramUsed >> 8;

	byte* loader_pages = loader + SaveCDT_AmstradCPC6128_Pages;
	byte* loader_entries = loader_pages + 1;

	unsigned char* pages_ram[4];
	aint pages_len[4];
	aint pages_start[4];
	aint count = 0;

	// Ignore the lower 64K at this time!
	for (aint i = 4; i < Device->PagesCount; i++) {
		{
			// Calc start and end of this block
			aint base = CDTUtil::calcRAMStart(Device->GetPage(i)->RAM, 0x4000);
			aint length = CDTUtil::calcRAMLength(Device->GetPage(i)->RAM, 0x4000);

			if (0x10000 == base || 0 == length) {
				continue;
			}

			loader_entries[0] = i;	// configuration (4-7)
			loader_entries[1] = base & 0xFF;
			loader_entries[2] = (base >> 8) & 0xFF;
			loader_entries[3] = length & 0xFF;
			loader_entries[4] = (length >> 8) & 0xFF;

			loader_actual_len += SaveCDT_AmstradCPC6128_PageEntrySize;
			loader_entries += SaveCDT_AmstradCPC6128_PageEntrySize;

			pages_ram[count] = Device->GetPage(i)->RAM;
			pages_start[count] = base;
			pages_len[count] = length;

			++count;
		}
	}

	loader_pages[0] = count;

	// Create an empty file with a 2s pause to start with
	TZX_CreateEmpty(fname);
	TZX_AppendPauseBlock(fname, 2000);

	// append a CPC basic loader which will run the asm loader
	CDTUtil::writeBASICLoader(fname, screenMode, palette);

	// append the asm loader program
	CDTUtil::writeUserProgram(fname, "CODE", loader, loader_actual_len, SaveCDT_AmstradCPC6128_ORG, SaveCDT_AmstradCPC6128_ORG);

	// append screen if we have one
	if (hasScreen) {
		CDTUtil::writeChunkedData(fname, Device->GetPage(3)->RAM, Device->GetPage(3)->Size, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
	}

	// Write each of the pages
	for (aint i = 0; i < count; ++i) {
		CDTUtil::writeChunkedData(fname, pages_ram[i] + pages_start[i], pages_len[i], CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
	}

	// Now drop the rest of the low 64k
	CDTUtil::writeChunkedData(fname, ramptr + ramBase, ramUsed, CDTUtil::DefaultPause, CDTUtil::BlockTypeData);
}

static void SaveCDT_SnapshotWithPalette(const std::filesystem::path & fname,
										aint startAddr, byte screenMode, const byte* palette)
{
	isCPC6128() ?
		createCDTDump6128(fname, startAddr, screenMode, palette) :
		createCDTDump464(fname, startAddr, screenMode, palette);
}

static void SaveCDT_Snapshot(const std::filesystem::path & fname, aint startAddr) {
	// Default mode after loading from BASIC
	constexpr byte mode = 1;
	// Default ROM palette
	constexpr byte palette[] = {
		 1,
		 1, 24, 20,  6, 26,  0,  2,  8,
		10, 12, 14, 16, 18, 22, 24, 16,
	};
	SaveCDT_SnapshotWithPalette(fname, startAddr, mode, palette);
}

static void SaveCDT_BASIC(const std::filesystem::path & fname, const std::string & tfname, aint startAddr, aint length) {
	std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));

	CDTUtil::writeTapeFile(fname, tfname, CDTUtil::FileTypeBASIC, data.get(), length, startAddr, 0x0000, CDTUtil::DefaultPause);
}

static void SaveCDT_Code(const std::filesystem::path & fname, const std::string & tfname, aint startAddr, aint length, aint entryAddr) {
	if (entryAddr < 0) entryAddr = startAddr;

	std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));
	CDTUtil::writeTapeFile(fname, tfname, CDTUtil::FileTypeBINARY, data.get(), length, startAddr, entryAddr, CDTUtil::DefaultPause);
}

static void SaveCDT_Headless(const std::filesystem::path & fname, aint startAddr, aint length, byte sync, ECDTHeadlessFormat format) {
	assert(ECDTHeadlessFormat::AMSTRAD == format || ECDTHeadlessFormat::SPECTRUM == format);
	std::unique_ptr<byte[]> data(CDTUtil::getContigRAM(startAddr, length));

	if (format == ECDTHeadlessFormat::AMSTRAD) CDTUtil::writeChunkedData(fname, data.get(), length, CDTUtil::DefaultPause, sync);
	else TZX_AppendStandardBlock(fname, data.get(), length, CDTUtil::DefaultPause, sync);
}

typedef void (*savecdt_command_t)(const std::filesystem::path &);

// Creates a CDT tape file of a full memory snapshot, with loader
static void dirSAVECDTFull(const std::filesystem::path & cdtname) {
	constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT FULL <cdtname>[,<startaddr>[,<screenmode>[,<border>[,<ink0>...<ink15>]]]]";

	aint args[] = {
		StartAddress,
		0xFF, 0, // mode, border
		0, 0, 0, 0, 0, 0, 0, 0, // palette
		0, 0, 0, 0, 0, 0, 0, 0,
	};

	bool opt[] = {
		false,	// this is used only when comma was parsed after cdtname => not optional then
		true, true,
		true, true, true, true, true, true, true, true,
		true, true, true, true, true, true, true, true,
	};

	if (anyComma(lp) && !getIntArguments<19>(lp, args, opt)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	if (args[1] != 0xFF) {
		byte palette[17];
		for (aint i = 0; i < 17; ++i) {
			palette[i] = args[2 + i];
		}

		SaveCDT_SnapshotWithPalette(cdtname, args[0], args[1], palette);
	}
	else {
		SaveCDT_Snapshot(cdtname, args[0]);
	}
}

static void dirSAVECDTEmpty(const std::filesystem::path & cdtname) {
	// EMPTY <cdtname>
	TZX_CreateEmpty(cdtname);
}

static void dirSAVECDTBasic(const std::filesystem::path & cdtname) {
	constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT BASIC <cdtname>,<name>,<start>,<length>";

	if (!anyComma(lp)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	const std::string tfname = GetDelimitedString(lp);
	if (!anyComma(lp)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	aint args[] = { /*0:start*/ 0, /*1:length*/ 0 };
	bool opt[] = { false, false };
	if (!getIntArguments<2>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
		Error(argerr, lp, SUPPRESS); return;
	}

	SaveCDT_BASIC(cdtname, tfname, args[0], args[1]);
}

static void dirSAVECDTCode(const std::filesystem::path & cdtname) {
	constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT CODE <cdtname>,<name>,<start>,<length>[,<customstartaddress>]";

	if (!anyComma(lp)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	const std::string tfname = GetDelimitedString(lp);
	if (!anyComma(lp)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	aint args[] = { /*0:start*/ 0, /*1:length*/ 0, /*2:customStart*/ -1 };
	bool opt[] = { false, false, true };
	if (!getIntArguments<3>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
		Error(argerr, lp, SUPPRESS); return;
	}

	SaveCDT_Code(cdtname, tfname, args[0], args[1], args[2]);
}

static void dirSAVECDTHeadless(const std::filesystem::path & cdtname) {
	constexpr const char* argerr = "[SAVECDT] Invalid args. SAVECDT HEADLESS <cdtname>,<start>,<length>[,<sync>[,<format>]]";

	if (!anyComma(lp)) {
		Error(argerr, lp, SUPPRESS); return;
	}

	aint args[] = { /*0:start*/ 0, /*1:length*/ 0, /*2:sync*/ CDTUtil::BlockTypeData, /*3:format*/ 0 };
	bool opt[] = { false, false, true, true };
	if (!getIntArguments<4>(lp, args, opt) || args[0] < 0 || args[1] < 1 || 0x10000 <= args[1] || 0x10000 < (args[0]+args[1])) {
		Error(argerr, lp, SUPPRESS); return;
	}

	ECDTHeadlessFormat format;
	switch (args[3]) {
	case 0:
		format = ECDTHeadlessFormat::AMSTRAD;
		break;
	case 1:
		format = ECDTHeadlessFormat::SPECTRUM;
		break;
	default:
		Error("[SAVECDT HEADLESS] invalid format flag. Expected 0 (AMSTRAD) or 1 (SPECTRUM).", NULL, SUPPRESS); return;
	}

	SaveCDT_Headless(cdtname, args[0], args[1], args[2], format);
}

static void cdtParseFnameAndExecuteCmd(savecdt_command_t command_fn) {
	const std::filesystem::path cdtname = GetOutputFileName(lp);
	if (cdtname.has_filename()) command_fn(cdtname);
	else Error("[SAVECDT] CDT file name is empty", bp, SUPPRESS);
}

void dirSAVECDT() {
	if (pass != LASTPASS) {
		SkipToEol(lp);
		return;
	}
	if (!IsAmstradCPCDevice(DeviceID)) {
		Error("[SAVECDT] is allowed only in AMSTRADCPC464 or AMSTRADCPC6128 device mode", NULL, SUPPRESS);
		return;
	}
	SkipBlanks(lp);
	if (cmphstr(lp, "full")) cdtParseFnameAndExecuteCmd(dirSAVECDTFull);
	else if (cmphstr(lp, "empty")) cdtParseFnameAndExecuteCmd(dirSAVECDTEmpty);
	else if (cmphstr(lp, "basic")) cdtParseFnameAndExecuteCmd(dirSAVECDTBasic);
	else if (cmphstr(lp, "code")) cdtParseFnameAndExecuteCmd(dirSAVECDTCode);
	else if (cmphstr(lp, "headless")) cdtParseFnameAndExecuteCmd(dirSAVECDTHeadless);
	else Error("[SAVECDT] unknown command (commands: FULL, EMPTY, BASIC, CODE, HEADLESS)", lp, SUPPRESS);	
}

//
// Amstrad CPC cartridge saving (CPR)
//

static int SaveCPR(const std::filesystem::path & fname, int cprSize) {
	FILE* ff;
	if (!FOPEN_ISOK(ff, fname, "wb")) {
		Error("[SAVECPR] Error opening file for write", fname.string().c_str());
		return 0;
	}
	AddDeleteOnError(fname);

	// format:  https://cpctech.cpcwiki.de/docs/cprdef.html
	const char magic[12] = { 'R', 'I', 'F', 'F', ' ', ' ', ' ', ' ', 'A', 'M', 'S', '!' };
	// header is 12 bytes
	byte snbuf[12];
	// copy over the magic marker
	memcpy(snbuf, magic, 12);
	// (('cbXX' + size of page + page length)) * number of pages) + 'AMS!')
	aint Length = ((4 + 4 + 0x4000) * cprSize) + 4;
	for (int loop = 0; loop < 4; ++loop) {
		snbuf[4 + loop] = Length & 0xFF;
		Length >>= 8;
	}

	// first we write the header
	if (fwrite(snbuf, 1, 12, ff) != 12) {
		return writeError(fname, ff);
	}

	// Write the pages out in order
	for (int page = 0; page < cprSize; ++page) {
		snbuf[0] = 'c';
		snbuf[1] = 'b';
		snbuf[2] = '0' + ((page / 10)%10);
		snbuf[3] = '0' + (page % 10);

		Length = Device->GetPage(page)->Size;
		for (int loop = 0; loop < 4; ++loop) {
			snbuf[4 + loop] = Length & 0xFF;
		Length >>= 8;
		}

		// first we write the page header
		if (fwrite(snbuf, 1, 8, ff) != 8) {
			return writeError(fname, ff);
		}

		// then we write the page
		if ((aint)fwrite(Device->GetPage(page)->RAM, 1, Device->GetPage(page)->Size, ff) != Device->GetPage(page)->Size) {
			return writeError(fname, ff);
		}
	}

	fclose(ff);
	return 1;
}

static const char* err_txt_size = "[SAVECPR] only a size from 1 (16KiB) to 32 (512KiB) is allowed";

void dirSAVECPR() {
	if (pass != LASTPASS) {
		SkipToEol(lp);
		return;
	}
	if (!IsAmstradPLUSDevice(DeviceID)) {
		Error("[SAVECPR] is allowed only in AMSTRADCPCPLUS device mode", NULL, SUPPRESS);
		return;
	}
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	if (!fnaam.has_filename()) {
		Error("[SAVECPR] CPR file name is empty", NULL, SUPPRESS);
		return;
	}
	int cprSize = 32;
	if (anyComma(lp)) {
		if (SkipBlanks()) Error(err_txt_size, NULL, SUPPRESS);	// return will happen on ParseExpression
		aint val;
		if (!ParseExpression(lp, val)) return;
		if ((val < 1) || (val > 32)) {
			Error(err_txt_size, NULL, SUPPRESS);
			return;
		}
		cprSize = val;
	}
	if (!SaveCPR(fnaam, cprSize)) Error("[SAVECPR] Error writing file (Disk full?)", NULL, IF_FIRST);
}

// eof io_cpc.cpp
