/*

  SjASMPlus Z80 Cross Compiler - modified - SAVENEX extension

  Copyright (c) 2006 Sjoerd Mastijn (original SW)
  Copyright (c) 2019 Peter Ped Helcmanovsky (SAVENEX extension)

  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.

*/

#include "sjdefs.h"
#include "crc32c.h"

// Banks in file are ordered in SNA way (but array "banks" in header is in numeric order instead)
static constexpr aint nexBankOrder[8] = {5, 2, 0, 1, 3, 4, 6, 7};

#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct SNexHeader {
	constexpr static size_t COPPER_SIZE = 0x0800;
	constexpr static size_t PAL_SIZE = 0x200;
	constexpr static aint MAX_BANK = 112;
	constexpr static aint MAX_PAGE = MAX_BANK * 2;
	constexpr static byte SCR_LAYER2	= 0x01;
	constexpr static byte SCR_ULA		= 0x02;
	constexpr static byte SCR_LORES		= 0x04;
	constexpr static byte SCR_HIRES		= 0x08;
	constexpr static byte SCR_HICOL		= 0x10;
	constexpr static byte SCR_EXT2		= 0x40;
	constexpr static byte SCR_NOPAL		= 0x80;
	constexpr static byte SCR2_320x256	= 1;
	constexpr static byte SCR2_640x256	= 2;
	constexpr static byte SCR2_tilemap	= 3;

	byte		magicAndVersion[8];	// the "magic" number + file version at the beginning
	byte		ramReq;				// 0 = 768k, 1 = 1792k
	byte		numBanks;			// number of 16k banks to load: 0..112
	byte		screen;				// loading screen flags
	byte		border;				// border colour 0..7
	word		sp;					// stack pointer
	word		pc;					// start address (0 = no start)
	word		_obsolete_numfiles;
	byte		banks[MAX_BANK];	// 112 16ki banks (1.75MiB) - non-zero value = in file
	// banks array is ordinary order 0, 1, 2, ..., but banks in file are in order: 5, 2, 0, 1, ...
	byte		loadbar;			// 0/1 show progress bar
	byte		loadbarColour;		// colour of progress bar (precise meaning depends on gfx mode)
	byte		loadDelay;			// delay after each bank is loaded (number of frames)
	byte		startDelay;			// delay after whole file is loaded (number of frames)
	byte		preserveNextRegs;	// 0 = reset whole machine state, 1 = preserve most of it
	byte		coreVersion[3];
	byte		hiResColour;		// bits 5-3 for port 255 (ASM source provides 0..7 value, needs shift)
	byte		entryBank;			// 16ki bank 0..111 to be mapped into C000..FFFF range
	word		fileHandleCfg;		// 0 = close NEX file, 1 = pass handle in BC, 0x4000+ = address to write handle
	// V1.3 fields
	byte		expBusDisable;		// 0 = disable expansion bus by setting top four bits of NextReg 0x80, 1 = no-op
	byte		hasChecksum;		// 0 = no checksum, 1 = last 4B of header are CRC-32C (Castagnoli)
	uint32_t	banksOffset;		// where data of first bank start in file (NEX V1.3+ file, 0 olders)
	word		cliBuffer;			// address of buffer for command line copy (0 = off)
	word		cliBufferSize;		// size of the provided buffer (0 = off) (cmd line is truncated to this size)
	byte		screen2;			// extended screen flag (old "screen" must have +64 flag)
	byte		hasCopperCode;		// 0 = no copper, 1 = 2048B copper block after loading screens
	byte		tilesScrConfig[4];	// NextReg registers $6B, $6C, $6E, $6F values for Tilemode screen
	byte		bigL2barPosY;		// Y position (0..255) of loading bar for new Layer 2 320x256 and 640x256 modes
	byte		_reserved[349];
	uint32_t	crc32c;				// CRC-32C build by: file offset 512->EOF (including append bin), then 508B header

	void init();
	void prepareLittleEndianBinaryForm();
	void restoreHostEndianBinaryForm();
}
#ifndef _MSC_VER
	__attribute__((packed));
#else
	;
#pragma pack(pop)
#endif
static_assert(512 == sizeof(SNexHeader), "NEX header is expected to be 512 bytes long!");

struct SNexFile {
	SNexHeader	h;
	aint		reqFileVersion;		// requested file version in OPEN command
	aint		minFileVersion;		// currently auto-detected file version
	FILE*		f = nullptr;		// NEX file handle, stay opened, fseek stays at <EOF>
		// file is build sequentially, adding further blocks, only finalize does refresh the header
	aint		lastBankIndex;		// numeric order (0, 1, ...) value, -1 is init value
	byte*		copper = nullptr;	// temporary storage of copper code (add it ahead of first bank)
	byte*		palette = nullptr;	// final palette (will override the one stored upon finalize)
	bool		palDefined;			// whether the palette data/type was enforced by PALETTE command
	bool		canAppend = false;	// true when `fwrite(..., f)` can be used in "append like" way
	// set `canAppend` to false whenever you do fseek/fread, it cancels validity of "next fwrite"

	~SNexFile();
	void init();
	void writeHeader();
	void writePalette();
	void calculateCrc32C();
	void updateIfAheadFirstBankSave();
	void finalizeFile();
};

// the instance holding all tooling data and header about currently opened NEX file
static SNexFile nex;

void SNexHeader::init() {
	memset(magicAndVersion, 0, sizeof(SNexHeader));	// clear whole 512 bytes
	// set "magic" number and file version
	memcpy(magicAndVersion, "NextV1.2", 8);			// setup "magic" number at beginning
	// required core version is by default 2.00.28 (latest released)
	coreVersion[0] = 2;
	coreVersion[1] = 0;
	coreVersion[2] = 28;
}

void SNexHeader::prepareLittleEndianBinaryForm() {
	if (Options::IsBigEndian) {
		sp = sj_bswap16(sp);
		pc = sj_bswap16(pc);
		_obsolete_numfiles = sj_bswap16(_obsolete_numfiles);
		fileHandleCfg = sj_bswap16(fileHandleCfg);
		banksOffset = sj_bswap32(banksOffset);
		cliBuffer = sj_bswap16(cliBuffer);
		cliBufferSize = sj_bswap16(cliBufferSize);
		crc32c = sj_bswap32(crc32c);
	}
}

