// license:BSD-3-Clause
// copyright-holders:Aaron Giles
/***************************************************************************

    ioport.cpp

    Input/output port handling.

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

#include "emu.h"

#include "config.h"
#include "emuopts.h"
#include "fileio.h"
#include "inputdev.h"
#include "main.h"
#include "natkeyboard.h"
#include "profiler.h"

#include "ui/uimain.h"

#include "util/corestr.h"
#include "util/ioprocsfilter.h"
#include "util/language.h"
#include "util/unicode.h"
#include "util/xmlfile.h"

#include "osdepend.h"

#include <algorithm>
#include <cctype>
#include <ctime>


namespace {

// temporary: set this to 1 to enable the originally defined behavior that
// a field specified via PORT_MODIFY which intersects a previously-defined
// field completely wipes out the previous definition
#define INPUT_PORT_OVERRIDE_FULLY_NUKES_PREVIOUS    1


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

const int SPACE_COUNT = 3;



//**************************************************************************
//  INLINE FUNCTIONS
//**************************************************************************

//-------------------------------------------------
//  compute_scale -- compute an 8.24 scale value
//  from a numerator and a denominator
//-------------------------------------------------

inline s64 compute_scale(s32 num, s32 den)
{
	return (s64(num) << 24) / den;
}


//-------------------------------------------------
//  recip_scale -- compute an 8.24 reciprocal of
//  an 8.24 scale value
//-------------------------------------------------

inline s64 recip_scale(s64 scale)
{
	return (s64(1) << 48) / scale;
}


//-------------------------------------------------
//  apply_scale -- apply an 8.24 scale value to
//  a 32-bit value
//-------------------------------------------------

inline s32 apply_scale(s32 value, s64 scale)
{
	return (s64(value) * scale) / (1 << 24);
}

//-------------------------------------------------
//  compute_shift -- get shift required to right-
//  align an I/O port field value
//-------------------------------------------------

inline u8 compute_shift(ioport_value mask)
{
	u8 result = 0U;
	while (mask && !BIT(mask, 0))
	{
		mask >>= 1;
		++result;
	}
	return result;
}



//**************************************************************************
//  COMMON SHARED STRINGS
//**************************************************************************

const struct
{
	u32 id;
	const char *string;
} input_port_default_strings[] =
{
	{ INPUT_STRING_Off, "Off" },
	{ INPUT_STRING_On, "On" },
	{ INPUT_STRING_No, "No" },
	{ INPUT_STRING_Yes, "Yes" },
	{ INPUT_STRING_Lives, "Lives" },
	{ INPUT_STRING_Bonus_Life, "Bonus Life" },
	{ INPUT_STRING_Difficulty, "Difficulty" },
	{ INPUT_STRING_Demo_Sounds, "Demo Sounds" },
	{ INPUT_STRING_Coinage, "Coinage" },
	{ INPUT_STRING_Coin_A, "Coin A" },
	{ INPUT_STRING_Coin_B, "Coin B" },
	{ INPUT_STRING_9C_1C, "9 Coins/1 Credit" },
	{ INPUT_STRING_8C_1C, "8 Coins/1 Credit" },
	{ INPUT_STRING_7C_1C, "7 Coins/1 Credit" },
	{ INPUT_STRING_6C_1C, "6 Coins/1 Credit" },
	{ INPUT_STRING_5C_1C, "5 Coins/1 Credit" },
	{ INPUT_STRING_4C_1C, "4 Coins/1 Credit" },
	{ INPUT_STRING_3C_1C, "3 Coins/1 Credit" },
	{ INPUT_STRING_8C_3C, "8 Coins/3 Credits" },
	{ INPUT_STRING_4C_2C, "4 Coins/2 Credits" },
	{ INPUT_STRING_5C_2C, "5 Coins/2 Credits" },
	{ INPUT_STRING_2C_1C, "2 Coins/1 Credit" },
	{ INPUT_STRING_5C_3C, "5 Coins/3 Credits" },
	{ INPUT_STRING_3C_2C, "3 Coins/2 Credits" },
	{ INPUT_STRING_4C_3C, "4 Coins/3 Credits" },
	{ INPUT_STRING_4C_4C, "4 Coins/4 Credits" },
	{ INPUT_STRING_3C_3C, "3 Coins/3 Credits" },
	{ INPUT_STRING_2C_2C, "2 Coins/2 Credits" },
	{ INPUT_STRING_1C_1C, "1 Coin/1 Credit" },
	{ INPUT_STRING_3C_5C, "3 Coins/5 Credits" },
	{ INPUT_STRING_4C_5C, "4 Coins/5 Credits" },
	{ INPUT_STRING_3C_4C, "3 Coins/4 Credits" },
	{ INPUT_STRING_2C_3C, "2 Coins/3 Credits" },
	{ INPUT_STRING_4C_7C, "4 Coins/7 Credits" },
	{ INPUT_STRING_2C_4C, "2 Coins/4 Credits" },
	{ INPUT_STRING_1C_2C, "1 Coin/2 Credits" },
	{ INPUT_STRING_2C_5C, "2 Coins/5 Credits" },
	{ INPUT_STRING_2C_6C, "2 Coins/6 Credits" },
	{ INPUT_STRING_1C_3C, "1 Coin/3 Credits" },
	{ INPUT_STRING_2C_7C, "2 Coins/7 Credits" },
	{ INPUT_STRING_2C_8C, "2 Coins/8 Credits" },
	{ INPUT_STRING_1C_4C, "1 Coin/4 Credits" },
	{ INPUT_STRING_1C_5C, "1 Coin/5 Credits" },
	{ INPUT_STRING_1C_6C, "1 Coin/6 Credits" },
	{ INPUT_STRING_1C_7C, "1 Coin/7 Credits" },
	{ INPUT_STRING_1C_8C, "1 Coin/8 Credits" },
	{ INPUT_STRING_1C_9C, "1 Coin/9 Credits" },
	{ INPUT_STRING_Free_Play, "Free Play" },
	{ INPUT_STRING_Cabinet, "Cabinet" },
	{ INPUT_STRING_Upright, "Upright" },
	{ INPUT_STRING_Cocktail, "Cocktail" },
	{ INPUT_STRING_Flip_Screen, "Flip Screen" },
	{ INPUT_STRING_Service_Mode, "Service Mode" },
	{ INPUT_STRING_Pause, "Pause" },
	{ INPUT_STRING_Test, "Test" },
	{ INPUT_STRING_Tilt, "Tilt" },
	{ INPUT_STRING_Version, "Version" },
	{ INPUT_STRING_Region, "Region" },
	{ INPUT_STRING_International, "International" },
	{ INPUT_STRING_Japan, "Japan" },
	{ INPUT_STRING_USA, "USA" },
	{ INPUT_STRING_Europe, "Europe" },
	{ INPUT_STRING_Asia, "Asia" },
	{ INPUT_STRING_China, "China" },
	{ INPUT_STRING_Hong_Kong, "Hong Kong" },
	{ INPUT_STRING_Korea, "Korea" },
	{ INPUT_STRING_Southeast_Asia, "Southeast Asia" },
	{ INPUT_STRING_Taiwan, "Taiwan" },
	{ INPUT_STRING_World, "World" },
	{ INPUT_STRING_Language, "Language" },
	{ INPUT_STRING_English, "English" },
	{ INPUT_STRING_Japanese, "Japanese" },
	{ INPUT_STRING_Chinese, "Chinese" },
	{ INPUT_STRING_French, "French" },
	{ INPUT_STRING_German, "German" },
	{ INPUT_STRING_Italian, "Italian" },
	{ INPUT_STRING_Korean, "Korean" },
	{ INPUT_STRING_Spanish, "Spanish" },
	{ INPUT_STRING_Very_Easy, "Very Easy" },
	{ INPUT_STRING_Easiest, "Easiest" },
	{ INPUT_STRING_Easier, "Easier" },
	{ INPUT_STRING_Easy, "Easy" },
	{ INPUT_STRING_Medium_Easy, "Medium Easy" },
	{ INPUT_STRING_Normal, "Normal" },
	{ INPUT_STRING_Medium, "Medium" },
	{ INPUT_STRING_Medium_Hard, "Medium Hard" },
	{ INPUT_STRING_Hard, "Hard" },
	{ INPUT_STRING_Harder, "Harder" },
	{ INPUT_STRING_Hardest, "Hardest" },
	{ INPUT_STRING_Very_Hard, "Very Hard" },
	{ INPUT_STRING_Medium_Difficult, "Medium Difficult" },
	{ INPUT_STRING_Difficult, "Difficult" },
	{ INPUT_STRING_Very_Difficult, "Very Difficult" },
	{ INPUT_STRING_Very_Low, "Very Low" },
	{ INPUT_STRING_Low, "Low" },
	{ INPUT_STRING_High, "High" },
	{ INPUT_STRING_Higher, "Higher" },
	{ INPUT_STRING_Highest, "Highest" },
	{ INPUT_STRING_Very_High, "Very High" },
	{ INPUT_STRING_Players, "Players" },
	{ INPUT_STRING_Controls, "Controls" },
	{ INPUT_STRING_Dual, "Dual" },
	{ INPUT_STRING_Single, "Single" },
	{ INPUT_STRING_Game_Time, "Game Time" },
	{ INPUT_STRING_Continue_Price, "Continue Price" },
	{ INPUT_STRING_Controller, "Controller" },
	{ INPUT_STRING_Light_Gun, "Light Gun" },
	{ INPUT_STRING_Joystick, "Joystick" },
	{ INPUT_STRING_Trackball, "Trackball" },
	{ INPUT_STRING_Continues, "Continues" },
	{ INPUT_STRING_Allow_Continue, "Allow Continue" },
	{ INPUT_STRING_Level_Select, "Level Select" },
	{ INPUT_STRING_Infinite, "Infinite" },
	{ INPUT_STRING_Stereo, "Stereo" },
	{ INPUT_STRING_Mono, "Mono" },
	{ INPUT_STRING_Unused, "Unused" },
	{ INPUT_STRING_Unknown, "Unknown" },
	{ INPUT_STRING_Standard, "Standard" },
	{ INPUT_STRING_Reverse, "Reverse" },
	{ INPUT_STRING_Alternate, "Alternate" },
	{ INPUT_STRING_None, "None" },
};


inline bool input_seq_good(running_machine &machine, input_seq const &seq)
{
	if (INPUT_CODE_INVALID == seq[0])
		return false;
	else if (seq.empty())
		return true;
	else
		return input_seq::end_code != machine.input().seq_clean(seq)[0];
}


std::string substitute_player(std::string_view name, u8 player)
{
	using util::lang_translate;

	std::string result;
	while (!name.empty())
	{
		auto const found = name.find('%');
		if ((std::string_view::npos == found) || (name.length() == found + 1))
		{
			result.append(name);
			break;
		}
		switch (name[found + 1])
		{
		case '%':
			result.append(name.substr(0, found + 1));
			break;
		case 'p':
			result.append(name.substr(0, found));
			result.append(util::string_format(_("input-name", "P%1$u"), player + 1));
			break;
		default:
			result.append(name.substr(0, found + 2));
		}
		name.remove_prefix(found + 2);
	}
	return result;
}

} // anonymous namespace


// XML attributes for the different types
const char *const ioport_manager::seqtypestrings[] = { "standard", "increment", "decrement" };


u8 const inp_header::MAGIC[inp_header::OFFS_BASETIME - inp_header::OFFS_MAGIC] = { 'M', 'A', 'M', 'E', 'I', 'N', 'P', 0 };



//**************************************************************************
//  BUILT-IN CORE MAPPINGS
//**************************************************************************

#include "inpttype.ipp"



//**************************************************************************
//  PORT CONFIGURATIONS
//**************************************************************************

//**************************************************************************
//  I/O PORT LIST
//**************************************************************************

//-------------------------------------------------
//  append - append the given device's input ports
//  to the current list
//-------------------------------------------------

void ioport_list::append(device_t &device, std::string &errorbuf)
{
	// no constructor, no list
	ioport_constructor constructor = device.input_ports();
	if (constructor == nullptr)
		return;

	// reset error buffer
	errorbuf.clear();

	// detokenize into the list
	(*constructor)(device, *this, errorbuf);

	// collapse fields and sort the list
	for (auto &port : *this)
		port.second->collapse_fields(errorbuf);
}



//**************************************************************************
//  INPUT TYPE ENTRY
//**************************************************************************

//-------------------------------------------------
//  input_type_entry - constructors
//-------------------------------------------------

input_type_entry::input_type_entry(ioport_type type, ioport_group group, int player, const char *token, const char *name, input_seq standard) noexcept
	: m_type(type),
		m_group(group),
		m_player(player),
		m_token(token),
		m_name(name)
{
	m_defseq[SEQ_TYPE_STANDARD] = m_seq[SEQ_TYPE_STANDARD] = standard;
}

input_type_entry::input_type_entry(ioport_type type, ioport_group group, int player, const char *token, const char *name, input_seq standard, input_seq decrement, input_seq increment) noexcept
	: m_type(type),
		m_group(group),
		m_player(player),
		m_token(token),
		m_name(name)
{
	m_defseq[SEQ_TYPE_STANDARD] = m_seq[SEQ_TYPE_STANDARD] = standard;
	m_defseq[SEQ_TYPE_INCREMENT] = m_seq[SEQ_TYPE_INCREMENT] = increment;
	m_defseq[SEQ_TYPE_DECREMENT] = m_seq[SEQ_TYPE_DECREMENT] = decrement;
}


//-------------------------------------------------
//  name - gets the display name for the input
//  type
//-------------------------------------------------

std::string input_type_entry::name() const
{
	using util::lang_translate;

	if (!m_name)
		return std::string();
	else if ((group() < IPG_PLAYER1) || (group() > IPG_PLAYER10))
		return _("input-name", m_name);
	else
		return substitute_player(_("input-name", m_name), player());
}


//-------------------------------------------------
//  replace_code - replace all instances of
//   oldcodewith newcode in all sequences
//-------------------------------------------------

void input_type_entry::replace_code(input_code oldcode, input_code newcode) noexcept
{
	for (input_seq &seq : m_seq)
		seq.replace(oldcode, newcode);
}


//-------------------------------------------------
//  configure_osd - set the token and name of an
//  OSD entry
//-------------------------------------------------

void input_type_entry::configure_osd(const char *token, const char *name) noexcept
{
	assert(m_type >= IPT_OSD_1 && m_type <= IPT_OSD_16);
	m_token = token;
	m_name = name;
}


//-------------------------------------------------
//  restore_default_seq - restores the sequence
//  from the default
//-------------------------------------------------

void input_type_entry::restore_default_seq() noexcept
{
	m_seq = m_defseq;
}


//**************************************************************************
//  DIGITAL JOYSTICKS
//**************************************************************************

//-------------------------------------------------
//  digital_joystick - constructor
//-------------------------------------------------

digital_joystick::digital_joystick(int player, int number)
	:   m_player(player),
		m_number(number),
		m_current(0),
		m_current4way(0),
		m_previous(0)
{
}


//-------------------------------------------------
//  set_axis - configure a single axis of a
//  digital joystick
//-------------------------------------------------

digital_joystick::direction_t digital_joystick::add_axis(ioport_field &field)
{
	direction_t direction = direction_t((field.type() - (IPT_DIGITAL_JOYSTICK_FIRST + 1)) % 4);
	m_field[direction].emplace_front(field);
	return direction;
}


//-------------------------------------------------
//  frame_update - update the state of digital
//  joysticks prior to accumulating the results
//  in a port
//-------------------------------------------------

void digital_joystick::frame_update()
{
	// remember previous state and reset current state
	m_previous = m_current;
	m_current = 0;

	// read all the associated ports
	running_machine *machine = nullptr;
	for (direction_t direction = JOYDIR_UP; direction < JOYDIR_COUNT; ++direction)
		for (const std::reference_wrapper<ioport_field> &i : m_field[direction])
		{
			machine = &i.get().machine();
			if (machine->input().seq_pressed(i.get().seq(SEQ_TYPE_STANDARD)))
				m_current |= 1 << direction;
		}

	// lock out opposing directions (left + right or up + down)
	if ((m_current & (UP_BIT | DOWN_BIT)) == (UP_BIT | DOWN_BIT))
		m_current &= ~(UP_BIT | DOWN_BIT);
	if ((m_current & (LEFT_BIT | RIGHT_BIT)) == (LEFT_BIT | RIGHT_BIT))
		m_current &= ~(LEFT_BIT | RIGHT_BIT);

	// only update 4-way case if joystick has moved
	if (m_current != m_previous)
	{
		m_current4way = m_current;

		//
		//  If joystick is pointing at a diagonal, acknowledge that the player moved
		//  the joystick by favoring a direction change.  This minimizes frustration
		//  and maximizes responsiveness.
		//
		//  For example, if you are holding "left" then switch to "up" (where both left
		//  and up are briefly pressed at the same time), we'll transition immediately
		//  to "up."
		//
		//  Zero any switches that didn't change from the previous to current state.
		//
		if ((m_current4way & (UP_BIT | DOWN_BIT)) &&
			(m_current4way & (LEFT_BIT | RIGHT_BIT)))
		{
			m_current4way ^= m_current4way & m_previous;
		}

		//
		//  If we are still pointing at a diagonal, we are in an indeterminant state.
		//
		//  This could happen if the player moved the joystick from the idle position directly
		//  to a diagonal, or from one diagonal directly to an extreme diagonal.
		//
		//  The chances of this happening with a keyboard are slim, but we still need to
		//  constrain this case. Let's pick the horizontal axis.
		//
		if ((m_current4way & (UP_BIT | DOWN_BIT)) &&
			(m_current4way & (LEFT_BIT | RIGHT_BIT)))
		{
			m_current4way &= ~(UP_BIT | DOWN_BIT);
		}
	}
}



//**************************************************************************
//  I/O PORT CONDITION
//**************************************************************************

//-------------------------------------------------
//  eval - evaluate condition
//-------------------------------------------------

bool ioport_condition::eval() const
{
	// always condition is always true
	if (m_condition == ALWAYS)
		return true;

	// otherwise, read the referenced port and switch off the condition type
	ioport_value const condvalue = m_port->read();
	switch (m_condition)
	{
		case ALWAYS:            return true;
		case EQUALS:            return ((condvalue & m_mask) == m_value);
		case NOTEQUALS:         return ((condvalue & m_mask) != m_value);
		case GREATERTHAN:       return ((condvalue & m_mask) > m_value);
		case NOTGREATERTHAN:    return ((condvalue & m_mask) <= m_value);
		case LESSTHAN:          return ((condvalue & m_mask) < m_value);
		case NOTLESSTHAN:       return ((condvalue & m_mask) >= m_value);
	}
	return true;
}


//-------------------------------------------------
//  initialize - create the live state
//-------------------------------------------------

void ioport_condition::initialize(device_t &device)
{
	if (m_tag != nullptr)
		m_port = device.ioport(m_tag);
}



//**************************************************************************
//  I/O PORT SETTING
//**************************************************************************

//-------------------------------------------------
//  ioport_setting - constructor
//-------------------------------------------------

ioport_setting::ioport_setting(ioport_field &field, ioport_value _value, const char *_name)
	: m_field(field),
		m_value(_value),
		m_name(_name)
{
}



//**************************************************************************
//  I/O PORT DIP LOCATION
//**************************************************************************

//-------------------------------------------------
//  ioport_diplocation - constructor
//-------------------------------------------------

ioport_diplocation::ioport_diplocation(const char *name, u8 swnum, bool invert)
	: m_name(name),
		m_number(swnum),
		m_invert(invert)
{
}



//**************************************************************************
//  I/O PORT FIELD
//**************************************************************************

//-------------------------------------------------
//  ioport_field - constructor
//-------------------------------------------------

ioport_field::ioport_field(ioport_port &port, ioport_type type, ioport_value defvalue, ioport_value maskbits, const char *name)
	: m_next(nullptr),
		m_port(port),
		m_modcount(port.modcount()),
		m_mask(maskbits),
		m_defvalue(defvalue & maskbits),
		m_type(type),
		m_player(0),
		m_flags(0),
		m_impulse(0),
		m_name(name),
		m_read(port.device()),
		m_write(port.device()),
		m_write_param(0),
		m_digital_value(false),
		m_min(0),
		m_max(maskbits),
		m_sensitivity(0),
		m_delta(0),
		m_centerdelta(0),
		m_crosshair_axis(CROSSHAIR_AXIS_NONE),
		m_crosshair_scale(1.0),
		m_crosshair_offset(0),
		m_crosshair_altaxis(0),
		m_crosshair_mapper(port.device()),
		m_full_turn_count(0),
		m_remap_table(nullptr),
		m_way(0)
{
	// reset sequences and chars
	for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
		m_seq[seqtype].set_default();

	for (int i = 0; i < std::size(m_chars); i++)
		std::fill(std::begin(m_chars[i]), std::end(m_chars[i]), char32_t(0));

	// for DIP switches and configs, look for a default value from the owner
	if (type == IPT_DIPSWITCH || type == IPT_CONFIG)
	{
		const input_device_default *def = device().input_ports_defaults();
		if (def != nullptr)
		{
			const char *fulltag = port.tag();
			for ( ; def->tag != nullptr; def++)
				if (device().subtag(def->tag) == fulltag && def->mask == m_mask)
					m_defvalue = def->defvalue & m_mask;
		}

		m_flags |= FIELD_FLAG_TOGGLE;
	}
}


//-------------------------------------------------
//  ~ioport_field - destructor
//-------------------------------------------------

ioport_field::~ioport_field()
{
}


//-------------------------------------------------
//  set_value - programmatically set field value
//-------------------------------------------------

void ioport_field::set_value(ioport_value value)
{
	if (is_analog())
		live().analog->set_value(s32(value));
	else
		m_digital_value = value != 0;
}


//-------------------------------------------------
//  clear_value - clear programmatic override
//-------------------------------------------------

void ioport_field::clear_value()
{
	if (is_analog())
		live().analog->clear_value();
	else
		m_digital_value = false;
}


//-------------------------------------------------
//  name - return the field name for a given input
//  field (this must never return nullptr)
//-------------------------------------------------

std::string ioport_field::name() const
{
	using util::lang_translate;

	// if we have an overridden name, use that
	if (m_live && !m_live->name.empty())
		return m_live->name;

	// if no specific name, use the generic name for the type
	if (!m_name)
		return manager().type_name(m_type, m_player);

	// return name for non-controller fields as-is
	ioport_group const group = manager().type_group(m_type, m_player);
	if ((group < IPG_PLAYER1) || (group > IPG_PLAYER10))
		return m_name;

	// substitute the player number in if necessary
	return substitute_player(m_name, m_player);
}


//-------------------------------------------------
//  seq - return the live input sequence for the
//  given input field
//-------------------------------------------------

const input_seq &ioport_field::seq(input_seq_type seqtype) const noexcept
{
	// if the sequence is not the special default code, return it
	if (m_live && !m_live->seq[seqtype].is_default())
		return m_live->seq[seqtype];

	// otherwise return the default sequence
	return defseq(seqtype);
}


//-------------------------------------------------
//  defseq - return the default input sequence for
//  the given input field
//-------------------------------------------------

const input_seq &ioport_field::defseq(input_seq_type seqtype) const noexcept
{
	// if the sequence is the special default code, return the expanded default value
	if (m_seq[seqtype].is_default())
		return manager().type_seq(m_type, m_player, seqtype);

	// otherwise, return the sequence as-is
	return m_seq[seqtype];
}


//-------------------------------------------------
//  set_defseq - dynamically alter the default
//  input sequence for the given input field
//-------------------------------------------------

void ioport_field::set_defseq(input_seq_type seqtype, const input_seq &newseq)
{
	// set the new sequence
	m_seq[seqtype] = newseq;
}


//-------------------------------------------------
//  type_class - return the type class for this
//  field
//-------------------------------------------------

ioport_type_class ioport_field::type_class() const noexcept
{
	// inputs associated with specific players
	ioport_group const group = manager().type_group(m_type, m_player);
	if ((group >= IPG_PLAYER1) && (group <= IPG_PLAYER10))
		return INPUT_CLASS_CONTROLLER;

	// keys (names derived from character codes)
	if (m_type == IPT_KEYPAD || m_type == IPT_KEYBOARD)
		return INPUT_CLASS_KEYBOARD;

	// configuration settings (specific names required)
	if (m_type == IPT_CONFIG || m_type == IPT_ADJUSTER)
		return INPUT_CLASS_CONFIG;

	// DIP switches (specific names required)
	if (m_type == IPT_DIPSWITCH)
		return INPUT_CLASS_DIPSWITCH;

	// miscellaneous non-player inputs (named and user-mappable)
	if (group == IPG_OTHER || (group == IPG_INVALID && m_name != nullptr))
		return INPUT_CLASS_MISC;

	// internal inputs (these may be anonymous)
	return INPUT_CLASS_INTERNAL;
}


//-------------------------------------------------
//  keyboard_codes - accesses a particular keyboard
//  code list
//-------------------------------------------------

std::vector<char32_t> ioport_field::keyboard_codes(int which) const
{
	if (which >= std::size(m_chars))
		throw emu_fatalerror("Tried to access keyboard_code with out-of-range index %d\n", which);

	std::vector<char32_t> result;
	for (int i = 0; i < std::size(m_chars[which]) && m_chars[which][i] != 0; i++)
		result.push_back(m_chars[which][i]);

	return result;
}


//-------------------------------------------------
//  key_name - returns the name of a specific key
//-------------------------------------------------

std::string ioport_field::key_name(int which) const
{
	std::vector<char32_t> codes = keyboard_codes(which);
	char32_t ch = codes.empty() ? 0 : codes[0];

	// attempt to get the string from the character info table
	switch (ch)
	{
	case 8: return "Backspace";
	case 9: return "Tab";
	case 12: return "Clear";
	case 13: return "Enter";
	case 27: return "Esc";
	case 32: return "Space";
	case UCHAR_SHIFT_1: return "Shift";
	case UCHAR_SHIFT_2: return "Ctrl";
	case UCHAR_MAMEKEY(ESC): return "Esc";
	case UCHAR_MAMEKEY(INSERT): return "Insert";
	case UCHAR_MAMEKEY(DEL): return "Delete";
	case UCHAR_MAMEKEY(HOME): return "Home";
	case UCHAR_MAMEKEY(END): return "End";
	case UCHAR_MAMEKEY(PGUP): return "Page Up";
	case UCHAR_MAMEKEY(PGDN): return "Page Down";
	case UCHAR_MAMEKEY(LEFT): return "Cursor Left";
	case UCHAR_MAMEKEY(RIGHT): return "Cursor Right";
	case UCHAR_MAMEKEY(UP): return "Cursor Up";
	case UCHAR_MAMEKEY(DOWN): return "Cursor Down";
	case UCHAR_MAMEKEY(SLASH_PAD): return "Keypad /";
	case UCHAR_MAMEKEY(ASTERISK): return "Keypad *";
	case UCHAR_MAMEKEY(MINUS_PAD): return "Keypad -";
	case UCHAR_MAMEKEY(PLUS_PAD): return "Keypad +";
	case UCHAR_MAMEKEY(DEL_PAD): return "Keypad .";
	case UCHAR_MAMEKEY(ENTER_PAD): return "Keypad Enter";
	case UCHAR_MAMEKEY(BS_PAD): return "Keypad Backspace";
	case UCHAR_MAMEKEY(TAB_PAD): return "Keypad Tab";
	case UCHAR_MAMEKEY(00_PAD): return "Keypad 00";
	case UCHAR_MAMEKEY(000_PAD): return "Keypad 000";
	case UCHAR_MAMEKEY(COMMA_PAD): return "Keypad ,";
	case UCHAR_MAMEKEY(EQUALS_PAD): return "Keypad =";
	case UCHAR_MAMEKEY(PRTSCR): return "Print Screen";
	case UCHAR_MAMEKEY(PAUSE): return "Pause";
	case UCHAR_MAMEKEY(LSHIFT): return "Left Shift";
	case UCHAR_MAMEKEY(RSHIFT): return "Right Shift";
	case UCHAR_MAMEKEY(LCONTROL): return "Left Ctrl";
	case UCHAR_MAMEKEY(RCONTROL): return "Right Ctrl";
	case UCHAR_MAMEKEY(LALT): return "Left Alt";
	case UCHAR_MAMEKEY(RALT): return "Right Alt";
	case UCHAR_MAMEKEY(SCRLOCK): return "Scroll Lock";
	case UCHAR_MAMEKEY(NUMLOCK): return "Num Lock";
	case UCHAR_MAMEKEY(CAPSLOCK): return "Caps Lock";
	case UCHAR_MAMEKEY(LWIN): return "Left Win";
	case UCHAR_MAMEKEY(RWIN): return "Right Win";
	case UCHAR_MAMEKEY(MENU): return "Menu";
	case UCHAR_MAMEKEY(CANCEL): return "Break";
	default: break;
	}

	// handle function keys
	if (ch >= UCHAR_MAMEKEY(F1) && ch <= UCHAR_MAMEKEY(F20))
		return util::string_format("F%d", ch - UCHAR_MAMEKEY(F1) + 1);

	// handle 0-9 on numeric keypad
	if (ch >= UCHAR_MAMEKEY(0_PAD) && ch <= UCHAR_MAMEKEY(9_PAD))
		return util::string_format("Keypad %d", ch - UCHAR_MAMEKEY(0_PAD));

	// if that doesn't work, convert to UTF-8
	if (ch > 0x7F || isprint(ch))
		return utf8_from_uchar(ch);

	// otherwise, opt for question marks
	return "???";
}


//-------------------------------------------------
//  get_user_settings - return the current
//  settings for the given input field
//-------------------------------------------------

void ioport_field::get_user_settings(user_settings &settings) const
{
	// zap the entire structure
	settings = user_settings();

	// copy the basics
	for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
	{
		settings.seq[seqtype] = seq(seqtype);
		if (m_live)
			settings.cfg[seqtype] = m_live->cfg[seqtype];
	}

	// if there's a list of settings or we're an adjuster, copy the current value
	if (!m_settinglist.empty() || m_type == IPT_ADJUSTER)
		settings.value = m_live->value;

	if (m_live->analog != nullptr)
	{
		// if there's analog data, extract the analog settings
		settings.sensitivity = m_live->analog->sensitivity();
		settings.delta = m_live->analog->delta();
		settings.centerdelta = m_live->analog->centerdelta();
		settings.reverse = m_live->analog->reverse();
	}
	else
	{
		// non-analog settings
		settings.toggle = m_live->toggle;
	}
}


//-------------------------------------------------
//  set_user_settings - modify the current
//  settings for the given input field
//-------------------------------------------------

void ioport_field::set_user_settings(const user_settings &settings)
{
	// copy the basics
	for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
	{
		if (settings.seq[seqtype].is_default())
			m_live->seq[seqtype].set_default();
		else
			m_live->seq[seqtype] = settings.seq[seqtype];
		m_live->cfg[seqtype] = settings.cfg[seqtype];
	}

	// if there's a list of settings or we're an adjuster, copy the current value
	if (!m_settinglist.empty() || m_type == IPT_ADJUSTER)
		m_live->value = settings.value;

	if (m_live->analog)
	{
		// if there's analog data, extract the analog settings
		m_live->analog->m_sensitivity = settings.sensitivity;
		m_live->analog->m_delta = settings.delta;
		m_live->analog->m_centerdelta = settings.centerdelta;
		m_live->analog->m_reverse = settings.reverse;
	}
	else
	{
		// non-analog settings
		m_live->toggle = settings.toggle;
	}
}


//-------------------------------------------------
//  setting_name - return the expanded setting
//  name for a field
//-------------------------------------------------

const char *ioport_field::setting_name() const
{
	// only makes sense if we have settings
	assert(!m_settinglist.empty());

	// scan the list of settings looking for a match on the current value
	for (ioport_setting const &setting : m_settinglist)
		if (setting.enabled())
			if (setting.value() == m_live->value)
				return setting.name();

	return "INVALID";
}


//-------------------------------------------------
//  has_previous_setting - return true if the
//  given field has a "previous" setting
//-------------------------------------------------

bool ioport_field::has_previous_setting() const
{
	// only makes sense if we have settings
	assert(!m_settinglist.empty());

	// scan the list of settings looking for a match on the current value
	for (ioport_setting const &setting : m_settinglist)
		if (setting.enabled())
			return (setting.value() != m_live->value);

	return false;
}


//-------------------------------------------------
//  select_previous_setting - select the previous
//  item for a DIP switch or configuration field
//-------------------------------------------------

void ioport_field::select_previous_setting()
{
	// only makes sense if we have settings
	assert(!m_settinglist.empty());

	// scan the list of settings looking for a match on the current value
	auto prevsetting = m_settinglist.end();
	bool found_match = false;
	for (auto setting = m_settinglist.begin(); m_settinglist.end() != setting; ++setting)
	{
		if (setting->enabled())
		{
			if (setting->value() == m_live->value)
			{
				found_match = true;
				if (m_settinglist.end() != prevsetting)
					break;
			}
			prevsetting = setting;
		}
	}

	// if we didn't find a matching value, select the first
	if (!found_match)
	{
		prevsetting = m_settinglist.begin();
		while ((m_settinglist.end() != prevsetting) && !prevsetting->enabled())
			++prevsetting;
	}

	// update the value to the previous one
	if (m_settinglist.end() != prevsetting)
		m_live->value = prevsetting->value();
}


//-------------------------------------------------
//  has_next_setting - return true if the given
//  field has a "next" setting
//-------------------------------------------------

bool ioport_field::has_next_setting() const
{
	// only makes sense if we have settings
	assert(!m_settinglist.empty());

	// scan the list of settings looking for a match on the current value
	bool found = false;
	for (ioport_setting const &setting : m_settinglist)
	{
		if (setting.enabled())
		{
			if (found)
				return true;
			if (setting.value() == m_live->value)
				found = true;
		}
	}
	return false;
}


//-------------------------------------------------
//  select_next_setting - select the next item for
//  a DIP switch or configuration field
//-------------------------------------------------

void ioport_field::select_next_setting()
{
	// only makes sense if we have settings
	assert(!m_settinglist.empty());

	// scan the list of settings looking for a match on the current value
	auto setting = m_settinglist.begin();
	while ((m_settinglist.end() != setting) && (!setting->enabled() || (setting->value() != m_live->value)))
		++setting;

	// if we found one, scan forward for the next valid one
	auto nextsetting = setting;
	if (m_settinglist.end() != nextsetting)
	{
		++nextsetting;
		while ((m_settinglist.end() != nextsetting) && !nextsetting->enabled())
			++nextsetting;
	}

	// if we hit the end, search from the beginning
	if (m_settinglist.end() == nextsetting)
	{
		nextsetting = m_settinglist.begin();
		while ((m_settinglist.end() != nextsetting) && !nextsetting->enabled())
			++nextsetting;
	}

	// update the value to the previous one
	if (m_settinglist.end() != nextsetting)
		m_live->value = nextsetting->value();
}


//-------------------------------------------------
//  frame_update_digital - get the state of a
//  digital field
//-------------------------------------------------

void ioport_field::frame_update(ioport_value &result)
{
	// skip if not enabled
	if (!enabled())
		return;

	// handle analog inputs first
	if (m_live->analog != nullptr)
	{
		m_live->analog->frame_update(machine());
		return;
	}

	// if UI is active, ignore digital inputs
	if (machine().ui().is_menu_active())
		return;

	// if user input is locked out here, bail
	if (m_live->lockout)
	{
		// use just the digital value
		if (m_digital_value)
			result |= m_mask;
		return;
	}

	// if the state changed, look for switch down/switch up
	bool curstate = m_digital_value || machine().input().seq_pressed(seq());
	bool changed = false;
	if (curstate != m_live->last)
	{
		m_live->last = curstate;
		changed = true;
	}

	// coin impulse option
	int effective_impulse = m_impulse;
	int impulse_option_val = machine().options().coin_impulse();
	if (impulse_option_val != 0)
	{
		if (impulse_option_val < 0)
			effective_impulse = 0;
		else if ((m_type >= IPT_COIN1 && m_type <= IPT_COIN12) || m_impulse != 0)
			effective_impulse = impulse_option_val;
	}

	// if this is a switch-down event, handle impulse and toggle
	if (changed && curstate)
	{
		// impulse controls: reset the impulse counter
		if (effective_impulse != 0 && m_live->impulse == 0)
			m_live->impulse = effective_impulse;

		// toggle controls: flip the toggle state or advance to the next setting
		if (m_live->toggle)
		{
			if (m_settinglist.empty())
				m_live->value ^= m_mask;
			else
				select_next_setting();
		}
	}

	// update the current state with the impulse state
	if (effective_impulse != 0)
	{
		curstate = (m_live->impulse != 0);
		if (curstate)
			m_live->impulse--;
	}

	// for toggle switches, the current value is folded into the port's default value
	// so we always return false here
	if (m_live->toggle)
		curstate = false;

	// additional logic to restrict digital joysticks
	if (curstate && !m_digital_value && m_live->joystick != nullptr && m_way != 16 && !machine().options().joystick_contradictory())
	{
		u8 mask = (m_way == 4) ? m_live->joystick->current4way() : m_live->joystick->current();
		if (!(mask & (1 << m_live->joydir)))
			curstate = false;
	}

	// skip locked-out coin inputs
	if (curstate && m_type >= IPT_COIN1 && m_type <= IPT_COIN12 && machine().bookkeeping().coin_lockout_get_state(m_type - IPT_COIN1))
	{
		bool verbose = machine().options().verbose();
#ifdef MAME_DEBUG
		verbose = true;
#endif
		if (machine().options().coin_lockout())
		{
			if (verbose)
				machine().ui().popup_time(3, "Coinlock disabled %s.", name());
			curstate = false;
		}
		else
			if (verbose)
				machine().ui().popup_time(3, "Coinlock disabled, but broken through %s.", name());
	}

	// if we're active, set the appropriate bits in the digital state
	if (curstate)
		result |= m_mask;
}


//-------------------------------------------------
//  crosshair_read - compute the crosshair
//  position
//-------------------------------------------------

float ioport_field::crosshair_read() const
{
	float value = m_live->analog->crosshair_read();

	// apply the scale and offset
	if (m_crosshair_scale < 0)
		value = -(1.0f - value) * m_crosshair_scale;
	else
		value *= m_crosshair_scale;
	value += m_crosshair_offset;

	// apply custom mapping if necessary
	if (!m_crosshair_mapper.isnull())
		value = m_crosshair_mapper(value);

	return value;
}


//-------------------------------------------------
//  expand_diplocation - expand a string-based
//  DIP location into a linked list of
//  descriptions
//-------------------------------------------------

void ioport_field::expand_diplocation(const char *location, std::string &errorbuf)
{
	// if nothing present, bail
	if (!location)
		return;

	m_diploclist.clear();

	// parse the string
	std::string name; // Don't move this variable inside the loop, lastname's lifetime depends on it being outside
	const char *lastname = nullptr;
	const char *curentry = location;
	int entries = 0;
	while (*curentry != 0)
	{
		// find the end of this entry
		const char *comma = strchr(curentry, ',');
		if (comma == nullptr)
			comma = curentry + strlen(curentry);

		// extract it to tempbuf
		std::string tempstr(curentry, comma - curentry);

		// first extract the switch name if present
		const char *number = tempstr.c_str();
		const char *colon = strchr(tempstr.c_str(), ':');

		if (colon != nullptr)
		{
			// allocate and copy the name if it is present
			lastname = name.assign(number, colon - number).c_str();
			number = colon + 1;
		}
		else
		{
			// otherwise, just copy the last name
			if (lastname == nullptr)
			{
				errorbuf.append(string_format("Switch location '%s' missing switch name!\n", location));
				lastname = (char *)"UNK";
			}
			name.assign(lastname);
		}

		// if the number is preceded by a '!' it's active high
		bool invert = false;
		if (*number == '!')
		{
			invert = true;
			number++;
		}

		// now scan the switch number
		int swnum = -1;
		if (sscanf(number, "%d", &swnum) != 1)
			errorbuf.append(string_format("Switch location '%s' has invalid format!\n", location));

		// allocate a new entry
		m_diploclist.emplace_back(name.c_str(), swnum, invert);
		entries++;

		// advance to the next item
		curentry = comma;
		if (*curentry != 0)
			curentry++;
	}

	// then verify the number of bits in the mask matches
	ioport_value temp;
	int bits;
	for (bits = 0, temp = m_mask; temp != 0 && bits < 32; bits++)
		temp &= temp - 1;
	if (bits != entries)
		errorbuf.append(string_format("Switch location '%s' does not describe enough bits for mask %X\n", location, m_mask));
}


//-------------------------------------------------
//  init_live_state - create live state structures
//-------------------------------------------------

void ioport_field::init_live_state(analog_field *analog)
{
	// resolve callbacks
	m_read.resolve();
	m_write.resolve();
	m_crosshair_mapper.resolve();

	// allocate live state
	m_live = std::make_unique<ioport_field_live>(*this, analog);

	m_condition.initialize(device());

	for (ioport_setting &setting : m_settinglist)
		setting.condition().initialize(setting.device());
}



//**************************************************************************
//  I/O PORT FIELD LIVE
//**************************************************************************

//-------------------------------------------------
//  ioport_field_live - constructor
//-------------------------------------------------

ioport_field_live::ioport_field_live(ioport_field &field, analog_field *analog)
	: analog(analog),
		joystick(nullptr),
		value(field.defvalue()),
		impulse(0),
		last(0),
		toggle(field.toggle()),
		joydir(digital_joystick::JOYDIR_COUNT),
		lockout(false)
{
	// fill in the basic values
	for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
		seq[seqtype] = field.defseq_unresolved(seqtype);

	// if this is a digital joystick field, make a note of it
	if (field.is_digital_joystick())
	{
		joystick = &field.manager().digjoystick(field.player(), (field.type() - (IPT_DIGITAL_JOYSTICK_FIRST + 1)) / 4);
		joydir = joystick->add_axis(field);
	}

	// Name keyboard key names
	if (field.type_class() == INPUT_CLASS_KEYBOARD && field.specific_name() == nullptr)
	{
		// loop through each character on the field
		for (int which = 0; which < (1 << (UCHAR_SHIFT_END - UCHAR_SHIFT_BEGIN + 1)); which++)
		{
			std::vector<char32_t> const codes = field.keyboard_codes(which);
			if (codes.empty())
				break;
			name.append(string_format("%-*s ", std::max(SPACE_COUNT - 1, 0), field.key_name(which)));
		}

		// special case
		if (name.empty())
			name.assign("Unnamed Key");
		else
		{
			// trim extra spaces
			auto pos = name.find_last_not_of(' ');
			assert(pos < name.size());
			name.erase(pos + 1);
		}
	}
}



//**************************************************************************
//  I/O PORT
//**************************************************************************

//-------------------------------------------------
//  ioport_port - constructor
//-------------------------------------------------

ioport_port::ioport_port(device_t &owner, const char *tag)
	: m_next(nullptr),
		m_device(owner),
		m_tag(tag),
		m_modcount(0),
		m_active(0)
{
}


//-------------------------------------------------
//  ~ioport_port - destructor
//-------------------------------------------------

ioport_port::~ioport_port()
{
}


//-------------------------------------------------
//  machine - return a reference to the running
//  machine
//-------------------------------------------------

running_machine &ioport_port::machine() const
{
	return m_device.machine();
}


//-------------------------------------------------
//  manager - return a reference to the
//  ioport_manager on the running machine
//-------------------------------------------------

ioport_manager &ioport_port::manager() const
{
	return machine().ioport();
}


//-------------------------------------------------
//  field - return a pointer to the first field
//  that intersects the given mask
//-------------------------------------------------

ioport_field *ioport_port::field(ioport_value mask) const
{
	// if we got the port, look for the field
	for (ioport_field &field : fields())
		if ((field.mask() & mask) != 0 && field.enabled())
			return &field;
	return nullptr;
}


//-------------------------------------------------
//  read - return the value of an I/O port
//-------------------------------------------------

ioport_value ioport_port::read()
{
	if (!manager().safe_to_read())
		throw emu_fatalerror("Input ports cannot be read at init time!");

	// start with the digital state
	ioport_value result = m_live->digital;

	// insert dynamic read values
	for (dynamic_field &dynfield : m_live->readlist)
		dynfield.read(result);

	// apply active high/low state to digital and dynamic read inputs
	result ^= m_live->defvalue;

	// insert analog portions
	for (analog_field &analog : m_live->analoglist)
		analog.read(result);

	return result;
}


//-------------------------------------------------
//  write - write a value to a port
//-------------------------------------------------

void ioport_port::write(ioport_value data, ioport_value mem_mask)
{
	// call device line write handlers
	COMBINE_DATA(&m_live->outputvalue);
	for (dynamic_field &dynfield : m_live->writelist)
		if (dynfield.field().type() == IPT_OUTPUT)
			dynfield.write(m_live->outputvalue ^ dynfield.field().defvalue());
}


//-------------------------------------------------
//  frame_update - once/frame update
//-------------------------------------------------

void ioport_port::frame_update()
{
	// start with 0 values for the digital bits
	m_live->digital = 0;

	// now loop back and modify based on the inputs
	for (ioport_field &field : m_fieldlist)
		field.frame_update(m_live->digital);
}


//-------------------------------------------------
//  collapse_fields - remove any fields that are
//  wholly overlapped by other fields
//-------------------------------------------------

void ioport_port::collapse_fields(std::string &errorbuf)
{
	ioport_value maskbits = 0;
	int lastmodcount = -1;

	// remove the whole list and start from scratch
	ioport_field *field = m_fieldlist.detach_all();
	while (field != nullptr)
	{
		// if this modcount doesn't match, reset
		if (field->modcount() != lastmodcount)
		{
			lastmodcount = field->modcount();
			maskbits = 0;
		}

		// reinsert this field
		ioport_field *current = field;
		field = field->next();
		insert_field(*current, maskbits, errorbuf);
	}
}


//-------------------------------------------------
//  insert_field - insert a new field, checking
//  for errors
//-------------------------------------------------

void ioport_port::insert_field(ioport_field &newfield, ioport_value &disallowedbits, std::string &errorbuf)
{
	// verify against the disallowed bits, but only if we are condition-free
	if (newfield.condition().none())
	{
		if ((newfield.mask() & disallowedbits) != 0)
			errorbuf.append(string_format("INPUT_TOKEN_FIELD specifies duplicate port bits (port=%s mask=%X)\n", tag(), newfield.mask()));
		disallowedbits |= newfield.mask();
	}

	// first modify/nuke any entries that intersect our maskbits
	ioport_field *nextfield;
	for (ioport_field *field = m_fieldlist.first(); field != nullptr; field = nextfield)
	{
		nextfield = field->next();
		if ((field->mask() & newfield.mask()) &&
			(newfield.condition().none() || field->condition().none() || field->condition() == newfield.condition()))
		{
			// reduce the mask of the field we found
			field->reduce_mask(newfield.mask());

			// if the new entry fully overrides the previous one, we nuke
			if (!field->mask() || (INPUT_PORT_OVERRIDE_FULLY_NUKES_PREVIOUS && (field->type() != IPT_UNUSED) && (field->type() != IPT_UNKNOWN)))
				m_fieldlist.remove(*field);
		}
	}

	// make a mask of just the low bit
	ioport_value lowbit = (newfield.mask() ^ (newfield.mask() - 1)) & newfield.mask();

	// scan forward to find where to insert ourselves
	ioport_field *field;
	for (field = m_fieldlist.first(); field != nullptr; field = field->next())
		if (field->mask() > lowbit)
			break;

	// insert it into the list
	m_fieldlist.insert_before(newfield, field);
}


//-------------------------------------------------
//  init_live_state - create the live state
//-------------------------------------------------

void ioport_port::init_live_state()
{
	m_live = std::make_unique<ioport_port_live>(*this);
}


//-------------------------------------------------
//  update_defvalue - force an update to the input
//  port values based on current conditions
//-------------------------------------------------

void ioport_port::update_defvalue(bool flush_defaults)
{
	// only clear on the first pass
	if (flush_defaults)
		m_live->defvalue = 0;

	// recompute the default value for the entire port
	for (ioport_field &field : m_fieldlist)
		if (field.enabled())
			m_live->defvalue = (m_live->defvalue & ~field.mask()) | (field.live().value & field.mask());
}



//**************************************************************************
//  I/O PORT LIVE STATE
//**************************************************************************

//-------------------------------------------------
//  ioport_port_live - constructor
//-------------------------------------------------

ioport_port_live::ioport_port_live(ioport_port &port)
	: defvalue(0),
		digital(0),
		outputvalue(0)
{
	// iterate over fields
	for (ioport_field &field : port.fields())
	{
		// allocate analog state if it's analog
		analog_field *analog = nullptr;
		if (field.is_analog())
			analog = &analoglist.emplace_back(field);

		// allocate a dynamic field for reading
		if (field.has_dynamic_read())
			readlist.emplace_back(field);

		// allocate a dynamic field for writing
		if (field.has_dynamic_write())
			writelist.emplace_back(field);

		// let the field initialize its live state
		field.init_live_state(analog);
	}
}



//**************************************************************************
//  I/O PORT MANAGER
//**************************************************************************

//-------------------------------------------------
//  ioport_manager - constructor
//-------------------------------------------------

ioport_manager::ioport_manager(running_machine &machine)
	: m_machine(machine)
	, m_safe_to_read(false)
	, m_last_frame_time(attotime::zero)
	, m_last_delta_nsec(0)
	, m_playback_accumulated_speed(0)
	, m_playback_accumulated_frames(0)
	, m_deselected_card_config()
	, m_applied_device_defaults(false)
{
	for (auto &entries : m_type_to_entry)
		std::fill(std::begin(entries), std::end(entries), nullptr);
}


//-------------------------------------------------
//  initialize - walk the configured ports and
//  create live state information
//-------------------------------------------------

time_t ioport_manager::initialize()
{
	// add an exit callback and a frame callback
	machine().add_notifier(MACHINE_NOTIFY_EXIT, machine_notify_delegate(&ioport_manager::exit, this));
	machine().add_notifier(MACHINE_NOTIFY_FRAME, machine_notify_delegate(&ioport_manager::frame_update_callback, this));

	// initialize the default port info from the OSD
	init_port_types();

	// if we have a token list, proceed
	device_enumerator iter(machine().root_device());
	for (device_t &device : iter)
	{
		std::string errors;
		m_portlist.append(device, errors);
		if (!errors.empty())
			osd_printf_error("Input port errors:\n%s", errors);
	}

	// renumber player numbers for controller ports
	int player_offset = 0;
	for (device_t &device : iter)
	{
		int players = 0;
		for (auto &port : m_portlist)
		{
			if (&port.second->device() == &device)
			{
				for (ioport_field &field : port.second->fields())
				{
					if (field.type_class() == INPUT_CLASS_CONTROLLER)
					{
						if (players < field.player() + 1)
							players = field.player() + 1;
						field.set_player(field.player() + player_offset);
					}
				}
			}
		}
		player_offset += players;
	}

	// allocate live structures to mirror the configuration
	for (auto &port : m_portlist)
		port.second->init_live_state();

	// handle autoselection of devices
	init_autoselect_devices({ IPT_AD_STICK_X,  IPT_AD_STICK_Y,   IPT_AD_STICK_Z }, OPTION_ADSTICK_DEVICE,    "analog joystick");
	init_autoselect_devices({ IPT_PADDLE,      IPT_PADDLE_V },                     OPTION_PADDLE_DEVICE,     "paddle");
	init_autoselect_devices({ IPT_PEDAL,       IPT_PEDAL2,       IPT_PEDAL3 },     OPTION_PEDAL_DEVICE,      "pedal");
	init_autoselect_devices({ IPT_LIGHTGUN_X,  IPT_LIGHTGUN_Y },                   OPTION_LIGHTGUN_DEVICE,   "lightgun");
	init_autoselect_devices({ IPT_POSITIONAL,  IPT_POSITIONAL_V },                 OPTION_POSITIONAL_DEVICE, "positional");
	init_autoselect_devices({ IPT_DIAL,        IPT_DIAL_V },                       OPTION_DIAL_DEVICE,       "dial");
	init_autoselect_devices({ IPT_TRACKBALL_X, IPT_TRACKBALL_Y },                  OPTION_TRACKBALL_DEVICE,  "trackball");
	init_autoselect_devices({ IPT_MOUSE_X,     IPT_MOUSE_Y },                      OPTION_MOUSE_DEVICE,      "mouse");

	// look for 4-way diagonal joysticks and change the default map if we find any
	const char *joystick_map_default = machine().options().joystick_map();
	if (joystick_map_default[0] == 0 || strcmp(joystick_map_default, "auto") == 0)
		for (auto &port : m_portlist)
			for (ioport_field const &field : port.second->fields())
				if (field.live().joystick != nullptr && field.rotated())
				{
					input_class_joystick &devclass = downcast<input_class_joystick &>(machine().input().device_class(DEVICE_CLASS_JOYSTICK));
					devclass.set_global_joystick_map(input_class_joystick::map_4way_diagonal);
					break;
				}

	// register callbacks for when we load configurations
	machine().configuration().config_register(
			"input",
			configuration_manager::load_delegate(&ioport_manager::load_config, this),
			configuration_manager::save_delegate(&ioport_manager::save_config, this));

	// open playback and record files if specified
	time_t basetime = playback_init();
	record_init();
	return basetime;
}


//-------------------------------------------------
//  init_port_types - initialize the default
//  type list
//-------------------------------------------------

void ioport_manager::init_port_types()
{
	// convert the array into a list of type states that can be modified
	emplace_core_types(m_typelist);

	// ask the OSD to customize the list
	machine().osd().customize_input_type_list(m_typelist);

	// now iterate over the OSD-modified types
	for (input_type_entry &curtype : m_typelist)
	{
		// first copy all the OSD-updated sequences into our current state
		curtype.restore_default_seq();

		// also make a lookup table mapping type/player to the appropriate type list entry
		m_type_to_entry[curtype.type()][curtype.player()] = &curtype;
	}
}


//-------------------------------------------------
//  init_autoselect_devices - autoselect a single
//  device based on the input port list passed
//  in and the corresponding option
//-------------------------------------------------

void ioport_manager::init_autoselect_devices(std::initializer_list<ioport_type> types, std::string_view option, std::string_view ananame)
{
	static std::pair<char const *, char const *> const CLASS_OPTIONS[] = {
			{ "mouse",     OPTION_MOUSE },
			{ "joystick",  OPTION_JOYSTICK },
			{ "lightgun",  OPTION_LIGHTGUN } };

	// if nothing specified, ignore the option
	auto const autooption = machine().options().get_entry(option);
	char const *const autoclass = autooption->value();
	if (!autoclass || !*autoclass || !std::strcmp(autoclass, "none"))
		return;

	// if the device class is enabled anyway or disabled at a higher priority level, do nothing
	auto const classname = std::find_if(
			std::begin(CLASS_OPTIONS),
			std::end(CLASS_OPTIONS),
			[&autoclass] (auto const &x) { return !std::strcmp(autoclass, x.first); });
	if (std::end(CLASS_OPTIONS) != classname)
	{
		if (machine().options().bool_value(classname->second))
			return;

		auto const classoption = machine().options().get_entry(classname->second);
		if (classoption->priority() > autooption->priority())
		{
			osd_printf_verbose("Input: Won't autoenable %s in presence of a %s as it's disabled at a higher priority\n", autoclass, ananame);
			return;
		}
	}

	// find matching device class
	input_class *autoenable_class = nullptr;
	for (input_device_class devclass = DEVICE_CLASS_FIRST_VALID; devclass <= DEVICE_CLASS_LAST_VALID; ++devclass)
	{
		if (!std::strcmp(autoclass, machine().input().device_class(devclass).name()))
		{
			autoenable_class = &machine().input().device_class(devclass);
			break;
		}
	}
	if (!autoenable_class)
	{
		osd_printf_error("Invalid %s value %s; reverting to keyboard\n", option, autoclass);
		autoenable_class = &machine().input().device_class(DEVICE_CLASS_KEYBOARD);
	}

	// nothing to do if the class is already enabled
	if (autoenable_class->enabled())
		return;

	// scan the port list
	for (auto &port : m_portlist)
	{
		for (ioport_field const &field : port.second->fields())
		{
			// if this port type is in use, apply the autoselect criteria
			if (std::find(std::begin(types), std::end(types), field.type()) != std::end(types))
			{
				osd_printf_verbose("Input: Autoenabling %s due to presence of a %s\n", autoenable_class->name(), ananame);
				autoenable_class->enable();
				return;
			}
		}
	}
}


//-------------------------------------------------
//  exit - exit callback to ensure we clean up
//  and close our files
//-------------------------------------------------

void ioport_manager::exit()
{
	// close any playback or recording files
	playback_end();
	record_end();
}


//-------------------------------------------------
//  ~ioport_manager - destructor
//-------------------------------------------------

ioport_manager::~ioport_manager()
{
}


//-------------------------------------------------
//  type_name - return the name for the given
//  type/player
//-------------------------------------------------

std::string ioport_manager::type_name(ioport_type type, u8 player) const
{
	using util::lang_translate;

	// if we have a machine, use the live state and quick lookup
	input_type_entry const *const entry = m_type_to_entry[type][player];
	if (entry)
	{
		std::string name = entry->name();
		if (!name.empty())
			return name;
	}

	// if we find nothing, return a default string (not a null pointer)
	return _("input-name", "???");
}


//-------------------------------------------------
//  type_group - return the group for the given
//  type/player
//-------------------------------------------------

ioport_group ioport_manager::type_group(ioport_type type, int player) const noexcept
{
	input_type_entry *entry = m_type_to_entry[type][player];
	if (entry != nullptr)
		return entry->group();

	// if we find nothing, return an invalid group
	return IPG_INVALID;
}


//-------------------------------------------------
//  type_seq - return the input sequence for the
//  given type/player
//-------------------------------------------------

const input_seq &ioport_manager::type_seq(ioport_type type, int player, input_seq_type seqtype) const noexcept
{
	assert(type >= 0 && type < IPT_COUNT);
	assert(player >= 0 && player < MAX_PLAYERS);

	// if we have a machine, use the live state and quick lookup
	input_type_entry *entry = m_type_to_entry[type][player];
	if (entry != nullptr)
		return entry->seq(seqtype);

	// if we find nothing, return an empty sequence
	return input_seq::empty_seq;
}


//-------------------------------------------------
//  set_type_seq - change the input sequence for
//  the given type/player
//-------------------------------------------------

void ioport_manager::set_type_seq(ioport_type type, int player, input_seq_type seqtype, const input_seq &newseq) noexcept
{
	input_type_entry *const entry = m_type_to_entry[type][player];
	if (entry)
	{
		if (newseq.is_default())
		{
			entry->set_seq(seqtype, entry->defseq(seqtype));
			entry->set_cfg(seqtype, "");
		}
		else
		{
			entry->set_seq(seqtype, newseq);
			if (!newseq.length())
				entry->set_cfg(seqtype, "NONE");
			else
				entry->set_cfg(seqtype, machine().input().seq_to_tokens(newseq));
		}
	}
}


//-------------------------------------------------
//  type_pressed - return true if the sequence for
//  the given input type/player is pressed
//-------------------------------------------------

bool ioport_manager::type_pressed(ioport_type type, int player)
{
	return machine().input().seq_pressed(type_seq(type, player));
}


//-------------------------------------------------
//  type_class_present - return true if the given
//  ioport_type_class exists in at least one port
//-------------------------------------------------

bool ioport_manager::type_class_present(ioport_type_class inputclass) const noexcept
{
	for (auto &port : m_portlist)
		for (ioport_field const &field : port.second->fields())
			if (field.type_class() == inputclass)
				return true;
	return false;
}


//-------------------------------------------------
//  count_players - counts the number of active
//  players
//-------------------------------------------------

int ioport_manager::count_players() const noexcept
{
	int max_player = 0;
	for (auto &port : m_portlist)
		for (ioport_field const &field : port.second->fields())
			if (field.type_class() == INPUT_CLASS_CONTROLLER && max_player <= field.player() + 1)
				max_player = field.player() + 1;

	return max_player;
}


//-------------------------------------------------
//  frame_update - core logic for per-frame input
//  port updating
//-------------------------------------------------

digital_joystick &ioport_manager::digjoystick(int player, int number)
{
	// find it in the list
	for (digital_joystick &joystick : m_joystick_list)
		if (joystick.player() == player && joystick.number() == number)
			return joystick;

	// create a new one
	return m_joystick_list.emplace_back(player, number);
}


//-------------------------------------------------
//  frame_update - callback for once/frame updating
//-------------------------------------------------

void ioport_manager::frame_update_callback()
{
	// if we're paused, don't do anything
	if (!machine().paused())
		frame_update();
}


//-------------------------------------------------
//  frame_update_internal - core logic for
//  per-frame input port updating
//-------------------------------------------------

void ioport_manager::frame_update()
{
	auto profile = g_profiler.start(PROFILER_INPUT);

	// record/playback information about the current frame
	attotime curtime = machine().time();
	playback_frame(curtime);
	record_frame(curtime);

	// track the duration of the previous frame
	m_last_delta_nsec = (curtime - m_last_frame_time).as_attoseconds() / ATTOSECONDS_PER_NANOSECOND;
	m_last_frame_time = curtime;

	// update the digital joysticks
	for (digital_joystick &joystick : m_joystick_list)
		joystick.frame_update();

	// compute default values for all the ports
	// two passes to catch conditionals properly
	for (auto &port : m_portlist)
		port.second->update_defvalue(true);
	for (auto &port : m_portlist)
		port.second->update_defvalue(false);

	// loop over all input ports
	for (auto &port : m_portlist)
	{
		port.second->frame_update();

		// handle playback/record
		playback_port(*port.second.get());
		record_port(*port.second.get());

		// call device line write handlers
		ioport_value newvalue = port.second->read();
		for (dynamic_field &dynfield : port.second->live().writelist)
			if (dynfield.field().type() != IPT_OUTPUT)
				dynfield.write(newvalue);
	}
}


//-------------------------------------------------
//  frame_interpolate - interpolate between two
//  values based on the time between frames
//-------------------------------------------------

s32 ioport_manager::frame_interpolate(s32 oldval, s32 newval)
{
	// if no last delta, just use new value
	if (m_last_delta_nsec == 0)
		return newval;

	// otherwise, interpolate
	attoseconds_t nsec_since_last = (machine().time() - m_last_frame_time).as_attoseconds() / ATTOSECONDS_PER_NANOSECOND;
	return oldval + (s64(newval - oldval) * nsec_since_last / m_last_delta_nsec);
}


//-------------------------------------------------
//  load_config - callback to extract configuration
//  data from the XML nodes
//-------------------------------------------------

void ioport_manager::load_config(config_type cfg_type, config_level cfg_level, util::xml::data_node const *parentnode)
{
	// make sure device defaults get applied at some point
	if ((cfg_type > config_type::CONTROLLER) && !m_applied_device_defaults)
	{
		apply_device_defaults();

		// after applying controller config, push that to the backup, as it's what we'll diff against
		for (input_type_entry &entry : m_typelist)
			for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
				entry.defseq(seqtype) = entry.seq(seqtype);
	}

	// in the completion phase, we finish the initialization with the final ports
	if (cfg_type == config_type::FINAL)
	{
		m_safe_to_read = true;
		frame_update();
	}

	// early exit if no data to parse
	if (!parentnode)
		return;

	// load device map table for controller configs only
	if (cfg_type == config_type::CONTROLLER)
	{
		// iterate over device remapping entries
		input_manager::devicemap_table devicemap;
		for (util::xml::data_node const *mapdevice_node = parentnode->get_child("mapdevice"); mapdevice_node != nullptr; mapdevice_node = mapdevice_node->get_next_sibling("mapdevice"))
		{
			char const *const devicename = mapdevice_node->get_attribute_string("device", nullptr);
			char const *const controllername = mapdevice_node->get_attribute_string("controller", nullptr);
			if (devicename && controllername)
				devicemap.emplace(devicename, controllername);
		}

		// we can't rearrange controllers after applying device-supplied defaults
		if (!m_applied_device_defaults)
		{
			// map device to controller if we have a device map
			if (!devicemap.empty())
				machine().input().map_device_to_controller(devicemap);

			// add extra default assignments for input devices
			apply_device_defaults();
		}
		else if (!devicemap.empty())
		{
			osd_printf_warning("Controller configuration: Only <mapdevice> elements from the first applicable <system> element are applied\n");
		}

		// iterate over any input code remapping nodes
		load_remap_table(*parentnode);
	}

	// iterate over all the port nodes
	for (util::xml::data_node const *portnode = parentnode->get_child("port"); portnode; portnode = portnode->get_next_sibling("port"))
	{
		// get the basic port info from the attributes
		int player;
		int type = token_to_input_type(portnode->get_attribute_string("type", ""), player);

		// initialize sequences to invalid defaults
		std::pair<input_seq, char const *> newseq[SEQ_TYPE_TOTAL];
		for (auto &seq : newseq)
		{
			seq.first.set(INPUT_CODE_INVALID);
			seq.second = "";
		}

		// loop over new sequences
		for (util::xml::data_node const *seqnode = portnode->get_child("newseq"); seqnode; seqnode = seqnode->get_next_sibling("newseq"))
		{
			// with a valid type, parse out the new sequence
			input_seq_type seqtype = token_to_seq_type(seqnode->get_attribute_string("type", ""));
			if ((seqtype != -1) && seqnode->get_value())
			{
				if (!strcmp(seqnode->get_value(), "NONE"))
					newseq[seqtype].first.reset();
				else
					machine().input().seq_from_tokens(newseq[seqtype].first, seqnode->get_value());
				if (config_type::CONTROLLER != cfg_type)
					newseq[seqtype].second = seqnode->get_value();
			}
		}

		// load into the appropriate place for the config type/level
		if (config_type::SYSTEM == cfg_type)
			load_system_config(*portnode, type, player, newseq);
		else if ((config_type::CONTROLLER == cfg_type) && (config_level::DEFAULT != cfg_level))
			load_controller_config(*portnode, type, player, newseq);
		else
			load_default_config(type, player, newseq);
	}

	if (cfg_type == config_type::CONTROLLER)
	{
		// after applying the controller config, push that back into the backup, since that is what we will diff against
		for (input_type_entry &entry : m_typelist)
			for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
				entry.defseq(seqtype) = entry.seq(seqtype);
	}
	else if (cfg_type == config_type::SYSTEM)
	{
		// load keyboard enable/disable state
		std::vector<bool> kbd_enable_set;
		bool keyboard_enabled = false, missing_enabled = false;
		natural_keyboard &natkbd = machine().natkeyboard();
		for (util::xml::data_node const *kbdnode = parentnode->get_child("keyboard"); kbdnode; kbdnode = kbdnode->get_next_sibling("keyboard"))
		{
			char const *const tag = kbdnode->get_attribute_string("tag", nullptr);
			int const enabled = kbdnode->get_attribute_int("enabled", -1);
			if (tag && (0 <= enabled))
			{
				size_t i;
				for (i = 0; natkbd.keyboard_count() > i; ++i)
				{
					if (!strcmp(natkbd.keyboard_device(i).tag(), tag))
					{
						if (kbd_enable_set.empty())
							kbd_enable_set.resize(natkbd.keyboard_count(), false);
						kbd_enable_set[i] = true;
						if (enabled)
						{
							if (!natkbd.keyboard_is_keypad(i))
								keyboard_enabled = true;
							natkbd.enable_keyboard(i);
						}
						else
						{
							natkbd.disable_keyboard(i);
						}
						break;
					}
				}
				missing_enabled = missing_enabled || (enabled && (natkbd.keyboard_count() <= i));
			}
		}

		// if keyboard enable configuration was loaded, patch it up for principle of least surprise
		if (!kbd_enable_set.empty())
		{
			for (size_t i = 0; natkbd.keyboard_count() > i; ++i)
			{
				if (!natkbd.keyboard_is_keypad(i))
				{
					if (!keyboard_enabled && missing_enabled)
					{
						natkbd.enable_keyboard(i);
						keyboard_enabled = true;
					}
					else if (!kbd_enable_set[i])
					{
						if (keyboard_enabled)
							natkbd.disable_keyboard(i);
						else
							natkbd.enable_keyboard(i);
						keyboard_enabled = true;
					}
				}
			}
		}
	}
}


//-------------------------------------------------
//  load_remap_table - extract and apply the
//  global remapping table
//-------------------------------------------------

void ioport_manager::load_remap_table(util::xml::data_node const &parentnode)
{
	// count items first so we can allocate
	int count = 0;
	for (util::xml::data_node const *remapnode = parentnode.get_child("remap"); remapnode != nullptr; remapnode = remapnode->get_next_sibling("remap"))
		count++;

	// if we have some, deal with them
	if (count > 0)
	{
		// allocate tables
		std::vector<input_code> oldtable(count);
		std::vector<input_code> newtable(count);

		// build up the remap table
		count = 0;
		for (util::xml::data_node const *remapnode = parentnode.get_child("remap"); remapnode != nullptr; remapnode = remapnode->get_next_sibling("remap"))
		{
			input_code origcode = machine().input().code_from_token(remapnode->get_attribute_string("origcode", ""));
			input_code newcode = machine().input().code_from_token(remapnode->get_attribute_string("newcode", ""));
			if (origcode != INPUT_CODE_INVALID && newcode != INPUT_CODE_INVALID)
			{
				oldtable[count] = origcode;
				newtable[count] = newcode;
				count++;
			}
		}

		// loop over the remapping table, then over default ports, replacing old with new
		for (int remapnum = 0; remapnum < count; remapnum++)
			for (input_type_entry &entry : m_typelist)
				entry.replace_code(oldtable[remapnum], newtable[remapnum]);
	}
}


//-------------------------------------------------
//  load_default_config - apply input settings
//  to defaults for all systems
//-------------------------------------------------

bool ioport_manager::load_default_config(
		int type,
		int player,
		const std::pair<input_seq, char const *> (&newseq)[SEQ_TYPE_TOTAL])
{
	// find a matching port in the list
	for (input_type_entry &entry : m_typelist)
	{
		if (entry.type() == type && entry.player() == player)
		{
			for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
			{
				if (input_seq_good(machine(), newseq[seqtype].first))
					entry.set_seq(seqtype, newseq[seqtype].first);
				entry.set_cfg(seqtype, newseq[seqtype].second);
			}
			return true;
		}
	}
	return false;
}


//-------------------------------------------------
//  load_controller_config - apply controler
//  profile settings to defaults
//-------------------------------------------------

bool ioport_manager::load_controller_config(
		util::xml::data_node const &portnode,
		int type,
		int player,
		const std::pair<input_seq, char const *> (&newseq)[SEQ_TYPE_TOTAL])
{
	// without a tag, apply to the defaults for all systems
	char const *const tag = portnode.get_attribute_string("tag", nullptr);
	if (!tag)
		return load_default_config(type, player, newseq);

	// ensure the port actually exists
	auto const port(m_portlist.find(tag));
	if (m_portlist.end() == port)
		return false;
	ioport_value const mask = portnode.get_attribute_int("mask", 0);
	if (!mask)
		return false;

	// find the matching field
	ioport_value const defvalue = portnode.get_attribute_int("defvalue", 0);
	bool matched = false;
	for (ioport_field &field : port->second->fields())
	{
		// find the matching mask and default value
		if (field.type() == type && field.player() == player &&
			field.mask() == mask && (field.defvalue() & mask) == (defvalue & mask))
		{
			// if a sequence was specified, override the developer-specified default for the field
			for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
			{
				if (input_seq_good(machine(), newseq[seqtype].first))
				{
					field.live().seq[seqtype] = newseq[seqtype].first;
					field.set_defseq(seqtype, newseq[seqtype].first);
				}
			}

			// fetch configurable attributes
			if (!field.live().analog)
			{
				// for non-analog fields

				// can't practically set value here

				// fetch yes/no for toggle setting
				char const *const togstring = portnode.get_attribute_string("toggle", nullptr);
				if (togstring && !strcmp(togstring, "yes"))
				{
					field.live().toggle = true;
					field.m_flags |= ioport_field::FIELD_FLAG_TOGGLE;
				}
				else if (togstring && !strcmp(togstring, "no"))
				{
					field.live().toggle = false;
					field.m_flags &= ~ioport_field::FIELD_FLAG_TOGGLE;
				}
			}
			else
			{
				// for analog fields

#if 0 // changing this stuff causes issues because of the way it's tied up with the analog_field object
				// get base attributes
				field.live().analog->m_delta = field.m_delta = portnode.get_attribute_int("keydelta", field.delta());
				field.live().analog->m_centerdelta = field.m_centerdelta = portnode.get_attribute_int("centerdelta", field.centerdelta());
				field.live().analog->m_sensitivity = field.m_sensitivity = portnode.get_attribute_int("sensitivity", field.sensitivity());

				// fetch yes/no for reverse setting
				char const *const revstring = portnode.get_attribute_string("reverse", nullptr);
				if (revstring && !strcmp(revstring, "yes"))
				{
					field.live().analog->m_reverse = true;
					field.m_flags |= ioport_field::ANALOG_FLAG_REVERSE;
				}
				else if (revstring && !strcmp(revstring, "no"))
				{
					field.live().analog->m_reverse = false;
					field.m_flags &= ~ioport_field::ANALOG_FLAG_REVERSE;
				}
#endif
			}

			// only break out of the loop for unconditional inputs
			if (field.condition().condition() == ioport_condition::ALWAYS)
				return true;
			else
				matched = true;
		}
	}

	// return whether any field matched
	return matched;
}


//-------------------------------------------------
//  load_system_config - apply saved input
//  configuration for the current system
//-------------------------------------------------

void ioport_manager::load_system_config(
		util::xml::data_node const &portnode,
		int type,
		int player,
		const std::pair<input_seq, char const *> (&newseq)[SEQ_TYPE_TOTAL])
{
	// system-specific configuration should always apply by port/field
	char const *const tag = portnode.get_attribute_string("tag", nullptr);
	ioport_value const mask = portnode.get_attribute_int("mask", 0);
	ioport_value const defvalue = portnode.get_attribute_int("defvalue", 0);
	if (!tag || !mask)
		return;

	// find the port we want
	auto const port(m_portlist.find(tag));
	if (m_portlist.end() != port)
	{
		for (ioport_field &field : port->second->fields())
		{
			// find the matching mask and default value
			if (field.type() == type && field.player() == player &&
				field.mask() == mask && (field.defvalue() & mask) == (defvalue & mask))
			{
				// if a sequence was specified, copy it in
				for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
				{
					if (input_seq_good(machine(), newseq[seqtype].first))
						field.live().seq[seqtype] = newseq[seqtype].first;
					field.live().cfg[seqtype] = newseq[seqtype].second;
				}

				// fetch configurable attributes
				if (!field.live().analog)
				{
					// for non-analog fields

					// fetch the value
					field.live().value = portnode.get_attribute_int("value", field.defvalue());

					// fetch yes/no for toggle setting
					char const *const togstring = portnode.get_attribute_string("toggle", nullptr);
					if (togstring && !strcmp(togstring, "yes"))
						field.live().toggle = true;
					else if (togstring && !strcmp(togstring, "no"))
						field.live().toggle = false;
				}
				else
				{
					// for analog fields

					// get base attributes
					field.live().analog->m_delta = portnode.get_attribute_int("keydelta", field.delta());
					field.live().analog->m_centerdelta = portnode.get_attribute_int("centerdelta", field.centerdelta());
					field.live().analog->m_sensitivity = portnode.get_attribute_int("sensitivity", field.sensitivity());

					// fetch yes/no for reverse setting
					char const *const revstring = portnode.get_attribute_string("reverse", nullptr);
					if (revstring && !strcmp(revstring, "yes"))
						field.live().analog->m_reverse = true;
					else if (revstring && !strcmp(revstring, "no"))
						field.live().analog->m_reverse = false;
				}

				// only break out of the loop for unconditional inputs
				if (field.condition().condition() == ioport_condition::ALWAYS)
					break;
			}
		}
	}
	else
	{
		// see if this belongs to a slot card that isn't inserted
		std::string_view parent_tag(tag);
		auto pos(parent_tag.rfind(':'));
		if (pos && (std::string_view::npos != pos))
		{
			parent_tag = parent_tag.substr(0, pos);
			if (!machine().root_device().subdevice(parent_tag))
			{
				for (pos = parent_tag.rfind(':'); pos && (std::string_view::npos != pos); pos = parent_tag.rfind(':'))
				{
					std::string_view const child_tag(parent_tag.substr(pos + 1));
					parent_tag = parent_tag.substr(0, pos);
					device_t const *const parent_device(machine().root_device().subdevice(parent_tag));
					if (parent_device)
					{
						device_slot_interface const *slot;
						if (parent_device->interface(slot) && (slot->option_list().find(std::string(child_tag)) != slot->option_list().end()))
						{
							if (!m_deselected_card_config)
								m_deselected_card_config = util::xml::file::create();
							portnode.copy_into(*m_deselected_card_config);
						}
						break;
					}
				}
			}
		}
	}
}


//-------------------------------------------------
//  apply_device_defaults - add default assignments
//  supplied by input devices
//-------------------------------------------------

void ioport_manager::apply_device_defaults()
{
	// make sure this only happens once
	assert(!m_applied_device_defaults);
	m_applied_device_defaults = true;

	// TODO: come up with a way to deal with non-multi device classes here?
	for (input_device_class classno = DEVICE_CLASS_FIRST_VALID; DEVICE_CLASS_LAST_VALID >= classno; ++classno)
	{
		input_class &devclass = machine().input().device_class(classno);
		for (int devnum = 0; devclass.maxindex() >= devnum; ++devnum)
		{
			// make sure device exists
			input_device const *const device = devclass.device(devnum);
			if (!device)
				continue;

			// iterate over default assignments
			for (auto [porttype, seqtype, seq] : device->default_assignments())
			{
				assert(!seq.empty());
				assert(seq.is_valid());
				assert(!seq.is_default());

				// only apply UI assignments for first device in a class
				if ((IPT_UI_FIRST < porttype) && (IPT_UI_LAST > porttype) && (device->devindex() != 0))
					continue;

				// limit to maximum player count
				if (device->devindex() >= MAX_PLAYERS)
					continue;

				// find a matching port in the list
				auto const found = std::find_if(
						m_typelist.begin(),
						m_typelist.end(),
						[type = porttype, device] (input_type_entry const &entry)
						{
							return (entry.type() == type) && (entry.player() == device->devindex());
						});
				if (m_typelist.end() == found)
					continue;

				// start with the current setting
				input_seq remapped(found->seq(seqtype));
				if (!remapped.empty())
					remapped += input_seq::or_code;

				// append adjusting the device index
				for (int i = 0, len = seq.length(); i < len; ++i)
				{
					input_code code = seq[i];
					if (!code.internal())
					{
						assert(code.device_class() == classno);
						assert(code.device_index() == 0);
						assert(code.item_id() >= ITEM_ID_FIRST_VALID);
						assert(code.item_id() <= ITEM_ID_ABSOLUTE_MAXIMUM);
						code.set_device_index(device->devindex());
					}
					remapped += code;
				}

				// apply to the entry
				found->set_seq(seqtype, remapped);
			}
		}
	}
}



//**************************************************************************
//  SETTINGS SAVE
//**************************************************************************

//-------------------------------------------------
//  save_config - config callback for saving input
//  port configuration
//-------------------------------------------------

void ioport_manager::save_config(config_type cfg_type, util::xml::data_node *parentnode)
{
	// if no parentnode, ignore
	if (!parentnode)
		return;

	// default ports save differently
	if (cfg_type == config_type::DEFAULT)
		save_default_inputs(*parentnode);
	else if (cfg_type == config_type::SYSTEM)
		save_game_inputs(*parentnode);
}


//-------------------------------------------------
//  save_this_input_field_type - determine if the
//  given port type is worth saving
//-------------------------------------------------

bool ioport_manager::save_this_input_field_type(ioport_type type)
{
	switch (type)
	{
		case IPT_UNUSED:
		case IPT_END:
		case IPT_PORT:
		case IPT_UNKNOWN:
			return false;

		default:
			break;
	}
	return true;
}


//-------------------------------------------------
//  save_default_inputs - add nodes for any default
//  mappings that have changed
//-------------------------------------------------

void ioport_manager::save_default_inputs(util::xml::data_node &parentnode)
{
	// iterate over ports
	for (input_type_entry &entry : m_typelist)
	{
		// only save if this port is a type we save
		if (save_this_input_field_type(entry.type()))
		{
			// see if any of the sequences have changed
			input_seq_type seqtype;
			for (seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
				if (!entry.cfg(seqtype).empty())
					break;

			// if so, we need to add a node
			if (seqtype < SEQ_TYPE_TOTAL)
			{
				// add a new port node
				util::xml::data_node *const portnode = parentnode.add_child("port", nullptr);
				if (portnode)
				{
					// add the port information and attributes
					portnode->set_attribute("type", input_type_to_token(entry.type(), entry.player()).c_str());

					// add only the sequences that have changed from the defaults
					for (input_seq_type type = SEQ_TYPE_STANDARD; type < SEQ_TYPE_TOTAL; ++type)
					{
						if (!entry.cfg(type).empty())
						{
							util::xml::data_node *const seqnode = portnode->add_child("newseq", entry.cfg(type).c_str());
							if (seqnode)
								seqnode->set_attribute("type", seqtypestrings[type]);
						}
					}
				}
			}
		}
	}
}


//-------------------------------------------------
//  save_game_inputs - add nodes for any game
//  mappings that have changed
//-------------------------------------------------

void ioport_manager::save_game_inputs(util::xml::data_node &parentnode)
{
	// save keyboard enable/disable state
	natural_keyboard &natkbd = machine().natkeyboard();
	for (size_t i = 0; natkbd.keyboard_count() > i; ++i)
	{
		util::xml::data_node *const kbdnode = parentnode.add_child("keyboard", nullptr);
		kbdnode->set_attribute("tag", natkbd.keyboard_device(i).tag());
		kbdnode->set_attribute_int("enabled", natkbd.keyboard_enabled(i));
	}

	// iterate over ports
	for (auto &port : m_portlist)
		for (ioport_field const &field : port.second->fields())
			if (save_this_input_field_type(field.type()) && field.enabled())
			{
				// determine if we changed
				bool changed = false;
				for (input_seq_type seqtype = SEQ_TYPE_STANDARD; (seqtype < SEQ_TYPE_TOTAL) && !changed; ++seqtype)
					changed = !field.live().cfg[seqtype].empty();

				if (!field.is_analog())
				{
					// non-analog changes
					changed = changed || ((field.live().value & field.mask()) != (field.defvalue() & field.mask()));
					changed = changed || (field.live().toggle != field.toggle());
				}
				else
				{
					// analog changes
					changed = changed || (field.live().analog->m_delta != field.delta());
					changed = changed || (field.live().analog->m_centerdelta != field.centerdelta());
					changed = changed || (field.live().analog->m_sensitivity != field.sensitivity());
					changed = changed || (field.live().analog->m_reverse != field.analog_reverse());
				}

				// if we did change, add a new node
				if (changed)
				{
					// add a new port node
					util::xml::data_node *const portnode = parentnode.add_child("port", nullptr);
					if (portnode)
					{
						// add the identifying information and attributes
						portnode->set_attribute("tag", port.second->tag());
						portnode->set_attribute("type", input_type_to_token(field.type(), field.player()).c_str());
						portnode->set_attribute_int("mask", field.mask());
						portnode->set_attribute_int("defvalue", field.defvalue() & field.mask());

						// add sequences if changed
						for (input_seq_type seqtype = SEQ_TYPE_STANDARD; seqtype < SEQ_TYPE_TOTAL; ++seqtype)
						{
							if (!field.live().cfg[seqtype].empty())
							{
								util::xml::data_node *const seqnode = portnode->add_child("newseq", field.live().cfg[seqtype].c_str());
								if (seqnode)
									seqnode->set_attribute("type", seqtypestrings[seqtype]);
							}
						}

						if (!field.is_analog())
						{
							// write out non-analog changes
							if ((field.live().value & field.mask()) != (field.defvalue() & field.mask()))
								portnode->set_attribute_int("value", field.live().value & field.mask());
							if (field.live().toggle != field.toggle())
								portnode->set_attribute("toggle", field.live().toggle ? "yes" : "no");
						}
						else
						{
							// write out analog changes
							if (field.live().analog->m_delta != field.delta())
								portnode->set_attribute_int("keydelta", field.live().analog->m_delta);
							if (field.live().analog->m_centerdelta != field.centerdelta())
								portnode->set_attribute_int("centerdelta", field.live().analog->m_centerdelta);
							if (field.live().analog->m_sensitivity != field.sensitivity())
								portnode->set_attribute_int("sensitivity", field.live().analog->m_sensitivity);
							if (field.live().analog->m_reverse != field.analog_reverse())
								portnode->set_attribute("reverse", field.live().analog->m_reverse ? "yes" : "no");
						}
					}
				}
			}

	// preserve configuration for deselected slot cards
	if (m_deselected_card_config)
	{
		for (util::xml::data_node const *node = m_deselected_card_config->get_first_child(); node; node = node->get_next_sibling())
			node->copy_into(parentnode);
	}
}



//**************************************************************************
//  INPUT PLAYBACK
//**************************************************************************

//-------------------------------------------------
//  playback_read - read a value from the playback
//  file
//-------------------------------------------------

template<typename Type>
Type ioport_manager::playback_read(Type &result)
{
	// protect against nullptr handles if previous reads fail
	if (!m_playback_stream)
		return result = Type(0);

	// read the value; if we fail, end playback
	size_t read;
	m_playback_stream->read(&result, sizeof(result), read);
	if (sizeof(result) != read)
	{
		playback_end("End of file");
		return result = Type(0);
	}

	// normalize byte order
	if (sizeof(result) == 8)
		result = little_endianize_int64(result);
	else if (sizeof(result) == 4)
		result = little_endianize_int32(result);
	else if (sizeof(result) == 2)
		result = little_endianize_int16(result);

	return result;
}

template<>
bool ioport_manager::playback_read<bool>(bool &result)
{
	u8 temp;
	playback_read(temp);
	return result = bool(temp);
}


//-------------------------------------------------
//  playback_init - initialize INP playback
//-------------------------------------------------

time_t ioport_manager::playback_init()
{
	// if no file, nothing to do
	const char *filename = machine().options().playback();
	if (filename[0] == 0)
		return 0;

	// open the playback file
	m_playback_file = std::make_unique<emu_file>(machine().options().input_directory(), OPEN_FLAG_READ);
	std::error_condition const filerr = m_playback_file->open(filename);

	// return an explicit error if file isn't found in given path
	if (filerr == std::errc::no_such_file_or_directory)
		fatalerror("Input file %s not found\n",filename);

	// TODO: bail out any other error laconically for now
	if (filerr)
		fatalerror("Failed to open file %s for playback (%s:%d %s)\n", filename, filerr.category().name(), filerr.value(), filerr.message());

	// read the header and verify that it is a modern version; if not, print an error
	inp_header header;
	if (!header.read(*m_playback_file))
		fatalerror("Input file is corrupt or invalid (missing header)\n");
	if (!header.check_magic())
		fatalerror("Input file invalid or in an older, unsupported format\n");
	if (header.get_majversion() != inp_header::MAJVERSION)
		fatalerror("Input file format version mismatch\n");

	// output info to console
	osd_printf_info("Input file: %s\n", filename);
	osd_printf_info("INP version %u.%u\n", header.get_majversion(), header.get_minversion());
	time_t basetime = header.get_basetime();
	osd_printf_info("Created %s\n", ctime(&basetime));
	osd_printf_info("Recorded using %s\n", header.get_appdesc());

	// verify the header against the current game
	std::string const sysname = header.get_sysname();
	if (sysname != machine().system().name)
		osd_printf_info("Input file is for machine '%s', not for current machine '%s'\n", sysname, machine().system().name);

	// enable compression
	m_playback_stream = util::zlib_read(*m_playback_file, 16386);
	return basetime;
}


//-------------------------------------------------
//  playback_end - end INP playback
//-------------------------------------------------

void ioport_manager::playback_end(const char *message)
{
	// only applies if we have a live file
	if (m_playback_stream)
	{
		// close the file
		m_playback_stream.reset();
		m_playback_file.reset();

		// pop a message
		if (message != nullptr)
			machine().popmessage("Playback Ended\nReason: %s", message);

		// display speed stats
		if (m_playback_accumulated_speed > 0)
			m_playback_accumulated_speed /= m_playback_accumulated_frames;
		osd_printf_info("Total playback frames: %d\n", u32(m_playback_accumulated_frames));
		osd_printf_info("Average recorded speed: %d%%\n", u32((m_playback_accumulated_speed * 200 + 1) >> 21));

		// close the program at the end of inp file playback
		if (machine().options().exit_after_playback())
		{
			osd_printf_info("Exiting MAME now...\n");
			machine().schedule_exit();
		}
	}
}


//-------------------------------------------------
//  playback_frame - start of frame callback for
//  playback
//-------------------------------------------------

void ioport_manager::playback_frame(const attotime &curtime)
{
	// if playing back, fetch the information and verify
	if (m_playback_stream)
	{
		// first the absolute time
		seconds_t seconds_temp;
		attoseconds_t attoseconds_temp;
		playback_read(seconds_temp);
		playback_read(attoseconds_temp);
		attotime readtime(seconds_temp, attoseconds_temp);
		if (readtime != curtime)
			playback_end("Out of sync");

		// then the speed
		u32 curspeed;
		m_playback_accumulated_speed += playback_read(curspeed);
		m_playback_accumulated_frames++;
	}
}


//-------------------------------------------------
//  playback_port - per-port callback for playback
//-------------------------------------------------

void ioport_manager::playback_port(ioport_port &port)
{
	// if playing back, fetch information about this port
	if (m_playback_stream)
	{
		// read the default value and the digital state
		playback_read(port.live().defvalue);
		playback_read(port.live().digital);

		// loop over analog ports and save their data
		for (analog_field &analog : port.live().analoglist)
		{
			// read current and previous values
			playback_read(analog.m_accum);
			playback_read(analog.m_previous);

			// read configuration information
			playback_read(analog.m_sensitivity);
			playback_read(analog.m_reverse);
		}
	}
}


//-------------------------------------------------
//  record_write - write a value to the record file
//-------------------------------------------------

template<typename Type>
void ioport_manager::record_write(Type value)
{
	// protect against nullptr handles if previous reads fail
	if (!m_record_stream)
		return;

	// normalize byte order
	if (sizeof(value) == 8)
		value = little_endianize_int64(value);
	else if (sizeof(value) == 4)
		value = little_endianize_int32(value);
	else if (sizeof(value) == 2)
		value = little_endianize_int16(value);

	// write the value; if we fail, end recording
	size_t written;
	if (m_record_stream->write(&value, sizeof(value), written) || (sizeof(value) != written))
		record_end("Out of space");
}

template<>
void ioport_manager::record_write<bool>(bool value)
{
	u8 byte = u8(value);
	record_write(byte);
}


//-------------------------------------------------
//  record_init - initialize INP recording
//-------------------------------------------------

void ioport_manager::record_init()
{
	// if no file, nothing to do
	const char *filename = machine().options().record();
	if (filename[0] == 0)
		return;

	// open the record file
	m_record_file = std::make_unique<emu_file>(machine().options().input_directory(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS);
	std::error_condition const filerr = m_record_file->open(filename);
	if (filerr)
		throw emu_fatalerror("ioport_manager::record_init: Failed to open file for recording (%s:%d %s)", filerr.category().name(), filerr.value(), filerr.message());

	// get the base time
	system_time systime;
	machine().base_datetime(systime);

	// fill in the header
	inp_header header;
	header.set_magic();
	header.set_basetime(systime.time);
	header.set_version();
	header.set_sysname(machine().system().name);
	header.set_appdesc(util::string_format("%s %s", emulator_info::get_appname(), emulator_info::get_build_version()));

	// write it
	header.write(*m_record_file);

	// enable compression
	m_record_stream = util::zlib_write(*m_record_file, 6, 16384);
}


//-------------------------------------------------
//  record_end - end INP recording
//-------------------------------------------------

void ioport_manager::record_end(const char *message)
{
	// only applies if we have a live file
	if (m_record_stream)
	{
		// close the file
		m_record_stream.reset(); // TODO: check for errors flushing the last compressed block before doing this
		m_record_file.reset();

		// pop a message
		if (message != nullptr)
			machine().popmessage("Recording Ended\nReason: %s", message);
	}
}


//-------------------------------------------------
//  record_frame - start of frame callback for
//  recording
//-------------------------------------------------

void ioport_manager::record_frame(const attotime &curtime)
{
	// if recording, record information about the current frame
	if (m_record_stream)
	{
		// first the absolute time
		record_write(curtime.seconds());
		record_write(curtime.attoseconds());

		// then the current speed
		record_write(u32(machine().video().speed_percent() * double(1 << 20)));
	}
}


//-------------------------------------------------
//  record_port - per-port callback for record
//-------------------------------------------------

void ioport_manager::record_port(ioport_port &port)
{
	// if recording, store information about this port
	if (m_record_stream)
	{
		// store the default value and digital state
		record_write(port.live().defvalue);
		record_write(port.live().digital);

		// loop over analog ports and save their data
		for (analog_field &analog : port.live().analoglist)
		{
			// store current and previous values
			record_write(analog.m_accum);
			record_write(analog.m_previous);

			// store configuration information
			record_write(analog.m_sensitivity);
			record_write(analog.m_reverse);
		}
	}
}



//**************************************************************************
//  I/O PORT CONFIGURER
//**************************************************************************

//-------------------------------------------------
//  ioport_configurer - constructor
//-------------------------------------------------

ioport_configurer::ioport_configurer(device_t &owner, ioport_list &portlist, std::string &errorbuf)
	: m_owner(owner),
		m_portlist(portlist),
		m_errorbuf(errorbuf),
		m_curport(nullptr),
		m_curfield(nullptr),
		m_cursetting(nullptr)
{
}


//-------------------------------------------------
//  string_from_token - convert an
//  ioport_token to a default string
//-------------------------------------------------

const char *ioport_configurer::string_from_token(const char *string)
{
	// 0 is an invalid index
	if (string == nullptr)
		return nullptr;

	// if the index is greater than the count, assume it to be a pointer
	if (uintptr_t(string) >= INPUT_STRING_COUNT)
		return string;

#if false // Set true, If you want to take care missing-token or wrong-sorting

	// otherwise, scan the list for a matching string and return it
	for (int index = 0; index < std::size(input_port_default_strings); index++)
		if (input_port_default_strings[index].id == uintptr_t(string))
			return input_port_default_strings[index].string;
	return "(Unknown Default)";

#else

	return input_port_default_strings[uintptr_t(string)-1].string;

#endif
}


//-------------------------------------------------
//  port_alloc - allocate a new port
//-------------------------------------------------

ioport_configurer& ioport_configurer::port_alloc(const char *tag)
{
	// create the full tag
	std::string fulltag = m_owner.subtag(tag);

	// add it to the list, and reset current field/setting
	if (m_portlist.count(fulltag) != 0) throw tag_add_exception(fulltag.c_str());
	m_portlist.emplace(std::make_pair(fulltag, std::make_unique<ioport_port>(m_owner, fulltag.c_str())));
	m_curport = m_portlist.find(fulltag)->second.get();
	m_curfield = nullptr;
	m_cursetting = nullptr;
	return *this;
}


//-------------------------------------------------
//  port_modify - find an existing port and
//  modify it
//-------------------------------------------------

ioport_configurer& ioport_configurer::port_modify(const char *tag)
{
	// create the full tag
	std::string fulltag = m_owner.subtag(tag);

	// find the existing port
	m_curport = m_portlist.find(fulltag)->second.get();
	if (m_curport == nullptr)
		throw emu_fatalerror("Requested to modify nonexistent port '%s'", fulltag);

	// bump the modification count, and reset current field/setting
	m_curport->m_modcount++;
	m_curfield = nullptr;
	m_cursetting = nullptr;
	return *this;
}


//-------------------------------------------------
//  field_alloc - allocate a new field
//-------------------------------------------------

ioport_configurer& ioport_configurer::field_alloc(ioport_type type, ioport_value defval, ioport_value mask, const char *name)
{
	// make sure we have a port
	if (m_curport == nullptr)
		throw emu_fatalerror("alloc_field called with no active port (mask=%X defval=%X)\n", mask, defval);
	// append the field
	if (type != IPT_UNKNOWN && type != IPT_UNUSED)
		m_curport->m_active |= mask;
	m_curfield = &m_curport->m_fieldlist.append(*new ioport_field(*m_curport, type, defval, mask, string_from_token(name)));

	// reset the current setting
	m_cursetting = nullptr;
	return *this;
}


//-------------------------------------------------
//  field_add_char - add a character to a field
//-------------------------------------------------

ioport_configurer& ioport_configurer::field_add_char(std::initializer_list<char32_t> charlist)
{
	for (int index = 0; index < std::size(m_curfield->m_chars); index++)
		if (m_curfield->m_chars[index][0] == 0)
		{
			const size_t char_count = std::size(m_curfield->m_chars[index]);
			assert(charlist.size() > 0 && charlist.size() <= char_count);

			for (size_t i = 0; i < char_count; i++)
				m_curfield->m_chars[index][i] = i < charlist.size() ? *(charlist.begin() + i) : 0;
			return *this;
		}

	std::ostringstream s;
	bool is_first = true;
	for (char32_t ch : charlist)
	{
		util::stream_format(s, "%s%d", is_first ? "" : ",", (int)ch);
		is_first = false;
	}
	throw emu_fatalerror("PORT_CHAR(%s) could not be added - maximum amount exceeded\n", s.str());
}


//-------------------------------------------------
//  field_add_code - add a character to a field
//-------------------------------------------------

ioport_configurer& ioport_configurer::field_add_code(input_seq_type which, input_code code)
{
	m_curfield->m_seq[which] |= code;
	return *this;
}


//-------------------------------------------------
//  setting_alloc - allocate a new setting
//-------------------------------------------------

ioport_configurer& ioport_configurer::setting_alloc(ioport_value value, const char *name)
{
	// make sure we have a field
	if (!m_curfield)
		throw emu_fatalerror("setting_alloc called with no active field (value=%X name=%s)\n", value, name);

	// append a new setting
	m_cursetting = &m_curfield->m_settinglist.emplace_back(*m_curfield, value & m_curfield->mask(), string_from_token(name));
	return *this;
}


//-------------------------------------------------
//  set_condition - set the condition for either
//  the current setting or field
//-------------------------------------------------

ioport_configurer& ioport_configurer::set_condition(ioport_condition::condition_t condition, const char *tag, ioport_value mask, ioport_value value)
{
	ioport_condition &target = m_cursetting ? m_cursetting->condition() : m_curfield->condition();
	target.set(condition, tag, mask, value);
	return *this;
}


//-------------------------------------------------
//  onoff_alloc - allocate an on/off DIP switch
//-------------------------------------------------

ioport_configurer& ioport_configurer::onoff_alloc(const char *name, ioport_value defval, ioport_value mask, const char *diplocation)
{
	// allocate a field normally
	field_alloc(IPT_DIPSWITCH, defval, mask, name);

	// expand the diplocation
	if (diplocation != nullptr)
		field_set_diplocation(diplocation);

	// allocate settings
	setting_alloc(defval & mask, DEF_STR(Off));
	setting_alloc(~defval & mask, DEF_STR(On));
	// clear cursettings set by setting_alloc
	m_cursetting = nullptr;
	return *this;
}



/***************************************************************************
    MISCELLANEOUS
***************************************************************************/

