// license:BSD-3-Clause
// copyright-holders:Nathan Woods
/***************************************************************************

    ui/state.cpp

    Menus for saving and loading state

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

#include "emu.h"
#include "ui/state.h"

#include "emuopts.h"
#include "inputdev.h"


namespace ui {

/***************************************************************************
    ANONYMOUS NAMESPACE
***************************************************************************/

namespace {

//-------------------------------------------------
//  keyboard_input_item_name
//-------------------------------------------------

std::string keyboard_input_item_name(input_item_id id)
{
	if (id >= ITEM_ID_A && id <= ITEM_ID_Z)
		return std::string(1, char(id - ITEM_ID_A + 'a'));
	if (id >= ITEM_ID_0 && id <= ITEM_ID_9)
		return std::string(1, char(id - ITEM_ID_0 + '0'));

	// only supported for A-Z/0-9
	throw false;
}


//-------------------------------------------------
//  code_item_pair
//-------------------------------------------------

std::pair<std::string, std::string> code_item_pair(const running_machine &machine, input_item_id id)
{
	// only supported for A-Z|0-9
	assert((id >= ITEM_ID_A && id <= ITEM_ID_Z) || (id >= ITEM_ID_0 && id <= ITEM_ID_9));
	input_code const code = input_code(DEVICE_CLASS_KEYBOARD, 0, ITEM_CLASS_SWITCH, ITEM_MODIFIER_NONE, id);

	return std::make_pair(keyboard_input_item_name(id), machine.input().code_name(code));
}

} // anonymous namespace


/***************************************************************************
    FILE ENTRY
***************************************************************************/

std::string menu_load_save_state_base::s_last_file_selected;


//-------------------------------------------------
//  file_entry ctor
//-------------------------------------------------

menu_load_save_state_base::file_entry::file_entry(std::string &&file_name, std::string &&visible_name, const std::chrono::system_clock::time_point &last_modified)
	: m_file_name(std::move(file_name))
	, m_visible_name(std::move(visible_name))
	, m_last_modified(last_modified)
{
}


/***************************************************************************
    BASE CLASS FOR LOAD AND SAVE
***************************************************************************/

//-------------------------------------------------
//  ctor
//-------------------------------------------------

menu_load_save_state_base::menu_load_save_state_base(
		mame_ui_manager &mui,
		render_container &container,
		std::string_view header,
		std::string_view footer,
		bool must_exist,
		bool one_shot)
	: autopause_menu<>(mui, container)
	, m_switch_poller(machine().input())
	, m_header(header)
	, m_footer(footer)
	, m_confirm_delete(nullptr)
	, m_must_exist(must_exist)
	, m_keys_released(false)
{
	set_one_shot(one_shot);
	set_needs_prev_menu_item(!one_shot);
}


//-------------------------------------------------
//  dtor
//-------------------------------------------------

menu_load_save_state_base::~menu_load_save_state_base()
{
}


//-------------------------------------------------
//  populate
//-------------------------------------------------