void SNexHeader::restoreHostEndianBinaryForm() {
	// it's actually identical to the prepareLittleEndianBinaryForm, but keeping unique naming
	prepareLittleEndianBinaryForm();
}

SNexFile::~SNexFile() {
	finalizeFile();
}

void SNexFile::init() {
	h.init();
	lastBankIndex = -1;		// reset last bank index
	palDefined = false;
	reqFileVersion = 0;
	minFileVersion = 2;
}

void SNexFile::updateIfAheadFirstBankSave() {
	// check if already updated or file is not ready for appending (no file, or finalizing)
	if (h.banksOffset || !canAppend) return;
	// updating bank offset after some bank was already stored -> should never happen
	if (-1 != lastBankIndex) Error("[SAVENEX] V1.3?!", NULL, FATAL);	// unreachable
	if (palDefined && 0 == h.screen) {
		Warning("[SAVENEX] some palette was defined, but without screen it is ignored.");
	}
	// V1.3 feature, copper code is the last block ahead of first bank
	if (h.hasCopperCode) {
		if (SNexHeader::COPPER_SIZE != fwrite(copper, 1, SNexHeader::COPPER_SIZE, f)) {
			Error("[SAVENEX] writing copper data failed", NULL, FATAL);
		}
	}
	// V1.3 feature, offset of very first bank stored in file
	h.banksOffset = ftell(f);
}

void SNexFile::writeHeader() {
	if (nullptr == f) return;
	canAppend = false;							// does fseek, cancel the "append" mode
	// refresh/write the file header
	fseek(f, 0, SEEK_SET);
	h.prepareLittleEndianBinaryForm();
	if (sizeof(SNexHeader) != fwrite(&h, 1, sizeof(SNexHeader), f)) {
		Error("[SAVENEX] writing header content failed", NULL, SUPPRESS);
	}
	h.restoreHostEndianBinaryForm();
}

void SNexFile::writePalette() {
	if (!canAppend) return;
	if (!palDefined || nullptr == palette) {	// palette is completely undefined or
		h.screen = SNexHeader::SCR_NOPAL;		// "no palette" was defined
	} else {
		if (SNexHeader::PAL_SIZE != fwrite(palette, 1, SNexHeader::PAL_SIZE, f)) {
			Error("[SAVENEX] writing palette data failed", NULL, FATAL);
		}
	}
}

void SNexFile::calculateCrc32C() {
	if (!h.hasChecksum) return;
	if (nullptr == f) return;
	canAppend = false;							// does fseek+fread, cancel the "append" mode
	// calculate checksum CRC-32C (Castagnoli)
	crc32_init();
	constexpr size_t BUFFER_SIZE = 128 * 1024;	// 128kiB buffer to read file (must be 512+ !!)
	uint8_t *buffer = new uint8_t[BUFFER_SIZE];
	if (nullptr == buffer) ErrorOOM();
	uint32_t crc = 0;
	// calculate CRC of the file part after header (offset 512)
	fseek(f, 512, SEEK_SET);
	size_t bytes_read = 0;
	do {
		bytes_read = fread(buffer, 1, BUFFER_SIZE, f);
		if (0 == bytes_read) break;
		crc = crc32c_append_sw(crc, buffer, bytes_read);
	} while (BUFFER_SIZE == bytes_read);
	// calculate CRC of the header part (first 508 bytes of header)
	fseek(f, 0, SEEK_SET);
	bytes_read = fread(buffer, 1, 508, f);
	h.hasChecksum = (508 == bytes_read);
	if (h.hasChecksum) {
		h.crc32c = crc32c_append_sw(crc, buffer, bytes_read);
	} else {
		Error("[SAVENEX] reading file for CRC calculation failed");
	}
	delete[] buffer;
}

void SNexFile::finalizeFile() {
	if (nullptr == f) return;
	// do the final V1.2 / V1.3 updates to the header fields
	// V1.3 auto-detected when V1.2 is required should never happen (Error should be unreachable)
	if (3 == minFileVersion && 2 == reqFileVersion) Error("[SAVENEX] V1.3?!", NULL, FATAL);
	updateIfAheadFirstBankSave();	// if no BANK/AUTO/CLOSE was used -> update all now
	if (2 == minFileVersion) {
		h.banksOffset = 0;		// clear banksOffset for V1.2 files
		h.bigL2barPosY = 0;		// clear big Layer 2 loading-bar posY for V1.2 files
	} else {
		h.magicAndVersion[7] = '3';				// modify file version to "V1.3" string
		calculateCrc32C();
	}
	// refresh the file header to final state
	writeHeader();
	// close the file
	fclose(f);
	f = nullptr;
	canAppend = false;
	if (nullptr != copper) delete[] copper;
	copper = nullptr;
	if (nullptr != palette) delete[] palette;
	palette = nullptr;
	// check if there were banks 48+, but 2MB required was not set
	byte hasExtendedBank = 0;
	for (int i = 48; i < SNexHeader::MAX_BANK; ++i) hasExtendedBank |= h.banks[i];
	if (!h.ramReq && hasExtendedBank) {
		Error("[SAVENEX] 2MB bank (48..111) stored without 2MbRamReq set in CFG");
	}
	return;
}

enum EBmpType { other, Layer2, LoRes, L2_320x256, L2_640x256 };

class SBmpFile {
	static constexpr size_t HEADERS_SIZE = 0x36;	// 14B header + BITMAPINFOHEADER 40B header
	static constexpr size_t PALETTE_SIZE = 0x100;

	FILE*		bmp;
	byte		tempHeader[HEADERS_SIZE];
	byte*		palBuffer = nullptr;

public:
	EBmpType	type = other;
	int32_t		width = 0, height = 0;
	bool		upsideDown = false;
	uint32_t	colorsUsed = 0;

	~SBmpFile();
	void close();
	bool open(fullpath_ref_t bmpIn);
	word getColor(uint32_t index);
	void loadPixelData(byte* buffer);
};

SBmpFile::~SBmpFile() {
	close();
}

void SBmpFile::close() {
	if (nullptr == bmp) return;
	fclose(bmp);
	bmp = nullptr;
	delete[] palBuffer;
	palBuffer = nullptr;
}