//-------------------------------------------------
//  dynamic_field - constructor
//-------------------------------------------------

dynamic_field::dynamic_field(ioport_field &field)
	: m_field(field)
	, m_shift(0)
	, m_oldval(field.defvalue())
{
	// fill in the data
	for (ioport_value mask = field.mask(); !(mask & 1); mask >>= 1)
		m_shift++;
	m_oldval >>= m_shift;
}


//-------------------------------------------------
//  read - read the updated value and merge it
//  into the target
//-------------------------------------------------

void dynamic_field::read(ioport_value &result)
{
	// skip if not enabled
	if (m_field.enabled())
	{
		// call the callback to read a new value
		ioport_value newval = m_field.m_read();
		m_oldval = newval;

		// merge in the bits (don't invert yet, as all digitals are inverted together)
		result = (result & ~m_field.mask()) | ((newval << m_shift) & m_field.mask());
	}
}


//-------------------------------------------------
//  write - track a change to a value and call
//  the write callback if there's something new
//-------------------------------------------------

void dynamic_field::write(ioport_value newval)
{
	// skip if not enabled
	if (m_field.enabled())
	{
		// if the bits have changed, call the handler
		newval = (newval & m_field.mask()) >> m_shift;
		if (m_oldval != newval)
		{
			m_field.m_write(m_field, m_field.m_write_param, m_oldval, newval);
			m_oldval = newval;
		}
	}
}


