// license:BSD-3-Clause
// copyright-holders:Nicola Salmoria,Paul Priest,Aaron Giles,Vas Crabb
/*********************************************************************

    romload.cpp

    ROM loading functions.

*********************************************************************/

#include "emu.h"
#include "romload.h"

#include "corestr.h"
#include "emuopts.h"
#include "drivenum.h"
#include "softlist_dev.h"
#include "ui/uimain.h"

#include <algorithm>
#include <set>


#define LOG_LOAD 0
#define LOG(...) do { if (LOG_LOAD) debugload(__VA_ARGS__); } while(0)


/***************************************************************************
    CONSTANTS
***************************************************************************/

#define TEMPBUFFER_MAX_SIZE     (1024 * 1024 * 1024)

/***************************************************************************
    HELPERS
****************************************************************************/

namespace {

auto next_parent_system(game_driver const &system)
{
	return
			[sys = &system, roms = std::vector<rom_entry>()] () mutable -> rom_entry const *
			{
				if (!sys)
					return nullptr;
				int const parent(driver_list::find(sys->parent));
				if (0 > parent)
				{
					sys = nullptr;
					return nullptr;
				}
				else
				{
					sys = &driver_list::driver(parent);
					roms = rom_build_entries(sys->rom);
					return &roms[0];
				}
			};
}


auto next_parent_software(std::vector<software_info const *> const &parents)
{
	auto part(parents.front()->parts().end());
	return
			[&parents, current = parents.cbegin(), part, end = part] () mutable -> const rom_entry *
			{
				if (part == end)
				{
					if (parents.end() == current)
						return nullptr;
					part = (*current)->parts().cbegin();
					end = (*current)->parts().cend();
					do { ++current; } while ((parents.cend() != current) && (*current)->parts().empty());
				}
				return &(*part++).romdata()[0];
			};
}


std::vector<std::string> make_software_searchpath(software_list_device &swlist, software_info const &swinfo, std::vector<software_info const *> &parents)
{
	std::vector<std::string> result;

	// search <rompath>/<list>/<software> following parents
	for (software_info const *i = &swinfo; i; )
	{
		if (std::find(parents.begin(), parents.end(), i) != parents.end())
			break;
		parents.emplace_back(i);
		result.emplace_back(util::string_format("%s" PATH_SEPARATOR "%s", swlist.list_name(), i->shortname()));
		i = i->parentname().empty() ? nullptr : swlist.find(i->parentname());
	}

	// search <rompath>/<software> following parents
	for (software_info const *i : parents)
		result.emplace_back(i->shortname());

	return result;
}


std::error_condition do_open_disk(const emu_options &options, std::initializer_list<std::reference_wrapper<const std::vector<std::string> > > searchpath, const rom_entry *romp, chd_file &chd, std::function<const rom_entry * ()> next_parent)
{
	// hashes are fixed, but we might need to try multiple filenames
	std::set<std::string> tried;
	const util::hash_collection hashes(romp->hashdata());
	std::string filename, fullpath;
	const rom_entry *parent(nullptr);
	std::error_condition result(std::errc::no_such_file_or_directory);
	while (romp && result)
	{
		filename = romp->name() + ".chd";
		if (tried.insert(filename).second)
		{
			// piggyback on emu_file to find the disk image file
			std::unique_ptr<emu_file> imgfile;
			for (const std::vector<std::string> &paths : searchpath)
			{
				imgfile.reset(new emu_file(options.media_path(), paths, OPEN_FLAG_READ));
				imgfile->set_restrict_to_mediapath(1);
				const std::error_condition filerr(imgfile->open(filename, OPEN_FLAG_READ));
				if (!filerr)
					break;
				else
					imgfile.reset();
			}

			// if we couldn't open a candidate file, report an error; otherwise reopen it as a CHD
			if (imgfile)
			{
				fullpath = imgfile->fullpath();
				imgfile.reset();
				result = chd.open(fullpath);
			}
		}

		// walk the parents looking for a CHD with the same hashes but a different name
		if (result)
		{
			while (romp)
			{
				// find a file in a disk region
				if (parent)
					romp = rom_next_file(romp);
				while (!parent || !romp)
				{
					if (!parent)
					{
						parent = next_parent();
						if (!parent)
						{
							romp = nullptr;
							break;
						}
						while (ROMENTRY_ISPARAMETER(parent) || ROMENTRY_ISSYSTEM_BIOS(parent) || ROMENTRY_ISDEFAULT_BIOS(parent))
							++parent;
						if (ROMENTRY_ISEND(parent))
							parent = nullptr;
					}
					else
					{
						parent = rom_next_region(parent);
					}
					while (parent && !ROMREGION_ISDISKDATA(parent))
						parent = rom_next_region(parent);
					if (parent)
						romp = rom_first_file(parent);
				}

				// try it if it matches the hashes
				if (romp && (util::hash_collection(romp->hashdata()) == hashes))
					break;
			}
		}
	}
	return result;
}

} // anonymous namespace


/***************************************************************************
    ROM LOADING
***************************************************************************/

/*-------------------------------------------------
    rom_first_region - return pointer to first ROM
    region
-------------------------------------------------*/

const rom_entry *rom_first_region(const device_t &device)
{
	return rom_first_region(&device.rom_region_vector().front());
}

const rom_entry *rom_first_region(const rom_entry *romp)
{
	while (ROMENTRY_ISPARAMETER(romp) || ROMENTRY_ISSYSTEM_BIOS(romp) || ROMENTRY_ISDEFAULT_BIOS(romp))
		romp++;
	return !ROMENTRY_ISEND(romp) ? romp : nullptr;
}


/*-------------------------------------------------
    rom_next_region - return pointer to next ROM
    region
-------------------------------------------------*/

const rom_entry *rom_next_region(const rom_entry *romp)
{
	romp++;
	while (!ROMENTRY_ISREGIONEND(romp))
		romp++;
	while (ROMENTRY_ISPARAMETER(romp))
		romp++;
	return ROMENTRY_ISEND(romp) ? nullptr : romp;
}