bool SBmpFile::open(fullpath_ref_t bmpIn) {
	if (!FOPEN_ISOK(bmp, bmpIn.full, "rb")) {
		Error("[SAVENEX] Error opening file", bmpIn.str.c_str(), SUPPRESS);
		return false;
	}
	palBuffer = new byte[4*PALETTE_SIZE];
	if (nullptr == palBuffer) ErrorOOM();
	// read header of BMP and verify the file is of expected format
	bool allRead = (HEADERS_SIZE == fread(tempHeader, 1, HEADERS_SIZE, bmp));
	allRead = allRead && (PALETTE_SIZE == fread(palBuffer, 4, PALETTE_SIZE, bmp));
	// these following casts assume the sjasmplus itself is running at little-endian host
	uint32_t header2Size = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 14)->val;
	uint16_t colorPlanes = *reinterpret_cast<uint16_t*>(tempHeader + 26);
	uint16_t bpp = *reinterpret_cast<uint16_t*>(tempHeader + 28);
	uint32_t compressionType = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 30)->val;
	// fix values on BE hosts
	if (Options::IsBigEndian) {
		header2Size = sj_bswap32(header2Size);
		colorPlanes = sj_bswap16(colorPlanes);
		bpp = sj_bswap16(bpp);
		compressionType = sj_bswap32(compressionType);
	}
	// check "BM", BITMAPINFOHEADER type (size 40), 8bpp, no compression
	if (!allRead || 'B' != tempHeader[0] || 'M' != tempHeader[1] ||
		40 != header2Size || 1 != colorPlanes || 8 != bpp || 0 != compressionType)
	{
		Error("[SAVENEX] BMP file is not in expected format (uncompressed, 8bpp, 40B BITMAPINFOHEADER header)",
				bmpIn.str.c_str(), SUPPRESS);
		close();
		return false;
	}
	// check if the size is 256x192 (Layer 2) or 128x96 (LoRes), or 320/640 x 256 (V1.3).
	colorsUsed = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 46)->val;
	width = reinterpret_cast<SAlignSafeCast<int32_t>*>(tempHeader + 18)->val;
	height = reinterpret_cast<SAlignSafeCast<int32_t>*>(tempHeader + 22)->val;
	// fix values on BE hosts
	if (Options::IsBigEndian) {
		colorsUsed = sj_bswap32(colorsUsed);
		width = sj_bswap32(width);
		height = sj_bswap32(height);
	}
	upsideDown = 0 < height;
	if (height < 0) height = -height;
	if (256 == width && 192 == height) type = Layer2;
	if (128 == width && 96 == height) type = LoRes;
	if (256 == height) {
		if (320 == width) type = L2_320x256;
		if (640 == width) type = L2_640x256;
	}
	return true;
}

word SBmpFile::getColor(uint32_t index) {
	if (nullptr == bmp || nullptr == palBuffer || 256 <= index) return 0;
	const byte B = palBuffer[index * 4 + 0] >> 5;
	const byte G = palBuffer[index * 4 + 1] >> 5;
	const byte R = palBuffer[index * 4 + 2] >> 5;
	return ((B&1) << 8) | (B >> 1) | (G << 2) | (R << 5);
}

void SBmpFile::loadPixelData(byte* buffer) {
	uint32_t offset = reinterpret_cast<SAlignSafeCast<uint32_t>*>(tempHeader + 10)->val;
	if (Options::IsBigEndian) offset = sj_bswap32(offset);
	const size_t w = static_cast<size_t>(width);
	for (int32_t y = 0; y < height; ++y) {
		const int32_t fileY = upsideDown ? (height - y - 1) : y;
		fseek(bmp, offset + (w * fileY), SEEK_SET);
		if (w != fread(buffer + (w * y), 1, w, bmp)) {
			Error("[SAVENEX] reading BMP pixel data failed", NULL, FATAL);
		}
	}
}

static aint getNexBankIndex(const aint bank16kNum) {
	if (8 <= bank16kNum && bank16kNum < SNexHeader::MAX_BANK) return bank16kNum;
	for (aint i = 0; i < 8; ++i) {
		if (nexBankOrder[i] == bank16kNum) return i;
	}
	return -2;
}

static aint getNexBankNum(const aint bankIndex) {
	if (0 <= bankIndex && bankIndex < 8) return nexBankOrder[bankIndex];
	if (8 <= bankIndex && bankIndex < SNexHeader::MAX_BANK) return bankIndex;
	return -1;
}

static void checkStackPointer() {
	constexpr size_t CHECK_SIZE = 10;
	constexpr size_t EXPECTED_SLOTS_COUNT = 8;
	const word adrMask = static_cast<word>(Device->GetCurrentSlot()->Size - 1);
	const page_t pages[EXPECTED_SLOTS_COUNT] = { 0, 0, 5*2, 5*2+1, 2*2, 2*2+1, static_cast<page_t>(nex.h.entryBank*2), static_cast<page_t>(nex.h.entryBank*2+1) };
	assert(EXPECTED_SLOTS_COUNT == Device->SlotsCount);
	// check if SP is too close to ROM (0x0001 ... 0x4009)
	if (0x0000 < nex.h.sp && nex.h.sp < 0x4000 + CHECK_SIZE) {
		Warning("[SAVENEX] stackAddress is too close to ROM area");
		return;
	}
	// check if good-looking SP points to enough of zeroed memory, warn about overwrite if not
	word spCheck = word(nex.h.sp - CHECK_SIZE);
	while (spCheck != nex.h.sp) {
		const page_t pageNum = pages[Device->GetSlotOfA16(spCheck)];
		const size_t offset = Device->GetMemoryOffset(pageNum, spCheck & adrMask);
		if (0 != Device->Memory[offset]) break;
		++spCheck;
	}
	if (spCheck == nex.h.sp) return;
	WarningById(W_NEX_STACK);
}