//-------------------------------------------------
//  analog_field - constructor
//-------------------------------------------------

analog_field::analog_field(ioport_field &field)
	: m_field(field)
	, m_shift(compute_shift(field.mask()))
	, m_adjdefvalue((field.defvalue() & field.mask()) >> m_shift)
	, m_adjmin((field.minval() & field.mask()) >> m_shift)
	, m_adjmax((field.maxval() & field.mask()) >> m_shift)
	, m_adjoverride((field.defvalue() & field.mask()) >> m_shift)
	, m_sensitivity(field.sensitivity())
	, m_reverse(field.analog_reverse())
	, m_delta(field.delta())
	, m_centerdelta(field.centerdelta())
	, m_accum(0)
	, m_previous(0)
	, m_previousanalog(0)
	, m_minimum(osd::input_device::ABSOLUTE_MIN)
	, m_maximum(osd::input_device::ABSOLUTE_MAX)
	, m_center(0)
	, m_reverse_val(0)
	, m_scalepos(0)
	, m_scaleneg(0)
	, m_keyscalepos(0)
	, m_keyscaleneg(0)
	, m_positionalscale(0)
	, m_absolute(false)
	, m_wraps(false)
	, m_autocenter(false)
	, m_single_scale(false)
	, m_interpolate(false)
	, m_lastdigital(false)
	, m_use_adjoverride(false)
{
	// set basic parameters based on the configured type
	switch (field.type())
	{
		// paddles and analog joysticks are absolute and autocenter
		case IPT_AD_STICK_X:
		case IPT_AD_STICK_Y:
		case IPT_AD_STICK_Z:
		case IPT_PADDLE:
		case IPT_PADDLE_V:
			m_absolute = true;
			m_autocenter = true;
			m_interpolate = !field.analog_reset();
			break;

		// pedals start at and autocenter to the min range
		case IPT_PEDAL:
		case IPT_PEDAL2:
		case IPT_PEDAL3:
			m_center = osd::input_device::ABSOLUTE_MIN;
			m_accum = apply_inverse_sensitivity(m_center);
			m_absolute = true;
			m_autocenter = true;
			m_interpolate = !field.analog_reset();
			break;

		// lightguns are absolute as well, but don't autocenter and don't interpolate their values
		case IPT_LIGHTGUN_X:
		case IPT_LIGHTGUN_Y:
			m_absolute = true;
			m_autocenter = false;
			m_interpolate = false;
			break;

		// positional devices are absolute, but can also wrap like relative devices
		// set each position to be 512 units
		case IPT_POSITIONAL:
		case IPT_POSITIONAL_V:
			m_positionalscale = compute_scale(field.maxval(), osd::input_device::ABSOLUTE_MAX - osd::input_device::ABSOLUTE_MIN);
			m_adjmin = 0;
			m_adjmax = field.maxval() - 1;
			m_wraps = field.analog_wraps();
			m_autocenter = !m_wraps;
			break;

		// dials, mice and trackballs are relative devices
		// these have fixed "min" and "max" values based on how many bits are in the port
		// in addition, we set the wrap around min/max values to 512 * the min/max values
		// this takes into account the mapping that one mouse unit ~= 512 analog units
		case IPT_DIAL:
		case IPT_DIAL_V:
		case IPT_TRACKBALL_X:
		case IPT_TRACKBALL_Y:
		case IPT_MOUSE_X:
		case IPT_MOUSE_Y:
			m_absolute = false;
			m_wraps = true;
			m_interpolate = !field.analog_reset();
			break;

		default:
			fatalerror("Unknown analog port type -- don't know if it is absolute or not\n");
	}

	if (m_absolute)
	{
		// further processing for absolute controls

		// if the default value is pegged at the min or max, use a single scale value for the whole axis
		m_single_scale = (m_adjdefvalue == m_adjmin) || (m_adjdefvalue == m_adjmax);

		// if not "single scale", compute separate scales for each side of the default
		if (!m_single_scale)
		{
			// unsigned, potentially passing through zero
			m_scalepos = compute_scale(
					(m_adjmax - m_adjdefvalue) & (field.mask() >> m_shift),
					osd::input_device::ABSOLUTE_MAX);
			m_scaleneg = compute_scale(
					(m_adjdefvalue - m_adjmin) & (field.mask() >> m_shift),
					-osd::input_device::ABSOLUTE_MIN);

			// reverse point is at center
			m_reverse_val = 0;
		}
		else
		{
			// single axis that increases from default
			m_scalepos = compute_scale(m_adjmax - m_adjmin, osd::input_device::ABSOLUTE_MAX - osd::input_device::ABSOLUTE_MIN);

			// make the scaling the same for easier coding when we need to scale
			m_scaleneg = m_scalepos;

			// reverse point is at max
			m_reverse_val = m_maximum;
		}
	}
	else
	{
		// relative and positional controls all map directly with a 512x scale factor

		// The relative code is set up to allow specifying PORT_MINMAX and default values.
		// The validity checks are purposely set up to not allow you to use anything other
		// a default of 0 and PORT_MINMAX(0,mask).  This is in case the need arises to use
		// this feature in the future.  Keeping the code in does not hurt anything.
		if (m_adjmin > m_adjmax)
			// adjust for signed
			m_adjmin = -m_adjmin;

		if (m_wraps)
			m_adjmax++;

		m_minimum = (m_adjmin - m_adjdefvalue) * osd::input_device::RELATIVE_PER_PIXEL;
		m_maximum = (m_adjmax - m_adjdefvalue) * osd::input_device::RELATIVE_PER_PIXEL;

		// make the scaling the same for easier coding when we need to scale
		m_scaleneg = m_scalepos = compute_scale(1, osd::input_device::RELATIVE_PER_PIXEL);

		if (m_field.analog_reset())
		{
			// delta values reverse from center
			m_reverse_val = 0;
		}
		else
		{
			// positional controls reverse from their max range
			m_reverse_val = m_maximum + m_minimum;

			// relative controls reverse from 1 past their max range
			if (m_wraps)
			{
				// FIXME: positional needs -1, using osd::input_device::RELATIVE_PER_PIXEL skips a position (and reads outside the table array)
				if (field.type() == IPT_POSITIONAL || field.type() == IPT_POSITIONAL_V)
					m_reverse_val--;
				else
					m_reverse_val -= osd::input_device::RELATIVE_PER_PIXEL;
			}
		}
	}

	// compute scale for keypresses
	m_keyscalepos = recip_scale(m_scalepos);
	m_keyscaleneg = recip_scale(m_scaleneg);
}