/*-------------------------------------------------
    rom_first_file - return pointer to first ROM
    file
-------------------------------------------------*/

const rom_entry *rom_first_file(const rom_entry *romp)
{
	romp++;
	while (!ROMENTRY_ISFILE(romp) && !ROMENTRY_ISREGIONEND(romp))
		romp++;
	return ROMENTRY_ISREGIONEND(romp) ? nullptr : romp;
}


/*-------------------------------------------------
    rom_next_file - return pointer to next ROM
    file
-------------------------------------------------*/

const rom_entry *rom_next_file(const rom_entry *romp)
{
	romp++;
	while (!ROMENTRY_ISFILE(romp) && !ROMENTRY_ISREGIONEND(romp))
		romp++;
	return ROMENTRY_ISREGIONEND(romp) ? nullptr : romp;
}


/*-------------------------------------------------
    rom_first_parameter - return pointer to the first
    per-game parameter
-------------------------------------------------*/

const rom_entry *rom_first_parameter(const device_t &device)
{
	const rom_entry *romp = &device.rom_region_vector().front();
	while (romp && !ROMENTRY_ISEND(romp) && !ROMENTRY_ISPARAMETER(romp))
		romp++;
	return (romp != nullptr && !ROMENTRY_ISEND(romp)) ? romp : nullptr;
}


/*-------------------------------------------------
    rom_next_parameter - return pointer to the next
    per-game parameter
-------------------------------------------------*/

const rom_entry *rom_next_parameter(const rom_entry *romp)
{
	romp++;
	while (!ROMENTRY_ISREGIONEND(romp) && !ROMENTRY_ISPARAMETER(romp))
		romp++;
	return ROMENTRY_ISEND(romp) ? nullptr : romp;
}


/*-------------------------------------------------
    rom_file_size - return the expected size of a
    file given the ROM description
-------------------------------------------------*/

u32 rom_file_size(const rom_entry *romp)
{
	u32 maxlength = 0;

	/* loop until we run out of reloads */
	do
	{
		/* loop until we run out of continues/ignores */
		u32 curlength = ROM_GETLENGTH(romp++);
		while (ROMENTRY_ISCONTINUE(romp) || ROMENTRY_ISIGNORE(romp))
			curlength += ROM_GETLENGTH(romp++);

		/* track the maximum length */
		maxlength = std::max(maxlength, curlength);
	}
	while (ROMENTRY_ISRELOAD(romp));

	return maxlength;
}


/*-------------------------------------------------
    debugload - log data to a file
-------------------------------------------------*/

static void CLIB_DECL ATTR_PRINTF(1,2) debugload(const char *string, ...)
{
	static int opened;
	va_list arg;
	FILE *f;

	f = fopen("romload.log", opened++ ? "a" : "w");
	if (f)
	{
		va_start(arg, string);
		vfprintf(f, string, arg);
		va_end(arg);
		fclose(f);
	}
}


/***************************************************************************
    HARD DISK HANDLING
***************************************************************************/

/*-------------------------------------------------
    get_disk_handle - return a pointer to the
    CHD file associated with the given region
-------------------------------------------------*/

chd_file *rom_load_manager::get_disk_handle(std::string_view region)
{
	for (auto &curdisk : m_chd_list)
		if (curdisk->region() == region)
			return &curdisk->chd();
	return nullptr;
}


/*-------------------------------------------------
    set_disk_handle - set a pointer to the CHD
    file associated with the given region
-------------------------------------------------*/

std::error_condition rom_load_manager::set_disk_handle(std::string_view region, const char *fullpath)
{
	auto chd = std::make_unique<open_chd>(region);
	auto err = chd->orig_chd().open(fullpath);
	if (!err)
		m_chd_list.push_back(std::move(chd));
	return err;
}

/*-------------------------------------------------
    determine_bios_rom - determine system_bios
    from SystemBios structure and OPTION_BIOS
-------------------------------------------------*/

void rom_load_manager::determine_bios_rom(device_t &device, const char *specbios)
{
	// default is applied by the device at config complete time
	if (specbios && *specbios && core_stricmp(specbios, "default"))
	{
		bool found(false);
		for (const rom_entry &rom : device.rom_region_vector())
		{
			if (ROMENTRY_ISSYSTEM_BIOS(&rom))
			{
				char const *const biosname = ROM_GETNAME(&rom);
				int const bios_flags = ROM_GETBIOSFLAGS(&rom);
				char bios_number[20];

				// Allow '-bios n' to still be used
				sprintf(bios_number, "%d", bios_flags - 1);
				if (!core_stricmp(bios_number, specbios) || !core_stricmp(biosname, specbios))
				{
					found = true;
					device.set_system_bios(bios_flags);
					break;
				}
			}
		}

		// if we got neither an empty string nor 'default' then warn the user
		if (!found)
		{
			m_errorstring.append(util::string_format("%s: invalid BIOS \"%s\", reverting to default\n", device.tag(), specbios));
			m_warnings++;
		}
	}

	// log final result
	LOG("For \"%s\" using System BIOS: %d\n", device.tag(), device.system_bios());
}


/*-------------------------------------------------
    count_roms - counts the total number of ROMs
    that will need to be loaded
-------------------------------------------------*/