static void dirNexOpen() {
	if (nex.f) {
		Error("[SAVENEX] NEX file is already open", bp, SUPPRESS);
		return;
	}
	nex.init();			// reset everything around NEX file data
	// read OPEN command arguments
	const std::filesystem::path fname = GetOutputFileName(lp);
	aint openArgs[4] = { (-1 == StartAddress ? 0 : StartAddress), 0xFFFE, 0, 0 };
	if (comma(lp)) {
		const bool optionals[] = {false, true, true, true};	// start address is mandatory because comma
		if (!getIntArguments<4>(lp, openArgs, optionals)) {
			Error("[SAVENEX] expected syntax is OPEN <filename>[,<startAddress>[,<stackAddress>[,<entryBank 0..111>[,<fileVersion 2..3>]]]]", bp, SUPPRESS);
			return;
		}
	}
	// validate argument values
	if (-1 != StartAddress && StartAddress != openArgs[0]) {
		Warning("[SAVENEX] Start address was also defined by END, OPEN argument used instead");
	}
	check16(openArgs[0]);
	check16(openArgs[1]);
	if (openArgs[2] < 0 || SNexHeader::MAX_BANK <= openArgs[2]) {
		ErrorInt("[SAVENEX] entry bank can be 0..111 value only", openArgs[2], SUPPRESS);
		return;
	}
	if (openArgs[3] && (openArgs[3] < 2 || 3 < openArgs[3])) {
		ErrorInt("[SAVENEX] only file version 2 (V1.2) or 3 (V1.3) can be enforced", openArgs[3], SUPPRESS);
		return;
	}
	// try to open the actual file
	if (!FOPEN_ISOK(nex.f, fname, "w+b")) Error("[SAVENEX] Error opening file for write", fname.string().c_str(), SUPPRESS);
	if (nullptr == nex.f) return;
	AddDeleteOnError(fname);

	// set the argument values into header, and write the initial version of header into file
	nex.h.pc = openArgs[0] & 0xFFFF;
	nex.h.sp = openArgs[1] & 0xFFFF;
	nex.h.entryBank = openArgs[2];
	nex.reqFileVersion = openArgs[3];
	nex.minFileVersion = (3 == nex.reqFileVersion) ? 3 : 2;	// reset auto-detected file version
	nex.writeHeader();
	// After writing header first time, the file is ready for "append like" usage
	nex.canAppend = true;
	checkStackPointer();
}

static void dirNexCore() {
	if (nullptr == nex.f) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	// parse arguments
	aint coreArgs[3] = {0};
	const bool optionals[] = {false, false, false};
	if (!getIntArguments<3>(lp, coreArgs, optionals)) {
		Error("[SAVENEX] expected syntax is CORE <major 0..15>,<minor 0..15>,<subminor 0..255>", bp, SUPPRESS);
		return;
	}
	// warn about invalid values
	if (coreArgs[0] < 0 || 15 < coreArgs[0] ||
		coreArgs[1] < 0 || 15 < coreArgs[1] ||
		coreArgs[2] < 0 || 255 < coreArgs[2]) Warning("[SAVENEX] values are not within 0..15,0..15,0..255 ranges");
	// set the values in header
	nex.h.coreVersion[0] = coreArgs[0];
	nex.h.coreVersion[1] = coreArgs[1];
	nex.h.coreVersion[2] = coreArgs[2];
}

static void dirNexCfg3() {
// ;; SAVENEX CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]
	if (nullptr == nex.f) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	if (nex.reqFileVersion == 2) {
		Error("[SAVENEX] V1.2 was requested with OPEN, but CFG3 is V1.3 feature.", NULL, SUPPRESS);
		return;
	}
	nex.minFileVersion = 3;		// V1.3 detected
	// parse arguments
	aint cfgArgs[4] = {1, 0};
	const bool optionals[] = {false, true, true, false};
	if (!getIntArguments<4>(lp, cfgArgs, optionals)) {
		Error("[SAVENEX] expected syntax is CFG3 <DoCRC 0/1>[,<PreserveExpansionBus 0/1>[,<CLIbufferAdr>,<CLIbufferSize>]]", bp, SUPPRESS);
		return;
	}
	const bool someCliBuffer = cfgArgs[2] || cfgArgs[3];	// [0, 0] = no CLI buffer, don't check validity
	// warn about invalid values
	if (cfgArgs[0] < 0 || 1 < cfgArgs[0] ||
		cfgArgs[1] < 0 || 1 < cfgArgs[1] ||
		(someCliBuffer &&
			(cfgArgs[2] < 0x4000 || 0x10000 < (cfgArgs[2] + cfgArgs[3]) ||
			cfgArgs[3] < 1 || 0x0800 < cfgArgs[3]))) {
		Warning("[SAVENEX] crc/preserve values are not 0/1 or CLI buffer doesn't fit into $4000..$FFFF range (size can be 2048 max)");
	}
	// set the values in header
	nex.h.hasChecksum = !!cfgArgs[0];
	nex.h.expBusDisable = !!cfgArgs[1];
	nex.h.cliBuffer = cfgArgs[2];
	nex.h.cliBufferSize = cfgArgs[3];
	if (nex.h.hasChecksum && Options::IsBigEndian) {
		Error("[SAVENEX] CRC feature is not available at big-endian host machine (wrong CRC implementation in sjasmplus, sorry)");
		nex.h.hasChecksum = false;
	}
}

static void dirNexCfg() {
// ;; SAVENEX CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]
	if (nullptr == nex.f) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	// parse arguments
	aint cfgArgs[4] = {0};
	const bool optionals[] = {false, true, true, true};
	if (!getIntArguments<4>(lp, cfgArgs, optionals)) {
		Error("[SAVENEX] expected syntax is CFG <border 0..7>[,<fileHandle 0/1/$4000+>[,<PreserveNextRegs 0/1>[,<2MbRamReq 0/1>]]]", bp, SUPPRESS);
		return;
	}
	// warn about invalid values
	if (cfgArgs[0] < 0 || 7 < cfgArgs[0] ||
		cfgArgs[1] < 0 || 0xFFFE < cfgArgs[1] ||
		cfgArgs[2] < 0 || 1 < cfgArgs[2] ||
		cfgArgs[3] < 0 || 1 < cfgArgs[3]) Warning("[SAVENEX] values are not within 0..7,0..65534,0/1,0/1 ranges");
	// set the values in header
	nex.h.border = cfgArgs[0] & 7;
	nex.h.fileHandleCfg = cfgArgs[1];
	nex.h.preserveNextRegs = cfgArgs[2];
	nex.h.ramReq = cfgArgs[3];
}