void menu_load_save_state_base::populate(float &customtop, float &custombottom)
{
	// build the "filename to code" map, if we have not already (if it were not for the
	// possibility that the system keyboard can be changed at runtime, I would put this
	// into a static)
	if (m_filename_to_code_map.empty())
	{
		// loop through A-Z/0-9
		for (input_item_id id = ITEM_ID_A; id <= ITEM_ID_Z; id++)
			m_filename_to_code_map.emplace(code_item_pair(machine(), id));
		for (input_item_id id = ITEM_ID_0; id <= ITEM_ID_9; id++)
			m_filename_to_code_map.emplace(code_item_pair(machine(), id));

		// do joysticks
		input_class const &sticks = machine().input().device_class(DEVICE_CLASS_JOYSTICK);
		if (sticks.enabled())
		{
			for (int i = 0; sticks.maxindex() >= i; ++i)
			{
				input_device const *const stick = sticks.device(i);
				if (stick)
				{
					for (input_item_id j = ITEM_ID_BUTTON1; (ITEM_ID_BUTTON32 >= j) && (stick->maxitem() >= j); ++j)
					{
						input_device_item const *const item = stick->item(j);
						if (item && (item->itemclass() == ITEM_CLASS_SWITCH))
						{
							m_filename_to_code_map.emplace(
									util::string_format("joy%i-%i", i, j - ITEM_ID_BUTTON1 + 1),
									machine().input().code_name(item->code()));
						}
					}
				}
			}
		}
	}

	// open the state directory
	osd::directory::ptr dir = osd::directory::open(state_directory());

	// create a separate vector, so we can add sorted entries to the menu
	std::vector<const file_entry *> m_entries_vec;

	// populate all file entries
	m_file_entries.clear();
	if (dir)
	{
		const osd::directory::entry *entry;
		while ((entry = dir->read()) != nullptr)
		{
			if (core_filename_ends_with(entry->name, ".sta"))
			{
				// get the file name of the entry
				std::string file_name(core_filename_extract_base(entry->name, true));

				// try translating it
				std::string visible_name = get_visible_name(file_name);

				// and proceed
				file_entry fileent(std::string(file_name), std::move(visible_name), entry->last_modified);
				auto iter = m_file_entries.emplace(std::make_pair(std::move(file_name), std::move(fileent))).first;
				m_entries_vec.push_back(&iter->second);
			}
		}
	}

	// sort the vector; put recently modified state files at the top
	std::sort(
			m_entries_vec.begin(),
			m_entries_vec.end(),
			[] (const file_entry *a, const file_entry *b)
			{
				return a->last_modified() > b->last_modified();
			});

	// add the entries
	for (const file_entry *entry : m_entries_vec)
	{
		// get the time as a local time string
		char time_string[128];
		auto last_modified_time_t = std::chrono::system_clock::to_time_t(entry->last_modified());
		std::strftime(time_string, sizeof(time_string), "%c", std::localtime(&last_modified_time_t));

		// format the text
		std::string text = util::string_format("%s: %s",
				entry->visible_name(),
				time_string);

		// append the menu item
		void *const itemref = itemref_from_file_entry(*entry);
		item_append(std::move(text), 0, itemref);

		// is this item selected?
		if (entry->file_name() == s_last_file_selected)
			set_selection(itemref);
	}

	if (m_entries_vec.empty())
	{
		item_append(_("[no saved states found]"), FLAG_DISABLE, nullptr);
		set_selection(nullptr);
	}
	item_append(menu_item_type::SEPARATOR);
	if (is_one_shot())
		item_append(_("Cancel"), 0, nullptr);

	// set up custom render proc
	customtop = ui().get_line_height() + (3.0f * ui().box_tb_border());
	custombottom = (2.0f * ui().get_line_height()) + (3.0f * ui().box_tb_border());

	// get ready to poll inputs
	m_switch_poller.reset();
	m_keys_released = false;
}


//-------------------------------------------------
//  handle
//-------------------------------------------------

void menu_load_save_state_base::handle(event const *ev)
{
	// process the event
	if (ev && (ev->iptkey == IPT_UI_SELECT))
	{
		if (ev->itemref)
		{
			// user selected one of the entries
			file_entry const &entry = file_entry_from_itemref(ev->itemref);
			slot_selected(std::string(entry.file_name()));
		}
		else
		{
			stack_pop();
		}
	}
	else if (ev && (ev->iptkey == IPT_UI_CLEAR))
	{
		if (ev->itemref)
		{
			// prompt to confirm delete
			m_confirm_delete = &file_entry_from_itemref(ev->itemref);
			m_confirm_prompt = util::string_format(
					_("Delete saved state %1$s?\nPress %2$s to delete\nPress %3$s to cancel"),
					m_confirm_delete->visible_name(),
					ui().get_general_input_setting(IPT_UI_SELECT),
					ui().get_general_input_setting(IPT_UI_CANCEL));
		}
	}
	else if (!m_confirm_delete)
	{
		// poll inputs
		std::string name = poll_inputs();
		if (!name.empty())
			try_select_slot(std::move(name));
	}
}