void rom_load_manager::count_roms()
{
	const rom_entry *region, *rom;

	/* start with 0 */
	m_romstotal = 0;
	m_romstotalsize = 0;

	/* loop over regions, then over files */
	for (device_t &device : device_enumerator(machine().config().root_device()))
		for (region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
			for (rom = rom_first_file(region); rom != nullptr; rom = rom_next_file(rom))
				if (ROM_GETBIOSFLAGS(rom) == 0 || ROM_GETBIOSFLAGS(rom) == device.system_bios())
				{
					m_romstotal++;
					m_romstotalsize += rom_file_size(rom);
				}
}


/*-------------------------------------------------
    fill_random - fills an area of memory with
    random data
-------------------------------------------------*/

void rom_load_manager::fill_random(u8 *base, u32 length)
{
	while (length--)
		*base++ = machine().rand();
}


/*-------------------------------------------------
    handle_missing_file - handles error generation
    for missing files
-------------------------------------------------*/

void rom_load_manager::handle_missing_file(const rom_entry *romp, const std::vector<std::string> &tried_file_names, std::error_condition chderr)
{
	std::string tried;
	if (!tried_file_names.empty())
	{
		tried = " (tried in";
		for (const std::string &path : tried_file_names)
		{
			tried += ' ';
			tried += path;
		}
		tried += ')';
	}

	const bool is_chd(chderr);
	const std::string name(is_chd ? romp->name() + ".chd" : romp->name());

	const bool is_chd_error(is_chd && chderr != std::errc::no_such_file_or_directory);
	if (is_chd_error)
		m_errorstring.append(string_format("%s CHD ERROR: %s\n", name, chderr.message()));

	if (ROM_ISOPTIONAL(romp))
	{
		// optional files are okay
		if (!is_chd_error)
			m_errorstring.append(string_format("OPTIONAL %s NOT FOUND%s\n", name, tried));
		m_warnings++;
	}
	else if (util::hash_collection(romp->hashdata()).flag(util::hash_collection::FLAG_NO_DUMP))
	{
		// no good dumps are okay
		if (!is_chd_error)
			m_errorstring.append(string_format("%s NOT FOUND (NO GOOD DUMP KNOWN)%s\n", name, tried));
		m_knownbad++;
	}
	else
	{
		// anything else is bad
		if (!is_chd_error)
			m_errorstring.append(string_format("%s NOT FOUND%s\n", name, tried));
		m_errors++;
	}
}


/*-------------------------------------------------
    dump_wrong_and_correct_checksums - dump an
    error message containing the wrong and the
    correct checksums for a given ROM
-------------------------------------------------*/

void rom_load_manager::dump_wrong_and_correct_checksums(const util::hash_collection &hashes, const util::hash_collection &acthashes)
{
	m_errorstring.append(string_format("    EXPECTED: %s\n", hashes.macro_string()));
	m_errorstring.append(string_format("       FOUND: %s\n", acthashes.macro_string()));
}


/*-------------------------------------------------
    verify_length_and_hash - verify the length
    and hash signatures of a file
-------------------------------------------------*/

void rom_load_manager::verify_length_and_hash(emu_file *file, std::string_view name, u32 explength, const util::hash_collection &hashes)
{
	// we've already complained if there is no file
	if (!file)
		return;

	// verify length
	u64 const actlength = file->size();
	if (explength != actlength)
	{
		m_errorstring.append(string_format("%s WRONG LENGTH (expected: %08x found: %08x)\n", name, explength, actlength));
		m_warnings++;
	}

	if (hashes.flag(util::hash_collection::FLAG_NO_DUMP))
	{
		// If there is no good dump known, write it
		m_errorstring.append(string_format("%s NO GOOD DUMP KNOWN\n", name));
		m_knownbad++;
	}
	else
	{
		// verify checksums
		util::hash_collection const &acthashes = file->hashes(hashes.hash_types());
		if (hashes != acthashes)
		{
			// otherwise, it's just bad
			util::hash_collection const &all_acthashes = (acthashes.hash_types() == util::hash_collection::HASH_TYPES_ALL)
					? acthashes
					: file->hashes(util::hash_collection::HASH_TYPES_ALL);
			m_errorstring.append(string_format("%s WRONG CHECKSUMS:\n", name));
			dump_wrong_and_correct_checksums(hashes, all_acthashes);
			m_warnings++;
		}
		else if (hashes.flag(util::hash_collection::FLAG_BAD_DUMP))
		{
			// If it matches, but it is actually a bad dump, write it
			m_errorstring.append(string_format("%s ROM NEEDS REDUMP\n", name));
			m_knownbad++;
		}
	}
}


/*-------------------------------------------------
    display_loading_rom_message - display
    messages about ROM loading to the user
-------------------------------------------------*/

void rom_load_manager::display_loading_rom_message(const char *name, bool from_list)
{
	std::string buffer;
	if (name)
		buffer = util::string_format("%s (%d%%)", from_list ? "Loading Software" : "Loading Machine", u32(100 * m_romsloadedsize / m_romstotalsize));
	else
		buffer = "Loading Complete";

	if (!machine().ui().is_menu_active())
		machine().ui().set_startup_text(buffer.c_str(), false);
}


/*-------------------------------------------------
    display_rom_load_results - display the final
    results of ROM loading
-------------------------------------------------*/

void rom_load_manager::display_rom_load_results(bool from_list)
{
	/* final status display */
	display_loading_rom_message(nullptr, from_list);

	/* if we had errors, they are fatal */
	if (m_errors != 0)
	{
		/* create the error message and exit fatally */
		osd_printf_error("%s", m_errorstring);
		throw emu_fatalerror(EMU_ERR_MISSING_FILES, "Required files are missing, the machine cannot be run.");
	}

	/* if we had warnings, output them, but continue */
	if ((m_warnings) || (m_knownbad))
	{
		m_errorstring.append("WARNING: the machine might not run correctly.");
		osd_printf_warning("%s\n", m_errorstring);
	}
}


/*-------------------------------------------------
    region_post_process - post-process a region,
    byte swapping and inverting data as necessary
-------------------------------------------------*/

void rom_load_manager::region_post_process(memory_region *region, bool invert)
{
	// do nothing if no region
	if (region == nullptr)
		return;

	LOG("+ datawidth=%dbit endian=%s\n", region->bitwidth(),
			region->endianness() == ENDIANNESS_LITTLE ? "little" : "big");

	/* if the region is inverted, do that now */
	if (invert)
	{
		LOG("+ Inverting region\n");
		u8 *base = region->base();
		for (int i = 0; i < region->bytes(); i++)
			*base++ ^= 0xff;
	}

	/* swap the endianness if we need to */
	if (region->bytewidth() > 1 && region->endianness() != ENDIANNESS_NATIVE)
	{
		LOG("+ Byte swapping region\n");
		int datawidth = region->bytewidth();
		u8 *base = region->base();
		for (int i = 0; i < region->bytes(); i += datawidth)
		{
			u8 temp[8];
			memcpy(temp, base, datawidth);
			for (int j = datawidth - 1; j >= 0; j--)
				*base++ = temp[j];
		}
	}
}


/*-------------------------------------------------
    open_rom_file - open a ROM file, searching
    up the parent and loading by checksum
-------------------------------------------------*/

std::unique_ptr<emu_file> rom_load_manager::open_rom_file(std::initializer_list<std::reference_wrapper<const std::vector<std::string> > > searchpath, const rom_entry *romp, std::vector<std::string> &tried_file_names, bool from_list)
{
	std::error_condition filerr = std::errc::no_such_file_or_directory;
	u32 const romsize = rom_file_size(romp);
	tried_file_names.clear();

	// update status display
	display_loading_rom_message(ROM_GETNAME(romp), from_list);

	// extract CRC to use for searching
	u32 crc = 0;
	bool const has_crc = util::hash_collection(romp->hashdata()).crc(crc);

	// attempt reading up the chain through the parents
	// it also automatically attempts any kind of load by checksum supported by the archives.
	std::unique_ptr<emu_file> result;
	for (const std::vector<std::string> &paths : searchpath)
	{
		result = open_rom_file(paths, tried_file_names, has_crc, crc, ROM_GETNAME(romp), filerr);
		if (result)
			break;
	}

	// update counters
	m_romsloaded++;
	m_romsloadedsize += romsize;

	// return the result
	if (filerr)
		return nullptr;
	else
		return result;
}


std::unique_ptr<emu_file> rom_load_manager::open_rom_file(const std::vector<std::string> &paths, std::vector<std::string> &tried, bool has_crc, u32 crc, std::string_view name, std::error_condition &filerr)
{
	// record the set names we search
	tried.insert(tried.end(), paths.begin(), paths.end());

	// attempt to open the file
	std::unique_ptr<emu_file> result(new emu_file(machine().options().media_path(), paths, OPEN_FLAG_READ));
	result->set_restrict_to_mediapath(1);
	if (has_crc)
		filerr = result->open(name, crc);
	else
		filerr = result->open(name);

	// don't return anything if unsuccessful
	if (filerr)
		return nullptr;
	else
		return result;
}


/*-------------------------------------------------
    rom_fread - cheesy fread that fills with
    random data for a nullptr file
-------------------------------------------------*/

int rom_load_manager::rom_fread(emu_file *file, u8 *buffer, int length, const rom_entry *parent_region)
{
	if (file) // files just pass through
		return file->read(buffer, length);

	if (!ROMREGION_ISERASE(parent_region)) // otherwise, fill with randomness unless it was already specifically erased
		fill_random(buffer, length);

	return length;
}


/*-------------------------------------------------
    read_rom_data - read ROM data for a single
    entry
-------------------------------------------------*/

int rom_load_manager::read_rom_data(emu_file *file, const rom_entry *parent_region, const rom_entry *romp)
{
	int datashift = ROM_GETBITSHIFT(romp);
	int datamask = ((1 << ROM_GETBITWIDTH(romp)) - 1) << datashift;
	int numbytes = ROM_GETLENGTH(romp);
	int groupsize = ROM_GETGROUPSIZE(romp);
	int skip = ROM_GETSKIPCOUNT(romp);
	int reversed = ROM_ISREVERSED(romp);
	int numgroups = (numbytes + groupsize - 1) / groupsize;
	u8 *base = m_region->base() + ROM_GETOFFSET(romp);
	u32 tempbufsize;
	int i;

	LOG("Loading ROM data: offs=%X len=%X mask=%02X group=%d skip=%d reverse=%d\n", ROM_GETOFFSET(romp), numbytes, datamask, groupsize, skip, reversed);

	/* make sure the length was an even multiple of the group size */
	if (numbytes % groupsize != 0)
		osd_printf_warning("Warning in RomModule definition: %s length not an even multiple of group size\n", romp->name());

	/* make sure we only fill within the region space */
	if (ROM_GETOFFSET(romp) + numgroups * groupsize + (numgroups - 1) * skip > m_region->bytes())
		throw emu_fatalerror("Error in RomModule definition: %s out of memory region space\n", romp->name());

	/* make sure the length was valid */
	if (numbytes == 0)
		throw emu_fatalerror("Error in RomModule definition: %s has an invalid length\n", romp->name());

	/* special case for simple loads */
	if (datamask == 0xff && (groupsize == 1 || !reversed) && skip == 0)
		return rom_fread(file, base, numbytes, parent_region);

	/* use a temporary buffer for complex loads */
	tempbufsize = std::min(TEMPBUFFER_MAX_SIZE, numbytes);
	std::vector<u8> tempbuf(tempbufsize);

	/* chunky reads for complex loads */
	skip += groupsize;
	while (numbytes > 0)
	{
		int evengroupcount = (tempbufsize / groupsize) * groupsize;
		int bytesleft = (numbytes > evengroupcount) ? evengroupcount : numbytes;
		u8 *bufptr = &tempbuf[0];

		/* read as much as we can */
		LOG("  Reading %X bytes into buffer\n", bytesleft);
		if (rom_fread(file, bufptr, bytesleft, parent_region) != bytesleft)
			return 0;
		numbytes -= bytesleft;

		LOG("  Copying to %p\n", base);

		/* unmasked cases */
		if (datamask == 0xff)
		{
			/* non-grouped data */
			if (groupsize == 1)
				for (i = 0; i < bytesleft; i++, base += skip)
					*base = *bufptr++;

			/* grouped data -- non-reversed case */
			else if (!reversed)
				while (bytesleft)
				{
					for (i = 0; i < groupsize && bytesleft; i++, bytesleft--)
						base[i] = *bufptr++;
					base += skip;
				}

			/* grouped data -- reversed case */
			else
				while (bytesleft)
				{
					for (i = groupsize - 1; i >= 0 && bytesleft; i--, bytesleft--)
						base[i] = *bufptr++;
					base += skip;
				}
		}

		/* masked cases */
		else
		{
			/* non-grouped data */
			if (groupsize == 1)
				for (i = 0; i < bytesleft; i++, base += skip)
					*base = (*base & ~datamask) | ((*bufptr++ << datashift) & datamask);

			/* grouped data -- non-reversed case */
			else if (!reversed)
				while (bytesleft)
				{
					for (i = 0; i < groupsize && bytesleft; i++, bytesleft--)
						base[i] = (base[i] & ~datamask) | ((*bufptr++ << datashift) & datamask);
					base += skip;
				}

			/* grouped data -- reversed case */
			else
				while (bytesleft)
				{
					for (i = groupsize - 1; i >= 0 && bytesleft; i--, bytesleft--)
						base[i] = (base[i] & ~datamask) | ((*bufptr++ << datashift) & datamask);
					base += skip;
				}
		}
	}

	LOG("  All done\n");
	return ROM_GETLENGTH(romp);
}


/*-------------------------------------------------
    fill_rom_data - fill a region of ROM space
-------------------------------------------------*/

void rom_load_manager::fill_rom_data(const rom_entry *romp)
{
	u32 numbytes = ROM_GETLENGTH(romp);
	int skip = ROM_GETSKIPCOUNT(romp);
	u8 *base = m_region->base() + ROM_GETOFFSET(romp);

	// make sure we fill within the region space
	if (ROM_GETOFFSET(romp) + numbytes > m_region->bytes())
		throw emu_fatalerror("Error in RomModule definition: FILL out of memory region space\n");

	// make sure the length was valid
	if (numbytes == 0)
		throw emu_fatalerror("Error in RomModule definition: FILL has an invalid length\n");

	// for fill bytes, the byte that gets filled is the first byte of the hashdata string
	u8 fill_byte = u8(strtol(romp->hashdata().c_str(), nullptr, 0));

	// fill the data (filling value is stored in place of the hashdata)
	if(skip != 0)
	{
		for (int i = 0; i < numbytes; i+= skip + 1)
			base[i] = fill_byte;
	}
	else
		memset(base, fill_byte, numbytes);
}


/*-------------------------------------------------
    copy_rom_data - copy a region of ROM space
-------------------------------------------------*/

void rom_load_manager::copy_rom_data(const rom_entry *romp)
{
	u8 *base = m_region->base() + ROM_GETOFFSET(romp);
	const std::string &srcrgntag = romp->name();
	u32 numbytes = ROM_GETLENGTH(romp);
	u32 srcoffs = u32(strtol(romp->hashdata().c_str(), nullptr, 0));  /* srcoffset in place of hashdata */

	/* make sure we copy within the region space */
	if (ROM_GETOFFSET(romp) + numbytes > m_region->bytes())
		throw emu_fatalerror("Error in RomModule definition: COPY out of target memory region space\n");

	/* make sure the length was valid */
	if (numbytes == 0)
		throw emu_fatalerror("Error in RomModule definition: COPY has an invalid length\n");

	/* make sure the source was valid */
	memory_region *region = machine().root_device().memregion(srcrgntag);
	if (region == nullptr)
		throw emu_fatalerror("Error in RomModule definition: COPY from an invalid region\n");

	/* make sure we find within the region space */
	if (srcoffs + numbytes > region->bytes())
		throw emu_fatalerror("Error in RomModule definition: COPY out of source memory region space\n");

	/* fill the data */
	memcpy(base, region->base() + srcoffs, numbytes);
}


/*-------------------------------------------------
    process_rom_entries - process all ROM entries
    for a region
-------------------------------------------------*/

void rom_load_manager::process_rom_entries(std::initializer_list<std::reference_wrapper<const std::vector<std::string> > > searchpath, u8 bios, const rom_entry *parent_region, const rom_entry *romp, bool from_list)
{
	u32 lastflags = 0;
	std::vector<std::string> tried_file_names;

	// loop until we hit the end of this region
	while (!ROMENTRY_ISREGIONEND(romp))
	{
		tried_file_names.clear();

		if (ROMENTRY_ISCONTINUE(romp))
			throw emu_fatalerror("Error in RomModule definition: ROM_CONTINUE not preceded by ROM_LOAD\n");

		if (ROMENTRY_ISIGNORE(romp))
			throw emu_fatalerror("Error in RomModule definition: ROM_IGNORE not preceded by ROM_LOAD\n");

		if (ROMENTRY_ISRELOAD(romp))
			throw emu_fatalerror("Error in RomModule definition: ROM_RELOAD not preceded by ROM_LOAD\n");

		if (ROMENTRY_ISFILL(romp))
		{
			if (!ROM_GETBIOSFLAGS(romp) || (ROM_GETBIOSFLAGS(romp) == bios))
				fill_rom_data(romp);

			romp++;
		}
		else if (ROMENTRY_ISCOPY(romp))
		{
			copy_rom_data(romp++);
		}
		else if (ROMENTRY_ISFILE(romp))
		{
			// handle files
			bool const irrelevantbios = (ROM_GETBIOSFLAGS(romp) != 0) && (ROM_GETBIOSFLAGS(romp) != bios);
			rom_entry const *baserom = romp;
			int explength = 0;

			// open the file if it is a non-BIOS or matches the current BIOS
			LOG("Opening ROM file: %s\n", ROM_GETNAME(romp));
			std::unique_ptr<emu_file> file;
			if (!irrelevantbios)
			{
				file = open_rom_file(searchpath, romp, tried_file_names, from_list);
				if (!file)
					handle_missing_file(romp, tried_file_names, std::error_condition());
			}

			// loop until we run out of reloads
			do
			{
				// loop until we run out of continues/ignores
				do
				{
					rom_entry modified_romp = *romp++;
					//int readresult;

					// handle flag inheritance
					if (!ROM_INHERITSFLAGS(&modified_romp))
						lastflags = modified_romp.get_flags();
					else
						modified_romp.set_flags((modified_romp.get_flags() & ~ROM_INHERITEDFLAGS) | lastflags);

					explength += ROM_GETLENGTH(&modified_romp);

					// attempt to read using the modified entry
					if (!ROMENTRY_ISIGNORE(&modified_romp) && !irrelevantbios)
						/*readresult = */read_rom_data(file.get(), parent_region, &modified_romp);
				}
				while (ROMENTRY_ISCONTINUE(romp) || ROMENTRY_ISIGNORE(romp));

				// if this was the first use of this file, verify the length and CRC
				if (baserom)
				{
					LOG("Verifying length (%X) and checksums\n", explength);
					verify_length_and_hash(file.get(), baserom->name(), explength, util::hash_collection(baserom->hashdata()));
					LOG("Verify finished\n");
				}

				// re-seek to the start and clear the baserom so we don't reverify
				if (file)
					file->seek(0, SEEK_SET);
				baserom = nullptr;
				explength = 0;
			}
			while (ROMENTRY_ISRELOAD(romp));

			// close the file
			if (file)
			{
				LOG("Closing ROM file\n");
				file.reset();
			}
		}
		else
		{
			romp++; // something else - skip
		}
	}
}


/*-------------------------------------------------
    open_disk_diff - open a DISK diff file
-------------------------------------------------*/

std::error_condition rom_load_manager::open_disk_diff(emu_options &options, const rom_entry *romp, chd_file &source, chd_file &diff_chd)
{
	// TODO: use system name and/or software list name in the path - the current setup doesn't scale
	std::string fname = romp->name() + ".dif";

	// try to open the diff
	LOG("Opening differencing image file: %s\n", fname.c_str());
	emu_file diff_file(options.diff_directory(), OPEN_FLAG_READ | OPEN_FLAG_WRITE);
	std::error_condition filerr = diff_file.open(fname);
	if (!filerr)
	{
		std::string fullpath(diff_file.fullpath());
		diff_file.close();

		LOG("Opening differencing image file: %s\n", fullpath.c_str());
		return diff_chd.open(fullpath, true, &source);
	}

	// didn't work; try creating it instead
	LOG("Creating differencing image: %s\n", fname.c_str());
	diff_file.set_openflags(OPEN_FLAG_READ | OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS);
	filerr = diff_file.open(fname);
	if (!filerr)
	{
		std::string fullpath(diff_file.fullpath());
		diff_file.close();

		// create the CHD
		LOG("Creating differencing image file: %s\n", fullpath.c_str());
		chd_codec_type compression[4] = { CHD_CODEC_NONE };
		std::error_condition err = diff_chd.create(fullpath, source.logical_bytes(), source.hunk_bytes(), compression, source);
		if (err)
			return err;

		return diff_chd.clone_all_metadata(source);
	}

	return std::errc::no_such_file_or_directory;
}


/*-------------------------------------------------
    process_disk_entries - process all disk entries
    for a region
-------------------------------------------------*/

void rom_load_manager::process_disk_entries(std::initializer_list<std::reference_wrapper<const std::vector<std::string> > > searchpath, std::string_view regiontag, const rom_entry *romp, std::function<const rom_entry * ()> next_parent)
{
	/* remove existing disk entries for this region */
	m_chd_list.erase(std::remove_if(m_chd_list.begin(), m_chd_list.end(),
			[regiontag] (std::unique_ptr<open_chd> &chd) { return chd->region() == regiontag; }), m_chd_list.end());

	/* loop until we hit the end of this region */
	for ( ; !ROMENTRY_ISREGIONEND(romp); romp++)
	{
		/* handle files */
		if (ROMENTRY_ISFILE(romp))
		{
			auto chd = std::make_unique<open_chd>(regiontag);
			std::error_condition err;

			/* make the filename of the source */
			const std::string filename = romp->name() + ".chd";

			/* first open the source drive */
			// FIXME: we've lost the ability to search parents here
			LOG("Opening disk image: %s\n", filename.c_str());
			err = do_open_disk(machine().options(), searchpath, romp, chd->orig_chd(), next_parent);
			if (err)
			{
				handle_missing_file(romp, std::vector<std::string>(), err);
				chd = nullptr;
				continue;
			}

			/* get the header and extract the SHA1 */
			util::hash_collection acthashes;
			acthashes.add_sha1(chd->orig_chd().sha1());

			/* verify the hash */
			const util::hash_collection hashes(romp->hashdata());
			if (hashes != acthashes)
			{
				m_errorstring.append(string_format("%s WRONG CHECKSUMS:\n", filename));
				dump_wrong_and_correct_checksums(hashes, acthashes);
				m_warnings++;
			}
			else if (hashes.flag(util::hash_collection::FLAG_BAD_DUMP))
			{
				m_errorstring.append(string_format("%s CHD NEEDS REDUMP\n", filename));
				m_knownbad++;
			}

			/* if not read-only, make the diff file */
			if (!DISK_ISREADONLY(romp))
			{
				/* try to open or create the diff */
				err = open_disk_diff(machine().options(), romp, chd->orig_chd(), chd->diff_chd());
				if (err)
				{
					m_errorstring.append(string_format("%s DIFF CHD ERROR: %s\n", filename, err.message()));
					m_errors++;
					chd = nullptr;
					continue;
				}
			}

			/* we're okay, add to the list of disks */
			LOG("Assigning to handle %d\n", DISK_GETINDEX(romp));
			m_chd_list.push_back(std::move(chd));
		}
	}
}


/*-------------------------------------------------
    get_software_searchpath - get search path
    for a software list item
-------------------------------------------------*/

std::vector<std::string> rom_load_manager::get_software_searchpath(software_list_device &swlist, const software_info &swinfo)
{
	std::vector<software_info const *> parents;
	return make_software_searchpath(swlist, swinfo, parents);
}


/*-------------------------------------------------
    open_disk_image - open a disk image for a
    device
-------------------------------------------------*/

std::error_condition rom_load_manager::open_disk_image(const emu_options &options, const device_t &device, const rom_entry *romp, chd_file &image_chd)
{
	const std::vector<std::string> searchpath(device.searchpath());

	driver_device const *const driver(dynamic_cast<driver_device const *>(&device));
	std::function<const rom_entry * ()> next_parent;
	if (driver)
		next_parent = next_parent_system(driver->system());
	else
		next_parent = [] () { return nullptr; };
	return do_open_disk(options, { searchpath }, romp, image_chd, std::move(next_parent));
}


/*-------------------------------------------------
    open_disk_image - open a disk image for a
    software item
-------------------------------------------------*/

std::error_condition rom_load_manager::open_disk_image(const emu_options &options, software_list_device &swlist, const software_info &swinfo, const rom_entry *romp, chd_file &image_chd)
{
	std::vector<software_info const *> parents;
	std::vector<std::string> searchpath = make_software_searchpath(swlist, swinfo, parents);
	searchpath.emplace_back(swlist.list_name()); // look for loose disk images in software list directory
	return do_open_disk(options, { searchpath }, romp, image_chd, next_parent_software(parents));
}


/*-------------------------------------------------
    normalize_flags_for_device - modify the region
    flags for the given device
-------------------------------------------------*/

void rom_load_manager::normalize_flags_for_device(std::string_view rgntag, u8 &width, endianness_t &endian)
{
	device_t *device = machine().root_device().subdevice(rgntag);
	device_memory_interface *memory;
	if (device != nullptr && device->interface(memory))
	{
		const address_space_config *spaceconfig = memory->space_config();
		if (spaceconfig != nullptr)
		{
			int buswidth;

			/* set the endianness */
			if (spaceconfig->endianness() == ENDIANNESS_LITTLE)
				endian = ENDIANNESS_LITTLE;
			else
				endian = ENDIANNESS_BIG;

			/* set the width */
			buswidth = spaceconfig->data_width();
			if (buswidth <= 8)
				width = 1;
			else if (buswidth <= 16)
				width = 2;
			else if (buswidth <= 32)
				width = 4;
			else
				width = 8;
		}
	}
}


/*-------------------------------------------------
    load_software_part_region - load a software part

    This is used by MESS when loading a piece of
    software. The code should be merged with
    process_region_list or updated to use a slight
    more general process_region_list.
-------------------------------------------------*/

void rom_load_manager::load_software_part_region(device_t &device, software_list_device &swlist, std::string_view swname, const rom_entry *start_region)
{
	m_errorstring.clear();
	m_softwarningstring.clear();

	m_romstotal = 0;
	m_romstotalsize = 0;
	m_romsloadedsize = 0;

	std::vector<const software_info *> parents;
	std::vector<std::string> swsearch, disksearch, devsearch;
	const software_info *const swinfo = swlist.find(std::string(swname));
	if (swinfo)
	{
		// dispay a warning for unsupported software
		// TODO: list supported clones like we do for machines?
		if (swinfo->supported() == software_support::PARTIALLY_SUPPORTED)
		{
			m_errorstring.append(string_format("WARNING: support for software %s (in list %s) is only partial\n", swname, swlist.list_name()));
			m_softwarningstring.append(string_format("Support for software %s (in list %s) is only partial\n", swname, swlist.list_name()));
		}
		else if (swinfo->supported() == software_support::UNSUPPORTED)
		{
			m_errorstring.append(string_format("WARNING: support for software %s (in list %s) is only preliminary\n", swname, swlist.list_name()));
			m_softwarningstring.append(string_format("Support for software %s (in list %s) is only preliminary\n", swname, swlist.list_name()));
		}

		// walk the chain of parents and add them to the search path
		swsearch = make_software_searchpath(swlist, *swinfo, parents);
	}
	else
	{
		swsearch.emplace_back(util::string_format("%s" PATH_SEPARATOR "%s", swlist.list_name(), swname));
		swsearch.emplace_back(swname);
	}

	// this is convenient for CD-only lists so you don't need an extra level of directories containing one file each
	disksearch.emplace_back(swlist.list_name());

	// for historical reasons, add the search path for the software list device's owner
	const device_t *const listowner = swlist.owner();
	if (listowner)
		devsearch = listowner->searchpath();

	// loop until we hit the end
	std::function<const rom_entry * ()> next_parent;
	for (const rom_entry *region = start_region; region != nullptr; region = rom_next_region(region))
	{
		u32 regionlength = ROMREGION_GETLENGTH(region);

		std::string regiontag = device.subtag(region->name());
		LOG("Processing region \"%s\" (length=%X)\n", regiontag.c_str(), regionlength);

		// the first entry must be a region
		assert(ROMENTRY_ISREGION(region));

		// if this is a device region, override with the device width and endianness
		endianness_t endianness = ROMREGION_ISBIGENDIAN(region) ? ENDIANNESS_BIG : ENDIANNESS_LITTLE;
		u8 width = ROMREGION_GETWIDTH(region) / 8;
		memory_region *memregion = machine().root_device().memregion(regiontag);
		if (memregion != nullptr)
		{
			normalize_flags_for_device(regiontag, width, endianness);

			// clear old region (TODO: should be moved to an image unload function)
			machine().memory().region_free(memregion->name());
		}

		// remember the base and length
		m_region = machine().memory().region_alloc(regiontag, regionlength, width, endianness);
		LOG("Allocated %X bytes @ %p\n", m_region->bytes(), m_region->base());

		if (ROMREGION_ISERASE(region)) // clear the region if it's requested
			memset(m_region->base(), ROMREGION_GETERASEVAL(region), m_region->bytes());
		else if (m_region->bytes() <= 0x400000) // or if it's sufficiently small (<= 4MB)
			memset(m_region->base(), 0, m_region->bytes());
#ifdef MAME_DEBUG
		else // if we're debugging, fill region with random data to catch errors
			fill_random(m_region->base(), m_region->bytes());
#endif

		// update total number of roms
		for (const rom_entry *rom = rom_first_file(region); rom != nullptr; rom = rom_next_file(rom))
		{
			m_romstotal++;
			m_romstotalsize += rom_file_size(rom);
		}

		// now process the entries in the region
		if (ROMREGION_ISROMDATA(region))
		{
			if (devsearch.empty())
				process_rom_entries({ swsearch }, 0U, region, region + 1, true);
			else
				process_rom_entries({ swsearch, devsearch }, 0U, region, region + 1, true);
		}
		else if (ROMREGION_ISDISKDATA(region))
		{
			if (!next_parent)
			{
				if (!parents.empty())
					next_parent = next_parent_software(parents);
				else
					next_parent = [] () { return nullptr; };
			}
			if (devsearch.empty())
				process_disk_entries({ swsearch, disksearch }, regiontag, region + 1, next_parent);
			else
				process_disk_entries({ swsearch, disksearch, devsearch }, regiontag, region + 1, next_parent);
		}
	}

	// now go back and post-process all the regions
	for (const rom_entry *region = start_region; region != nullptr; region = rom_next_region(region))
		region_post_process(device.memregion(region->name()), ROMREGION_ISINVERTED(region));

	// display the results and exit
	display_rom_load_results(true);
}


/*-------------------------------------------------
    process_region_list - process a region list
-------------------------------------------------*/

void rom_load_manager::process_region_list()
{
	// loop until we hit the end
	device_enumerator deviter(machine().root_device());
	std::vector<std::string> searchpath;
	for (device_t &device : deviter)
	{
		searchpath.clear();
		std::function<const rom_entry * ()> next_parent;
		for (const rom_entry *region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
		{
			u32 regionlength = ROMREGION_GETLENGTH(region);

			std::string regiontag = device.subtag(region->name());
			LOG("Processing region \"%s\" (length=%X)\n", regiontag.c_str(), regionlength);

			// the first entry must be a region
			assert(ROMENTRY_ISREGION(region));

			if (ROMREGION_ISROMDATA(region))
			{
				// if this is a device region, override with the device width and endianness
				u8 width = ROMREGION_GETWIDTH(region) / 8;
				endianness_t endianness = ROMREGION_ISBIGENDIAN(region) ? ENDIANNESS_BIG : ENDIANNESS_LITTLE;
				normalize_flags_for_device(regiontag, width, endianness);

				// remember the base and length
				m_region = machine().memory().region_alloc(regiontag, regionlength, width, endianness);
				LOG("Allocated %X bytes @ %p\n", m_region->bytes(), m_region->base());

				if (ROMREGION_ISERASE(region)) // clear the region if it's requested
					memset(m_region->base(), ROMREGION_GETERASEVAL(region), m_region->bytes());
				else if (m_region->bytes() <= 0x400000) // or if it's sufficiently small (<= 4MB)
					memset(m_region->base(), 0, m_region->bytes());
#ifdef MAME_DEBUG
				else // if we're debugging, fill region with random data to catch errors
					fill_random(m_region->base(), m_region->bytes());
#endif

				// now process the entries in the region
				if (searchpath.empty())
					searchpath = device.searchpath();
				assert(!searchpath.empty());
				process_rom_entries({ searchpath }, device.system_bios(), region, region + 1, false);
			}
			else if (ROMREGION_ISDISKDATA(region))
			{
				if (searchpath.empty())
					searchpath = device.searchpath();
				assert(!searchpath.empty());
				if (!next_parent)
				{
					driver_device const *const driver(dynamic_cast<driver_device const *>(&device));
					if (driver)
						next_parent = next_parent_system(driver->system());
					else
						next_parent = [] () { return nullptr; };
				}
				process_disk_entries({ searchpath }, regiontag, region + 1, next_parent);
			}
		}
	}

	// now go back and post-process all the regions
	for (device_t &device : deviter)
		for (const rom_entry *region = rom_first_region(device); region != nullptr; region = rom_next_region(region))
			region_post_process(device.memregion(region->name()), ROMREGION_ISINVERTED(region));

	// and finally register all per-game parameters
	for (device_t &device : deviter)
	{
		for (const rom_entry *param = rom_first_parameter(device); param != nullptr; param = rom_next_parameter(param))
		{
			std::string regiontag = device.subtag(param->name());
			machine().parameters().add(regiontag, param->hashdata());
		}
	}
}


/*-------------------------------------------------
    rom_init - load the ROMs and open the disk
    images associated with the given machine
-------------------------------------------------*/

rom_load_manager::rom_load_manager(running_machine &machine)
	: m_machine(machine)
	, m_warnings(0)
	, m_knownbad(0)
	, m_errors(0)
	, m_romsloaded(0)
	, m_romstotal(0)
	, m_romsloadedsize(0)
	, m_romstotalsize(0)
	, m_chd_list()
	, m_region(nullptr)
	, m_errorstring()
	, m_softwarningstring()
{
	// figure out which BIOS we are using
	std::map<std::string_view, std::string> card_bios;
	for (device_t &device : device_enumerator(machine.config().root_device()))
	{
		device_slot_interface const *const slot(dynamic_cast<device_slot_interface *>(&device));
		if (slot)
		{
			device_t const *const card(slot->get_card_device());
			slot_option const &slot_opt(machine.options().slot_option(slot->slot_name()));
			if (card && !slot_opt.bios().empty())
				card_bios.emplace(card->tag(), slot_opt.bios());
		}

		if (device.rom_region())
		{
			std::string specbios;
			if (!device.owner())
			{
				specbios = machine.options().bios();
			}
			else
			{
				auto const found(card_bios.find(device.tag()));
				if (card_bios.end() != found)
				{
					specbios = std::move(found->second);
					card_bios.erase(found);
				}
			}
			determine_bios_rom(device, specbios.c_str());
		}
	}

	// count the total number of ROMs
	count_roms();

	// reset the disk list
	m_chd_list.clear();

	// process the ROM entries we were passed
	process_region_list();

	// display the results and exit
	display_rom_load_results(false);
}


// -------------------------------------------------
// rom_build_entries - builds a rom_entry vector
// from a tiny_rom_entry array
// -------------------------------------------------

std::vector<rom_entry> rom_build_entries(const tiny_rom_entry *tinyentries)
{
	std::vector<rom_entry> result;
	if (tinyentries)
	{
		int i = 0;
		do
		{
			result.emplace_back(tinyentries[i]);
		}
		while (!ROMENTRY_ISEND(tinyentries[i++]));
	}
	else
	{
		tiny_rom_entry const end_entry = { nullptr, nullptr, 0, 0, ROMENTRYTYPE_END };
		result.emplace_back(end_entry);
	}
	return result;
}