//-------------------------------------------------
//  apply_min_max - clamp the given input value to
//  the appropriate min/max for the analog control
//-------------------------------------------------

inline s32 analog_field::apply_min_max(s32 value) const
{
	// take the analog minimum and maximum values and apply the inverse of the
	// sensitivity so that we can clamp against them before applying sensitivity
	s32 adjmin = apply_inverse_sensitivity(m_minimum);
	s32 adjmax = apply_inverse_sensitivity(m_maximum);

	// clamp to the bounds absolutely
	if (value > adjmax)
		value = adjmax;
	else if (value < adjmin)
		value = adjmin;

	return value;
}


//-------------------------------------------------
//  apply_sensitivity - apply a sensitivity
//  adjustment for a current value
//-------------------------------------------------

inline s32 analog_field::apply_sensitivity(s32 value) const
{
	return lround((s64(value) * m_sensitivity) / 100.0);
}


//-------------------------------------------------
//  apply_inverse_sensitivity - reverse-apply the
//  sensitivity adjustment for a current value
//-------------------------------------------------

inline s32 analog_field::apply_inverse_sensitivity(s32 value) const
{
	return s32((s64(value) * 100) / m_sensitivity);
}


//-------------------------------------------------
//  apply_settings - return the value of an
//  analog input
//-------------------------------------------------