//-------------------------------------------------
//  get_visible_name
//-------------------------------------------------

std::string menu_load_save_state_base::get_visible_name(const std::string &file_name)
{
	auto const iter = m_filename_to_code_map.find(file_name);
	if (iter != m_filename_to_code_map.end())
		return iter->second;

	// otherwise these are the same
	return file_name;
}


//-------------------------------------------------
//  poll_inputs
//-------------------------------------------------

std::string menu_load_save_state_base::poll_inputs()
{
	input_code const code = m_switch_poller.poll();
	if (INPUT_CODE_INVALID == code)
	{
		m_keys_released = true;
	}
	else if (m_keys_released)
	{
		input_item_id const id = code.item_id();

		// keyboard A-Z and 0-9
		if (((ITEM_ID_A <= id) && (ITEM_ID_Z >= id)) || ((ITEM_ID_0 <= id) && (ITEM_ID_9 >= id)))
			return keyboard_input_item_name(id);

		// joystick buttons
		if ((DEVICE_CLASS_JOYSTICK == code.device_class()) && (ITEM_CLASS_SWITCH == code.item_class()) && (ITEM_MODIFIER_NONE == code.item_modifier()) && (ITEM_ID_BUTTON1 <= id) && (ITEM_ID_BUTTON32 >= id))
			return util::string_format("joy%i-%i", code.device_index(), id - ITEM_ID_BUTTON1 + 1);
	}
	return "";
}


//-------------------------------------------------
//  try_select_slot
//-------------------------------------------------

void menu_load_save_state_base::try_select_slot(std::string &&name)
{
	if (!m_must_exist || is_present(name))
		slot_selected(std::move(name));
}


//-------------------------------------------------
//  slot_selected
//-------------------------------------------------

void menu_load_save_state_base::slot_selected(std::string &&name)
{
	// handle it
	process_file(std::string(name));

	// record the last slot touched
	s_last_file_selected = std::move(name);

	// no matter what, pop out
	menu::stack_pop();
}


//-------------------------------------------------
//  handle_keys - override key handling
//-------------------------------------------------

void menu_load_save_state_base::handle_keys(uint32_t flags, int &iptkey)
{
	if (m_confirm_delete)
	{
		if (exclusive_input_pressed(iptkey, IPT_UI_SELECT, 0))
		{
			// try to remove the file
			std::string const filename(util::string_format(
						"%2$s%1$s%3$s%1$s%4$s.sta",
						PATH_SEPARATOR,
						machine().options().state_directory(),
						machine().get_statename(machine().options().state_name()),
						m_confirm_delete->file_name()));
			std::error_condition const err(osd_file::remove(filename));
			if (err)
			{
				osd_printf_error(
						"Error removing file %s for state %s (%s:%d %s)\n",
						filename,
						m_confirm_delete->visible_name(),
						err.category().name(),
						err.value(),
						err.message());
				machine().popmessage(_("Error removing saved state file %1$s"), filename);
			}

			// repopulate the menu
			// reset switch poller here to avoid bogus save/load if confirmed with joystick button
			m_switch_poller.reset();
			m_confirm_prompt.clear();
			m_confirm_delete = nullptr;
			m_keys_released = false;
			reset(reset_options::REMEMBER_POSITION);
		}
		else if (exclusive_input_pressed(iptkey, IPT_UI_CANCEL, 0))
		{
			// don't delete it - dismiss the prompt
			m_switch_poller.reset();
			m_confirm_prompt.clear();
			m_confirm_delete = nullptr;
			m_keys_released = false;
		}
		iptkey = IPT_INVALID;
	}
	else
	{
		menu::handle_keys(flags, iptkey);
	}
}