static void dirNexBar() {
	if (nullptr == nex.f) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	// parse arguments
	aint barArgs[5] = {0, 0, 0, 0, 254};
	const bool optionals[] = {false, false, true, true, true};
	if (!getIntArguments<5>(lp, barArgs, optionals)) {
		Error("[SAVENEX] expected syntax is BAR <loadBar 0/1>,<barColour 0..255>[,<startDelay 0..255>[,<bankDelay 0..255>[,<posY 0..255>]]]", bp, SUPPRESS);
		return;
	}
	// warn about invalid values
	if (barArgs[0] < 0 || 1 < barArgs[0] ||
		barArgs[1] < 0 || 255 < barArgs[1] ||
		barArgs[2] < 0 || 255 < barArgs[2] ||
		barArgs[3] < 0 || 255 < barArgs[3] ||
		barArgs[4] < 0 || 255 < barArgs[4]) Warning("[SAVENEX] values are not within 0/1 or 0..255 ranges");
	// set the values in header
	nex.h.loadbar = barArgs[0];
	nex.h.loadbarColour = barArgs[1];
	nex.h.startDelay = barArgs[2];
	nex.h.loadDelay = barArgs[3];
	nex.h.bigL2barPosY = barArgs[4];
}

static void dirNexPaletteDefault() {
// ;; SAVENEX PALETTE DEFAULT
	nex.palDefined = true;
	nex.palette = new byte[SNexHeader::PAL_SIZE];
	if (nullptr == nex.palette) ErrorOOM();
	for (uint32_t i = 0; i < 256; ++i) {
		nex.palette[i*2 + 0] = static_cast<byte>(i);
		nex.palette[i*2 + 1] = (i & 3) ? 1 : 0;		// bottom blue bit is 1 when some upper bit is
	}
}

static bool dirNexPaletteMem(const aint page8kNum, const aint palOffset) {
	if (nex.palDefined) return true;	// palette was already defined, silently ignore
	if (-1 == page8kNum) {				// this is used as "no palette" by some screen commands
		nex.palDefined = true;
		return true;
	}
	// warn about invalid values
	const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
	const int32_t adrPalData = Device->GetMemoryOffset(page8kNum, palOffset);
	if (adrPalData < 0 || totalRam < adrPalData + SNexHeader::PAL_SIZE) {
		Error("[SAVENEX] palette data address range is outside of Next memory", bp, SUPPRESS);
		return false;
	}
	// copy the data into internal palette buffer
	nex.palDefined = true;
	nex.palette = new byte[SNexHeader::PAL_SIZE];
	if (nullptr == nex.palette) ErrorOOM();
	memcpy(nex.palette, Device->Memory + adrPalData, SNexHeader::PAL_SIZE);
	return true;
}

static bool dirNexPaletteBmp(SBmpFile & bmp) {
	if (nex.palDefined) return true;	// palette was already defined, silently ignore
	// copy the data into internal palette buffer
	nex.palDefined = true;
	nex.palette = new byte[SNexHeader::PAL_SIZE];
	if (nullptr == nex.palette) ErrorOOM();
	constexpr size_t palDataSize = 256;
	for (size_t i = 0; i < palDataSize; ++i) {
		const word nextColor = bmp.getColor(i);
		nex.palette[i*2 + 0] = nextColor & 0xFF;
		nex.palette[i*2 + 1] = nextColor >> 8;
	}
	return true;
}

static void dirNexPaletteMem() {
// ;; SAVENEX PALETTE MEM <palPage8kNum 0..223>,<palOffset>
	aint palArgs[2] = {0, 0};
	const bool optionals[] = {false, false};
	if (!getIntArguments<2>(lp, palArgs, optionals)
			|| palArgs[0] < 0 || SNexHeader::MAX_PAGE <= palArgs[0] || palArgs[1] < 0) {
		Error("[SAVENEX] expected syntax is MEM <palPage8kNum 0..223>,<palOffset 0+>", bp, SUPPRESS);
		return;
	}
	dirNexPaletteMem(palArgs[0], palArgs[1]);
}

static void dirNexPaletteBmp() {
// ;; SAVENEX PALETTE BMP <filename>
	fullpath_ref_t bmpIn = GetInputFile(lp);
	if (bmpIn.full.empty() || comma(lp)) {
		Error("[SAVENEX] expected syntax is BMP <filename>", bp, SUPPRESS);
		return;
	}
	// try to open the actual BMP file
	SBmpFile bmp;
	if (!bmp.open(bmpIn)) return;
	// check the palette if it was requested from this bmp and process it
	dirNexPaletteBmp(bmp);
}

static void dirNexPalette() {
// ;; SAVENEX PALETTE (NONE|DEFAULT|MEM|BMP)
	if (nullptr == nex.f) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	if (nex.palDefined || 0 != nex.h.screen) {
		Error("[SAVENEX] some palette/screen was already defined (define palette once and ahead)", NULL, SUPPRESS);
		return;
	}
	if (-1 != nex.lastBankIndex) {
		Error("[SAVENEX] some bank was already stored (define palette ahead)", NULL, SUPPRESS);
		return;
	}
	SkipBlanks(lp);
	if (cmphstr(lp, "none")) nex.palDefined = true;
	else if (cmphstr(lp, "default")) dirNexPaletteDefault();
	else if (cmphstr(lp, "mem")) dirNexPaletteMem();
	else if (cmphstr(lp, "bmp")) dirNexPaletteBmp();
	else Error("[SAVENEX] unknown palette command (commands: NONE, DEFAULT, MEM, BMP)", lp, SUPPRESS);
}

