// 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"

#include "path.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_footer(footer)
	, m_confirm_delete(nullptr)
	, m_must_exist(must_exist)
	, m_keys_released(false)
	, m_slot_selected(INPUT_CODE_INVALID)
{
	set_one_shot(one_shot);
	set_needs_prev_menu_item(!one_shot);
	set_heading(header);
}


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

menu_load_save_state_base::~menu_load_save_state_base()
{
}


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

void menu_load_save_state_base::populate()
{
	// 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);

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


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

bool menu_load_save_state_base::handle(event const *ev)
{
	// process the event
	if (INPUT_CODE_INVALID != m_slot_selected)
	{
		if (!machine().input().code_pressed(m_slot_selected))
			stack_pop();
		return false;
	}
	else 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()));
		}
		stack_pop();
		return false;
	}
	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_BACK));
			return true;
		}
		else
		{
			return false;
		}
	}
	else if (!m_confirm_delete)
	{
		// poll inputs
		input_code code;
		std::string name = poll_inputs(code);
		if (!name.empty() && try_select_slot(std::move(name)))
		{
			m_switch_poller.reset();
			m_slot_selected = code;
		}
		return false;
	}
	else
	{
		return false;
	}
}


//-------------------------------------------------
//  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 &code)
{
	input_code const result = m_switch_poller.poll();
	if (INPUT_CODE_INVALID == result)
	{
		m_keys_released = true;
	}
	else if (m_keys_released)
	{
		input_item_id const id = result.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)))
		{
			code = result;
			return keyboard_input_item_name(id);
		}

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


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

bool menu_load_save_state_base::try_select_slot(std::string &&name)
{
	if (!m_must_exist || is_present(name))
	{
		slot_selected(std::move(name));
		return true;
	}
	else
	{
		return false;
	}
}


//-------------------------------------------------
//  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);
}


//-------------------------------------------------
//  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::path_concat(
						machine().options().state_directory(),
						machine().get_statename(machine().options().state_name()),
						m_confirm_delete->file_name() + ".sta"));
			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_BACK, 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 if (INPUT_CODE_INVALID != m_slot_selected)
	{
		iptkey = IPT_INVALID;
	}
	else
	{
		menu::handle_keys(flags, iptkey);
	}
}


//-------------------------------------------------
//  recompute_metrics - recompute metrics
//-------------------------------------------------

void menu_load_save_state_base::recompute_metrics(uint32_t width, uint32_t height, float aspect)
{
	autopause_menu<>::recompute_metrics(width, height, aspect);

	// set up custom render proc
	set_custom_space(0.0F, (2.0F * line_height()) + (3.0F * tb_border()));
}


//-------------------------------------------------
//  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)
{
	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"), ui().get_general_input_setting(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 + tb_border(), origy2 + (count * line_height()) + (3.0F * tb_border()),
				text_layout::text_justify::CENTER, text_layout::word_wrapping::NEVER, false,
				ui().colors().text_color(), ui().colors().background_color());
	}

	// 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
{
	return util::path_concat(
			machine().options().state_directory(),
			machine().get_statename(machine().options().state_name()));
}


//-------------------------------------------------
//  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