//-------------------------------------------------
//  custom_render - perform our special rendering
//-------------------------------------------------

void menu_load_save_state_base::custom_render(void *selectedref, float top, float bottom, float origx1, float origy1, float origx2, float origy2)
{
	// draw menu title
	draw_text_box(
			&m_header, &m_header + 1,
			origx1, origx2, origy1 - top, origy1 - ui().box_tb_border(),
			text_layout::text_justify::CENTER, text_layout::word_wrapping::TRUNCATE, false,
			ui().colors().text_color(), UI_GREEN_COLOR, 1.0f);

	std::string_view text[2];
	unsigned count(0U);

	// add fixed footer if supplied
	if (!m_footer.empty())
		text[count++] = m_footer;

	// provide a prompt to delete if a state is selected
	if (selected_item().ref())
	{
		if (m_delete_prompt.empty())
			m_delete_prompt = util::string_format(_("Press %1$s to delete"), machine().input().seq_name(machine().ioport().type_seq(IPT_UI_CLEAR)));
		text[count++] = m_delete_prompt;
	}

	// draw the footer box if necessary
	if (count)
	{
		draw_text_box(
				std::begin(text), std::next(std::begin(text), count),
				origx1, origx2, origy2 + ui().box_tb_border(), origy2 + (count * ui().get_line_height()) + (3.0f * ui().box_tb_border()),
				text_layout::text_justify::CENTER, text_layout::word_wrapping::NEVER, false,
				ui().colors().text_color(), ui().colors().background_color(), 1.0f);
	}

	// draw the confirmation prompt if necessary
	if (!m_confirm_prompt.empty())
		ui().draw_text_box(container(), m_confirm_prompt, text_layout::text_justify::CENTER, 0.5f, 0.5f, ui().colors().background_color());
}


//-------------------------------------------------
//  itemref_from_file_entry
//-------------------------------------------------

void *menu_load_save_state_base::itemref_from_file_entry(const menu_load_save_state_base::file_entry &entry)
{
	return (void *)&entry;
}


//-------------------------------------------------
//  file_entry_from_itemref
//-------------------------------------------------

const menu_load_save_state_base::file_entry &menu_load_save_state_base::file_entry_from_itemref(void *itemref)
{
	return *((const file_entry *)itemref);
}


//-------------------------------------------------
//  state_name
//-------------------------------------------------

std::string menu_load_save_state_base::state_directory() const
{
	const char *stateopt = machine().options().state_name();
	return util::string_format("%s%s%s",
			machine().options().state_directory(),
			PATH_SEPARATOR,
			machine().get_statename(stateopt));
}


//-------------------------------------------------
//  is_present
//-------------------------------------------------

bool menu_load_save_state_base::is_present(const std::string &name) const
{
	return m_file_entries.find(name) != m_file_entries.end();
}


/***************************************************************************
    LOAD STATE
***************************************************************************/

//-------------------------------------------------
//  ctor
//-------------------------------------------------

menu_load_state::menu_load_state(mame_ui_manager &mui, render_container &container, bool one_shot)
	: menu_load_save_state_base(mui, container, _("Load State"), _("Select state to load"), true, one_shot)
{
}


//-------------------------------------------------
//  process_file
//-------------------------------------------------

void menu_load_state::process_file(std::string &&file_name)
{
	machine().schedule_load(std::move(file_name));
}


/***************************************************************************
    SAVE STATE
***************************************************************************/

//-------------------------------------------------
//  ctor
//-------------------------------------------------

menu_save_state::menu_save_state(mame_ui_manager &mui, render_container &container, bool one_shot)
	: menu_load_save_state_base(mui, container, _("Save State"), _("Press a key or joystick button, or select state to overwrite"), false, one_shot)
{
}


//-------------------------------------------------
//  process_file
//-------------------------------------------------

void menu_save_state::process_file(std::string &&file_name)
{
	machine().schedule_save(std::move(file_name));
}


} // namespace ui