static void dirNexScreenLayer2andLowRes(EBmpType type) {
// ;; SCREEN L2 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN LR [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN L2_320 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
// ;; SCREEN L2_640 [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]
	// check V1.3 features vs V1.2 enforced file version
	if (L2_320x256 == type || L2_640x256 == type) {
		if (2 == nex.reqFileVersion) {
			Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 screen is V1.3 feature.", NULL, SUPPRESS);
			return;
		}
		nex.minFileVersion = 3;
		nex.h.hiResColour = 0;
	}
	// parse arguments
	aint screenArgs[4] = {-1, 0, -1, 0};
	const bool optionals[] = {true, false, true, false};
	if (!getIntArguments<4>(lp, screenArgs, optionals)
			|| screenArgs[0] < -1 || SNexHeader::MAX_PAGE <= screenArgs[0]		// -1 for default pixel data
			|| screenArgs[2] < -1 || SNexHeader::MAX_PAGE <= screenArgs[2]) {	// -1 for no-palette
		Error("[SAVENEX] expected syntax is ... [<Page8kNum 0..223>,<offset>[,<palPage8kNum 0..223>,<palOffset>]]", bp, SUPPRESS);
		return;
	}
	// warn about invalid values
	const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
	size_t adrPixelData = (-1 == screenArgs[0]) ? 0 : Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
	size_t pixelDataSize = 0x14000;		// L2_320x256 and L2_640x256 size is default
	if (Layer2 == type) pixelDataSize = 0xC000;
	if (LoRes == type) pixelDataSize = 0x3000;
	if (totalRam < adrPixelData + pixelDataSize) {
		Error("[SAVENEX] pixel data address range is outside of Next memory", bp, SUPPRESS);
		return;
	}
	// extract palette into internal buffer
	if (!dirNexPaletteMem(screenArgs[2], screenArgs[3])) return;	// exit on serious error
	// write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
	nex.writePalette();
	// update header loading screen status
	switch (type) {
		case Layer2:		nex.h.screen |= SNexHeader::SCR_LAYER2;		break;
		case LoRes:			nex.h.screen |= SNexHeader::SCR_LORES;		break;
		case L2_320x256:
			nex.h.screen |= SNexHeader::SCR_EXT2;
			nex.h.screen2 = SNexHeader::SCR2_320x256;
			break;
		case L2_640x256:
			nex.h.screen |= SNexHeader::SCR_EXT2;
			nex.h.screen2 = SNexHeader::SCR2_640x256;
			break;
		default:
			break;
	}
	// write pixel data into file - first check if default RAM position should be used to read data
	if (-1 == screenArgs[0]) {
		if (LoRes == type) {	// write first half of LoRes data straight from the VRAM position
			adrPixelData = Device->GetMemoryOffset(5*2, 0);
			pixelDataSize = 0x1800;
			if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
				Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
			}
			adrPixelData += 0x2000;		// address of second half of data
		} else {
			adrPixelData = Device->GetMemoryOffset(9*2, 0);
		}
	}
	// write [remaining] part of pixel data into file
	if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
		Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
	}
}

static void dirNexScreenBmp() {
// ;; SAVENEX SCREEN BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]
	fullpath_ref_t bmpIn = GetInputFile(lp);
	aint bmpArgs[2] = { 1, -1 };
	if (comma(lp)) {	// empty filename will fall here too, causing syntax error
		const bool optionals[] = {false, true};	// savePalette is mandatory after comma
		if (!getIntArguments<2>(lp, bmpArgs, optionals)) {
			Error("[SAVENEX] expected syntax is BMP <filename>[,<savePalette 0/1>[,<paletteOffset 0..15>]]", bp, SUPPRESS);
			return;
		}
	}
	// validate argument values
	if (bmpArgs[0] < 0 || 1 < bmpArgs[0]) {
		Warning("[SAVENEX] savePalette should be 0 or 1 (defaulting to 1)");
		bmpArgs[0] = 1;
	}
	if (bmpArgs[1] < -1 || 15 < bmpArgs[1]) {	// -1 is internal "off" value
		Warning("[SAVENEX] paletteOffset should be in 0..15 range");
	}
	// try to open the actual BMP file
	SBmpFile bmp;
	bool bmpOpened = bmp.open(bmpIn);
	if (bmpOpened && other == bmp.type) {
		Error("[SAVENEX] BMP file is not 256x192, 128x96, 320x256 or 640x256", bmpIn.str.c_str(), SUPPRESS);
		bmpOpened = false;
	}
	if (!bmpOpened) return;
	// bmp opened, and some known type, verify details
	if ((-1 != bmpArgs[1]) && (Layer2 == bmp.type || LoRes == bmp.type)) {
		// V1.2 screen types -> no paletteOffset
		Warning("[SAVENEX] BMP paletteOffset is available only for new V1.3 images (320 or 640 x256)");
		bmpArgs[1] = -1;
	}
	// check if V1.3 screens were provided, and if V1.3 is allowed, init internals
	if (256 == bmp.height) {
		if (2 == nex.reqFileVersion) {
			Error("[SAVENEX] V1.2 was requested with OPEN, but 320x256 or 640x256 BMP is V1.3 feature.", NULL, SUPPRESS);
			return;
		} else {
			nex.minFileVersion = 3;
			if (-1 == bmpArgs[1]) bmpArgs[1] = 0;
		}
	}
	// check the palette if it was requested from this bmp and process it
	if (bmpArgs[0]) dirNexPaletteBmp(bmp);
	// palette is written first into file
	nex.writePalette();
	// update header loading screen status
	switch (bmp.type) {
		case Layer2:
			nex.h.screen |= SNexHeader::SCR_LAYER2;
			break;
		case LoRes:
			nex.h.screen |= SNexHeader::SCR_LORES;
			break;
		case L2_320x256:
			nex.h.screen |= SNexHeader::SCR_EXT2;
			nex.h.screen2 = SNexHeader::SCR2_320x256;
			nex.h.hiResColour = bmpArgs[1];
			break;
		case L2_640x256:
			nex.h.screen |= SNexHeader::SCR_EXT2;
			nex.h.screen2 = SNexHeader::SCR2_640x256;
			nex.h.hiResColour = bmpArgs[1];
			break;
		default:
			; // should be unreachable
	}
	// load and write pixel data
	byte* buffer = new byte[641*256];		// buffer to read pixel data +1 write buffer
	if (nullptr == buffer) ErrorOOM();
	// read BMP first line by line into buffer (undid upside-down also)
	bmp.loadPixelData(buffer);
	// write pixel data into file - do transformation for 320/640 x 256 modes
	if (Layer2 == bmp.type || LoRes == bmp.type) {
		const size_t pixelBlockSize = static_cast<size_t>(bmp.width) * static_cast<size_t>(bmp.height);
		if (pixelBlockSize != fwrite(buffer, 1, pixelBlockSize, nex.f)) {
			Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
		}
	} else {
		constexpr size_t h = 256, wB = 320;
		const bool xMul2 = (L2_640x256 == bmp.type);
		byte* const wbuf = buffer + 640 * 256;	// write buffer is at last 256B block
		// transpose data, store them column by column (two columns at time for 640x256)
		for (size_t x = 0; x < wB; ++x) {
			const byte* src = buffer + (xMul2 ? x*2 : x);
			for (size_t y = 0; y < h; ++y) {
				const byte pixel = xMul2 ? (src[0]<<4) | (src[1]&0x0F) : src[0];
				src += bmp.width;
				wbuf[y] = pixel;
			}
			if (h != fwrite(wbuf, 1, h, nex.f)) {
				Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
			}
		}
	}
	delete[] buffer;
}