s32 analog_field::apply_settings(s32 value) const
{
	// apply the min/max and then the sensitivity
	if (!m_wraps)
		value = apply_min_max(value);
	value = apply_sensitivity(value);

	// apply reversal if needed
	if (m_reverse)
		value = m_reverse_val - value;
	else if (m_single_scale)
		// it's a pedal or the default value is equal to min/max
		// so we need to adjust the center to the minimum
		value -= osd::input_device::ABSOLUTE_MIN;

	// map differently for positive and negative values
	if (value >= 0)
		value = apply_scale(value, m_scalepos);
	else
		value = apply_scale(value, m_scaleneg);
	value += m_adjdefvalue;

	// for relative devices, wrap around when we go past the edge
	// (this is done last to prevent rounding errors)
	if (m_wraps)
	{
		s32 range = m_adjmax - m_adjmin;
		// rolls to other end when 1 position past end.
		value = (value - m_adjmin) % range;
		if (value < 0)
			value += range;
		value += m_adjmin;
	}

	return value;
}


//-------------------------------------------------
//  set_value - override the value that will be
//  read from the field
//-------------------------------------------------

void analog_field::set_value(s32 value)
{
	m_use_adjoverride = true;
	m_adjoverride = std::clamp(value, m_adjmin, m_adjmax);
}


//-------------------------------------------------
//  clear_value - clear programmatic override
//-------------------------------------------------

void analog_field::clear_value()
{
	m_use_adjoverride = false;
	m_adjoverride = m_adjdefvalue;
}


//-------------------------------------------------
//  frame_update - update the internals of a
//  single analog field periodically
//-------------------------------------------------

void analog_field::frame_update(running_machine &machine)
{
	// clamp the previous value to the min/max range
	if (!m_wraps)
		m_accum = apply_min_max(m_accum);

	// remember the previous value in case we need to interpolate
	m_previous = m_accum;

	// get the new raw analog value and its type
	input_item_class itemclass;
	s32 rawvalue = machine.input().seq_axis_value(m_field.seq(SEQ_TYPE_STANDARD), itemclass);

	// if we got an absolute input, it overrides everything else
	if (itemclass == ITEM_CLASS_ABSOLUTE)
	{
		if (!m_absolute && !m_positionalscale)
		{
			// if port is relative, we use the value to simulate the speed of relative movement
			// sensitivity adjustment is allowed for this mode
			if (rawvalue)
			{
				if (m_field.analog_reset())
					m_accum = rawvalue / 8;
				else
					m_accum += rawvalue / 8;

				// do not bother with other control types if the analog data is changing
				m_lastdigital = false;
				return;
			}
		}
		else if (m_previousanalog != rawvalue)
		{
			// only update if analog value changed
			m_previousanalog = rawvalue;

			// apply the inverse of the sensitivity to the raw value so that
			// it will still cover the full min->max range requested after
			// we apply the sensitivity adjustment
			if (m_absolute || m_field.analog_reset())
			{
				// if port is absolute, then just return the absolute data supplied
				m_accum = apply_inverse_sensitivity(rawvalue);
			}
			else
			{
				assert(m_positionalscale); // only way to get here due to previous if

				// if port is positional, we will take the full analog control and divide it
				// into positions, that way as the control is moved full scale,
				// it moves through all the positions
				rawvalue = apply_scale(rawvalue - osd::input_device::ABSOLUTE_MIN, m_positionalscale) * osd::input_device::RELATIVE_PER_PIXEL + m_minimum;

				// clamp the high value so it does not roll over
				rawvalue = std::min(rawvalue, m_maximum);
				m_accum = apply_inverse_sensitivity(rawvalue);
			}

			// do not bother with other control types if the analog data is changing
			m_lastdigital = false;
			return;
		}
	}

	// if we got it from a relative device, use that as the starting delta
	// also note that the last input was not a digital one
	s32 delta = 0;
	if (itemclass == ITEM_CLASS_RELATIVE && rawvalue)
	{
		delta = rawvalue;
		m_lastdigital = false;
	}

	s64 keyscale = (m_accum >= 0) ? m_keyscalepos : m_keyscaleneg;

	// if the decrement code sequence is pressed, add the key delta to
	// the accumulated delta; also note that the last input was a digital one
	bool keypressed = false;
	if (machine.input().seq_pressed(m_field.seq(SEQ_TYPE_DECREMENT)))
	{
		keypressed = true;
		if (m_delta != 0)
			delta -= apply_scale(m_delta, keyscale);
		else if (!m_lastdigital)
			// decrement only once when first pressed
			delta -= apply_scale(1, keyscale);
		m_lastdigital = true;
	}

	// same for the increment code sequence
	if (machine.input().seq_pressed(m_field.seq(SEQ_TYPE_INCREMENT)))
	{
		keypressed = true;
		if (m_delta)
			delta += apply_scale(m_delta, keyscale);
		else if (!m_lastdigital)
			// increment only once when first pressed
			delta += apply_scale(1, keyscale);
		m_lastdigital = true;
	}

	// if resetting is requested, clear the accumulated position to 0 before
	// applying the deltas so that we only return this frame's delta
	// note that centering only works for relative controls
	// no need to check if absolute here because it is checked by the validity tests
	if (m_field.analog_reset())
		m_accum = 0;

	// apply the delta to the accumulated value
	m_accum += delta;

	// if our last movement was due to a digital input, and if this control
	// type autocenters, and if neither the increment nor the decrement seq
	// was pressed, apply autocentering
	if (m_autocenter)
	{
		s32 center = apply_inverse_sensitivity(m_center);
		if (m_lastdigital && !keypressed)
		{
			if (m_accum >= center)
			{
				// autocenter from positive values
				m_accum -= apply_scale(m_centerdelta, m_keyscalepos);
				if (m_accum < center)
				{
					m_accum = center;
					m_lastdigital = false;
				}
			}
			else
			{
				// autocenter from negative values
				m_accum += apply_scale(m_centerdelta, m_keyscaleneg);
				if (m_accum > center)
				{
					m_accum = center;
					m_lastdigital = false;
				}
			}
		}
	}
	else if (!keypressed)
		m_lastdigital = false;
}