static void dirNexScreenUlaTimex(byte scrType) {
// ;; SCREEN (SCR|SHC|SHR) [<hiResColour 0..7>]
	// parse argument (only HiRes screen type)
	if (SNexHeader::SCR_HIRES == scrType) {
		aint hiResColor = 0;
		if (ParseExpression(lp, hiResColor)) {
			if (hiResColor < 0 || 7 < hiResColor) Warning("[SAVENEX] value is not in 0..7 range", bp);
			nex.h.hiResColour = (hiResColor&7) << 3;
		}
	}
	// update header loading screen status
	nex.h.screen = scrType;
	// warn about invalid values
	size_t adrPixelData = Device->GetMemoryOffset(5*2, 0);
	size_t pixelDataSize = (SNexHeader::SCR_ULA == scrType) ? 0x1B00 : 0x1800;
	// write pixel data into file (from the default VRAM position of device)
	if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
		Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
	}
	if (SNexHeader::SCR_ULA == scrType) return;		//ULA is written in one go
	adrPixelData += 0x2000;							// address of second half of data
	// write [remaining] part of pixel data into file
	if (pixelDataSize != fwrite(Device->Memory + adrPixelData, 1, pixelDataSize, nex.f)) {
		Error("[SAVENEX] writing pixel data failed", NULL, FATAL);
	}
}

static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero = false);

static void dirNexScreenTile() {
// ;; SCREEN TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]
	// parse arguments
	aint tileArgs[5] = {0, 0, 0, 0, 1};
	const bool optionals[] = {false, false, false, false, true};
	if (!getIntArguments<5>(lp, tileArgs, optionals)
			|| tileArgs[0] < 0 || 255 < tileArgs[0]
			|| tileArgs[1] < 0 || 255 < tileArgs[1]
			|| tileArgs[2] < 0 || 255 < tileArgs[2]
			|| tileArgs[3] < 0 || 255 < tileArgs[3]
			|| tileArgs[4] < 0 || 1 < tileArgs[4]) {
		Error("[SAVENEX] expected syntax is TILE <NextReg $6B>,<NextReg $6C>,<NextReg $6E>,<NextReg $6F>[,<AlsoStoreBank5 0/1 = 1>]", bp, SUPPRESS);
		return;
	}
	// check file version and set it up to V1.3
	if (2 == nex.reqFileVersion) {
		Error("[SAVENEX] V1.2 was requested with OPEN, but tilemap screen is V1.3 feature.", NULL, SUPPRESS);
		return;
	}
	nex.minFileVersion = 3;
	// write palette into file (or update nex.h.screen with NOPAL flag if no palette was defined)
	nex.writePalette();
	nex.h.screen |= SNexHeader::SCR_EXT2;
	nex.h.screen2 = SNexHeader::SCR2_tilemap;
	nex.h.tilesScrConfig[0] = static_cast<byte>(tileArgs[0]);
	nex.h.tilesScrConfig[1] = static_cast<byte>(tileArgs[1]);
	nex.h.tilesScrConfig[2] = static_cast<byte>(tileArgs[2]);
	nex.h.tilesScrConfig[3] = static_cast<byte>(tileArgs[3]);
	// write Bank 5 into file, if requested/default
	if (0 == tileArgs[4]) return;		// suppressed
	saveBank(getNexBankIndex(5), 5);
}

static void dirNexScreen() {
	if (!nex.canAppend) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	if (0 != nex.h.screen) {
		Error("[SAVENEX] screen for this NEX file was already stored", NULL, SUPPRESS);
		return;
	}
	if (-1 != nex.lastBankIndex) {
		Error("[SAVENEX] some bank was already stored (store screen ahead)", NULL, SUPPRESS);
		return;
	}
	SkipBlanks(lp);
	if (cmphstr(lp, "l2")) dirNexScreenLayer2andLowRes(Layer2);
	else if (cmphstr(lp, "lr")) dirNexScreenLayer2andLowRes(LoRes);
	else if (cmphstr(lp, "l2_320")) dirNexScreenLayer2andLowRes(L2_320x256);
	else if (cmphstr(lp, "l2_640")) dirNexScreenLayer2andLowRes(L2_640x256);
	else if (cmphstr(lp, "bmp")) dirNexScreenBmp();
	else if (cmphstr(lp, "scr")) dirNexScreenUlaTimex(SNexHeader::SCR_ULA);
	else if (cmphstr(lp, "shc")) dirNexScreenUlaTimex(SNexHeader::SCR_HICOL);
	else if (cmphstr(lp, "shr")) dirNexScreenUlaTimex(SNexHeader::SCR_HIRES);
	else if (cmphstr(lp, "tile")) dirNexScreenTile();
	else Error("[SAVENEX] unknown screen type (types: BMP, L2, L2_320, L2_640, LR, SCR, SHC, SHR, TILE)", lp, SUPPRESS);
}

static void dirNexCopper() {
// ;; SAVENEX COPPER <Page8kNum 0..223>,<offset>
	if (!nex.canAppend) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	if (nex.reqFileVersion == 2) {
		Error("[SAVENEX] V1.2 was requested with OPEN, but COPPER is V1.3 feature.", NULL, SUPPRESS);
		return;
	}
	if (-1 != nex.lastBankIndex) {
		Error("[SAVENEX] some bank was already stored (store copper ahead)", NULL, SUPPRESS);
		return;
	}
	nex.minFileVersion = 3;		// V1.3 detected
	// parse arguments
	aint screenArgs[2] = {0, 0};
	const bool optionals[] = {false, false};
	if (!getIntArguments<2>(lp, screenArgs, optionals)
			|| screenArgs[0] < 0 || SNexHeader::MAX_PAGE <= screenArgs[0]) {
		Error("[SAVENEX] expected syntax is COPPER <Page8kNum 0..223>,<offset>", bp, SUPPRESS);
		return;
	}
	// warn about invalid values
	const size_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
	size_t adrCopperData = Device->GetMemoryOffset(screenArgs[0], screenArgs[1]);
	if (totalRam < adrCopperData + SNexHeader::COPPER_SIZE) {
		Error("[SAVENEX] copper data address range is outside of Next memory", bp, SUPPRESS);
		return;
	}
	// adjust header and remember the copper data for saving them ahead of first bank
	nex.h.hasCopperCode = 1;
	if (nullptr == nex.copper) nex.copper = new byte[SNexHeader::COPPER_SIZE];
	memcpy(nex.copper, Device->Memory + adrCopperData, SNexHeader::COPPER_SIZE);
}

static bool saveBank(aint bankIndex, aint bankNum, bool onlyNonZero) {
	if (bankNum < 0 || SNexHeader::MAX_BANK <= bankNum) return false;
	if (bankIndex <= nex.lastBankIndex) {
		ErrorInt("[SAVENEX] it's too late to save this bank (correct order: 5, 2, 0, 1, 3, 4, 6, ...)",
					bankNum, SUPPRESS);
		return false;
	}
	nex.updateIfAheadFirstBankSave();
	const size_t offset = Device->GetMemoryOffset(bankNum * 2, 0);
	const size_t size = 0x4000;
	nex.lastBankIndex = bankIndex;
	// detect bank which is just full of zeroes and exit early if onlyNonZero is requested
	if (onlyNonZero) {
		size_t zeroOfs = 0;
		while (zeroOfs < size) {
			if (0 != Device->Memory[offset + zeroOfs]) break;
			++zeroOfs;
		}
		if (size == zeroOfs) return true;
	}
	// update NEX header data
	nex.h.banks[bankNum] = 1;
	++nex.h.numBanks;
	// save the bank memory
	if (size != fwrite(Device->Memory + offset, 1, size, nex.f)) {
		Error("[SAVENEX] writing bank data failed", NULL, FATAL);
	}
	return true;
}

static void dirNexBank() {
	if (!nex.canAppend) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	do {
		aint bankNum, bankIndex;
		char *nextLp = lp;
		if (!ParseExpressionNoSyntaxError(lp, bankNum)
			|| (bankIndex = getNexBankIndex(bankNum)) < 0) {
			Error("[SAVENEX] expected bank number 0..111", nextLp, SUPPRESS);
			break;
		}
		if (!saveBank(bankIndex, bankNum)) break;
	} while (comma(lp));
}

static void dirNexAuto() {
	if (!nex.canAppend) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	if (SNexHeader::MAX_BANK-1 == nex.lastBankIndex) {	// actually there's nothing left to scan
		Error("[SAVENEX] all banks are already stored", NULL, SUPPRESS);
		return;
	}
	// parse arguments
	aint autoArgs[2] = { getNexBankNum(nex.lastBankIndex+1), SNexHeader::MAX_BANK-1 };
	const bool optionals[] = {true, true};
	if (!getIntArguments<2>(lp, autoArgs, optionals)
			|| autoArgs[0] < 0 || SNexHeader::MAX_BANK <= autoArgs[0]
			|| autoArgs[1] < 0 || SNexHeader::MAX_BANK <= autoArgs[1]) {
		Error("[SAVENEX] expected syntax is AUTO [<fromBank 0..111>[,<toBank 0..111>]]", bp, SUPPRESS);
		return;
	}
	// validate arguments
	aint fromI = getNexBankIndex(autoArgs[0]), toI = getNexBankIndex(autoArgs[1]);
	if (toI < fromI) {
		Error("[SAVENEX] 'toBank' is less than 'fromBank'", bp, SUPPRESS);
		return;
	}
	while (fromI <= toI) {
		if (!saveBank(fromI, getNexBankNum(fromI), true)) return;
		++fromI;
	}
}

static void dirNexClose() {
	if (!nex.canAppend) {
		Error("[SAVENEX] NEX file is not open", NULL, SUPPRESS);
		return;
	}
	// update V1.3 banksOffset in case there was no bank stored at all (before appending binary data!)
	nex.updateIfAheadFirstBankSave();
	// read CLOSE command arguments and try to append the proposed files (if some were provided)
	static constexpr int copyBufSize = 0x4000;
	std::unique_ptr<byte[]> copyBuffer;		// Lazily allocated buffer (if needed)
	while (!SkipBlanks(lp)) {
		fullpath_ref_t appendIn = GetInputFile(lp);
		if (appendIn.full.has_filename()) {	// some append file requested, try to copy its content at tail of NEX
			FILE* appendF = nullptr;
			if (!FOPEN_ISOK(appendF, appendIn.full, "rb")) {
				Error("[SAVENEX] Error opening append file", appendIn.str.c_str(), SUPPRESS);
			} else {
				if (!copyBuffer) copyBuffer = std::make_unique<byte[]>(copyBufSize);
				do {
					const size_t read = fread(copyBuffer.get(), 1, copyBufSize, appendF);
					if (read) {
						const size_t write = fwrite(copyBuffer.get(), 1, read, nex.f);
						if (write != read) Error("[SAVENEX] writing append data failed", NULL, FATAL);
					}
				} while (!feof(appendF));
				fclose(appendF);
			}
		}
		if (!comma(lp)) break;
	}
	// finalize the NEX file (refresh the header data and close it)
	nex.finalizeFile();
}

void dirSAVENEX() {
	if (pass != LASTPASS) return;		// syntax error is not visible in early passes
	if (nullptr == DeviceID || strcmp(DeviceID, "ZXSPECTRUMNEXT")) {
		Error("[SAVENEX] is allowed only in ZXSPECTRUMNEXT device mode", NULL, SUPPRESS);
		return;
	}
	SkipBlanks(lp);
	if (cmphstr(lp, "open")) dirNexOpen();
	else if (cmphstr(lp, "core")) dirNexCore();
	else if (cmphstr(lp, "cfg3")) dirNexCfg3();
	else if (cmphstr(lp, "cfg")) dirNexCfg();
	else if (cmphstr(lp, "bar")) dirNexBar();
	else if (cmphstr(lp, "palette")) dirNexPalette();
	else if (cmphstr(lp, "screen")) dirNexScreen();
	else if (cmphstr(lp, "copper")) dirNexCopper();
	else if (cmphstr(lp, "bank")) dirNexBank();
	else if (cmphstr(lp, "auto")) dirNexAuto();
	else if (cmphstr(lp, "close")) dirNexClose();
	else Error("[SAVENEX] unknown command (commands: OPEN, CORE, CFG, BAR, SCREEN, BANK, AUTO, CLOSE)", lp, SUPPRESS);
}