//-------------------------------------------------
//  read - read the current value and insert into
//  the provided ioport_value
//-------------------------------------------------

void analog_field::read(ioport_value &result)
{
	// do nothing if we're not enabled
	if (!m_field.enabled())
		return;

	// if set programmatically, only use the override value
	if (m_use_adjoverride)
	{
		result = m_adjoverride;
		return;
	}

	// start with the raw value
	s32 value = m_accum;

	// interpolate if appropriate and if time has passed since the last update
	if (m_interpolate)
		value = manager().frame_interpolate(m_previous, m_accum);

	// apply standard analog settings
	value = apply_settings(value);

	// remap the value if needed
	if (m_field.remap_table() != nullptr)
		value = m_field.remap_table()[value];

	// invert bits if needed
	if (m_field.analog_invert())
		value = ~value;

	// insert into the port
	result = (result & ~m_field.mask()) | ((value << m_shift) & m_field.mask());
}


//-------------------------------------------------
//  crosshair_read - read a value for crosshairs,
//  scaled between 0 and 1
//-------------------------------------------------

float analog_field::crosshair_read()
{
	s32 rawvalue = apply_settings(m_accum) & (m_field.mask() >> m_shift);
	return float(rawvalue - m_adjmin) / float(m_adjmax - m_adjmin);
}



/***************************************************************************
    TOKENIZATION HELPERS
***************************************************************************/

//-------------------------------------------------
//  token_to_input_type - convert a string token
//  to an input field type and player
//-------------------------------------------------

ioport_type ioport_manager::token_to_input_type(const char *string, int &player) const
{
	// check for our failsafe case first
	int ipnum;
	if (sscanf(string, "TYPE_OTHER(%d,%d)", &ipnum, &player) == 2)
		return ioport_type(ipnum);

	// find the token in the list
	for (const input_type_entry &entry : m_typelist)
		if (entry.token() != nullptr && !strcmp(entry.token(), string))
		{
			player = entry.player();
			return entry.type();
		}

	// if we fail, return IPT_UNKNOWN
	player = 0;
	return IPT_UNKNOWN;
}


//-------------------------------------------------
//  input_type_to_token - convert an input field
//  type and player to a string token
//-------------------------------------------------

std::string ioport_manager::input_type_to_token(ioport_type type, int player)
{
	// look up the port and return the token
	input_type_entry *entry = m_type_to_entry[type][player];
	if (entry != nullptr)
		return std::string(entry->token());

	// if that fails, carry on
	return string_format("TYPE_OTHER(%d,%d)", type, player);
}


//-------------------------------------------------
//  token_to_seq_type - convert a string to
//  a sequence type
//-------------------------------------------------

input_seq_type ioport_manager::token_to_seq_type(const char *string)
{
	// look up the string in the table of possible sequence types and return the index
	for (int seqindex = 0; seqindex < std::size(seqtypestrings); seqindex++)
		if (!core_stricmp(string, seqtypestrings[seqindex]))
			return input_seq_type(seqindex);
	return SEQ_TYPE_INVALID;
}
