/*
 *  Copyright (C) 2001-2022 The Exult Team
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include "SDL_keyboard.h"

#include "actors.h"
#include "keys.h"
#include "exult.h"
#include "game.h"
#include "cheat.h"
#include "U7file.h"
#include "ucmachine.h"
#include "Scroll_gump.h"
#include "mouse.h"
#include "gamewin.h"
#include "utils.h"
#include "keyactions.h"

using std::atoi;
using std::cerr;
using std::cout;
using std::endl;
using std::ifstream;
using std::isspace;
using std::strchr;
using std::string;
using std::strlen;

static  class Chardata { // ctype-like character lists
public:
	std::string whitespace;
	Chardata() {
		for (size_t i = 0; i < 256; i++)
			if (isspace(i))
				whitespace += static_cast<char>(i);
	}
} chardata;

using ActionFunc = void (*)(int const *);

const struct Action {
	const char *s;
	ActionFunc func;
	ActionFunc func_release;
	const char *desc;
	enum {
	    dont_show = 0,
	    normal_keys,
	    cheat_keys,
	    mapedit_keys
	} key_type;
	Exult_Game game;
	bool allow_during_dont_move;
	bool allow_if_cant_act;
	bool allow_if_cant_act_charmed;
	bool is_movement;
} ExultActions[] = {
	{ "QUIT", ActionQuit, nullptr, "Quit", Action::normal_keys, NONE, true, true, true, false },
	{ "SAVE_RESTORE", ActionFileGump, nullptr, "Save/restore", Action::normal_keys, NONE, true, true, true, false },
	{ "QUICKSAVE", ActionQuicksave, nullptr, "Quick-save", Action::normal_keys, NONE, false, true, true, false },
	{
		"QUICKRESTORE",
		ActionQuickrestore, nullptr, "Quick-restore", Action::normal_keys, NONE, true, true, true, false
	},
	{ "ABOUT", ActionAbout, nullptr, "About Exult", Action::normal_keys, NONE, false, true, true, false },
	{ "HELP", ActionHelp, nullptr, "List keys", Action::normal_keys, NONE, false, true, true, false },
	{ "CLOSE_GUMPS", ActionCloseGumps, nullptr, "Close gumps", Action::dont_show, NONE, false, true, true, false },
	{ "CLOSE_OR_MENU", ActionCloseOrMenu, nullptr, "Game menu", Action::normal_keys, NONE, true, true, true, false },
	{
		"SCREENSHOT",
		ActionScreenshot, nullptr, "Take screenshot", Action::normal_keys, NONE, true, true, true, false
	},
	{ "GAME_MENU", ActionMenuGump, nullptr, "Game Menu", Action::normal_keys, NONE, true, true, true, false },
	{
		"OLD_FILE_GUMP",
		ActionOldFileGump, nullptr, "Save/restore", Action::normal_keys, NONE, true, true, true, false
	},
	{
		"SCALEVAL_INCREASE",
		ActionScalevalIncrease, nullptr, "Increase scaleval", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"SCALEVAL_DECREASE",
		ActionScalevalDecrease, nullptr, "Decrease scaleval", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"BRIGHTER",
		ActionBrighter, nullptr, "Increase brightness", Action::normal_keys, NONE, true, true, true, false
	},
	{ "DARKER", ActionDarker, nullptr, "Decrease brightness", Action::normal_keys, NONE, true, true, true, false },
	{
		"TOGGLE_FULLSCREEN",
		ActionFullscreen, nullptr, "Toggle fullscreen", Action::normal_keys, NONE, true, true, true, false
	},

	{ "USEITEM", ActionUseItem, nullptr, "Use item", Action::dont_show, NONE, false, false, false, false },
	{ "USEFOOD", ActionUseFood, nullptr, "Feed", Action::normal_keys, NONE, false, false, false, false },
	{ "CALL_USECODE", ActionCallUsecode, nullptr, "Call Usecode", Action::dont_show, NONE, false, false, false, false },
	{ "TOGGLE_COMBAT", ActionCombat, nullptr, "Toggle combat", Action::normal_keys, NONE, false, false, true, false },
	{ "PAUSE_COMBAT", ActionCombatPause, nullptr, "Pause combat", Action::normal_keys, NONE, false, false, true, false },
	{ "TARGET_MODE", ActionTarget, nullptr, "Target mode", Action::normal_keys, NONE, false, false, true, false },
	{ "INVENTORY", ActionInventory, nullptr, "Show inventory", Action::normal_keys, NONE, false, false, true, false },
	{ "TRY_KEYS", ActionTryKeys, nullptr, "Try keys", Action::normal_keys, NONE, false, false, false, false },
	{ "STATS", ActionStats, nullptr, "Show stats", Action::normal_keys, NONE, false, true, true, false },
	{
		"COMBAT_STATS",
		ActionCombatStats, nullptr, "Show combat stats", Action::normal_keys, SERPENT_ISLE, false, false, true, false
	},
	{
		"FACE_STATS",
		ActionFaceStats, nullptr, "Change Face Stats State", Action::normal_keys, NONE, false, true, true, false
	},
	{ "USE_HEALING_ITEMS", ActionUseHealingItems, nullptr, "Use bandages or healing potions", Action::normal_keys, NONE, false, false, false, false },
	{
		"SHOW_SI_INTRO",
		ActionSIIntro, nullptr, "Show Alternate SI intro", Action::cheat_keys, SERPENT_ISLE, false, true, true, false
	},
	{ "SHOW_ENDGAME", ActionEndgame, nullptr, "Show endgame", Action::cheat_keys, NONE, false, true, true, false },
	{ "SCROLL_LEFT", ActionScrollLeft, nullptr, "Scroll left", Action::cheat_keys, NONE, false, true , true, false},
	{ "SCROLL_RIGHT", ActionScrollRight, nullptr, "Scroll right", Action::cheat_keys, NONE, false, true, true, false },
	{ "SCROLL_UP", ActionScrollUp, nullptr, "Scroll up", Action::cheat_keys, NONE, false, true, true, false },
	{ "SCROLL_DOWN", ActionScrollDown, nullptr, "Scroll down", Action::cheat_keys, NONE, false, true, true, false },
	{ "WALK_WEST", ActionWalkWest, ActionStopWalking, "Walk west", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_EAST", ActionWalkEast, ActionStopWalking, "Walk east", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_NORTH", ActionWalkNorth, ActionStopWalking, "Walk north", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_SOUTH", ActionWalkSouth, ActionStopWalking, "Walk south", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_NORTH_EAST", ActionWalkNorthEast, ActionStopWalking, "Walk north-east", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_SOUTH_EAST", ActionWalkSouthEast, ActionStopWalking, "Walk south-east", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_NORTH_WEST", ActionWalkNorthWest, ActionStopWalking, "Walk north-west", Action::normal_keys, NONE, false, false, false, true },
	{ "WALK_SOUTH_WEST", ActionWalkSouthWest, ActionStopWalking, "Walk south-west", Action::normal_keys, NONE, false, false, false, true },
	{ "CENTER_SCREEN", ActionCenter, nullptr, "Center screen", Action::cheat_keys, NONE, false, true, true, false },
	{
		"SHAPE_BROWSER",
		ActionShapeBrowser, nullptr, "Shape browser", Action::cheat_keys, NONE, false, true, true, false
	},
	{ "SHAPE_BROWSER_HELP", ActionShapeBrowserHelp, nullptr, "List shape browser keys", Action::cheat_keys, NONE, false, true, true, false },
	{
		"CREATE_ITEM",
		ActionCreateShape, nullptr, "Create last shape", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"DELETE_OBJECT",
		ActionDeleteObject, nullptr, "Delete object", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_EGGS",
		ActionToggleEggs, nullptr, "Toggle egg display", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_GOD_MODE",
		ActionGodMode, nullptr, "Toggle god mode", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"CHANGE_GENDER",
		ActionGender, nullptr, "Change gender", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"CHEAT_HELP",
		ActionCheatHelp, nullptr, "List cheat keys", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_INFRAVISION",
		ActionInfravision, nullptr, "Toggle infravision", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_HACK_MOVER",
		ActionHackMover, nullptr, "Toggle hack-mover mode", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"MAP_TELEPORT",
		ActionMapTeleport, nullptr, "Map teleport", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"CURSOR_TELEPORT",
		ActionTeleport, nullptr, "Teleport to cursor", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TARGET_MODE_TELEPORT",
		ActionTeleportTargetMode, nullptr, "Bring up cursor to teleport", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"NEXT_MAP_TELEPORT",
		ActionNextMapTeleport, nullptr, "Teleport to next map", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"NEXT_TIME_PERIOD",
		ActionTime, nullptr, "Next time period", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_WIZARD_MODE",
		ActionWizard, nullptr, "Toggle archwizard mode", Action::cheat_keys, NONE, false, true, true, false
	},
	{ "PARTY_HEAL", ActionHeal, nullptr, "Heal party", Action::cheat_keys, NONE, false, true, true, false },
	{
		"PARTY_INCREASE_LEVEL",
		ActionLevelup, nullptr, "Level-up party", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"CHEAT_SCREEN",
		ActionCheatScreen, nullptr, "Cheat Screen", Action::cheat_keys, NONE, true, true, true, false
	},
	{
		"PICK_POCKET",
		ActionPickPocket, nullptr, "Toggle Pick Pocket", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"NPC_NUMBERS",
		ActionNPCNumbers, nullptr, "Toggle NPC Numbers", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"GRAB_ACTOR",
		ActionGrabActor, nullptr, "Grab NPC for Cheat Screen", Action::cheat_keys, NONE, false, true, true, false
	},
	{ "PLAY_MUSIC", ActionPlayMusic, nullptr, "Play song", Action::cheat_keys, NONE, false, true, true, false },
	{
		"TOGGLE_NAKED",
		ActionNaked, nullptr, "Toggle naked mode", Action::cheat_keys, SERPENT_ISLE, false, true, true, false
	},
	{
		"TOGGLE_PETRA",
		ActionPetra, nullptr, "Toggle Petra mode", Action::cheat_keys, SERPENT_ISLE, false, true, true, false
	},
	{
		"CHANGE_SKIN",
		ActionSkinColour, nullptr, "Change skin colour", Action::cheat_keys, NONE, false, true, true, false
	},
	{
		"NOTEBOOK", ActionNotebook, nullptr, "Show notebook", Action::normal_keys,
		NONE, false, false, false, false
	},
	{
		"SOUND_TESTER",
		ActionSoundTester, nullptr, "Sound tester", Action::cheat_keys, NONE, false, true, true, false
	},
	{ "TEST", ActionTest, nullptr, "Test", Action::dont_show, NONE, false, true, true, false },

	{
		"MAPEDIT_HELP",
		ActionMapeditHelp, nullptr, "List mapedit keys", Action::mapedit_keys, NONE, false, true, true, false
	},
	{
		"TOGGLE_MAP_EDITOR",
		ActionMapEditor, nullptr, "Toggle map-editor mode", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"SKIPLIFT_DECREMENT",
		ActionSkipLift, nullptr, "Decrement skiplift", Action::mapedit_keys, NONE, false, true, true, false
	},
	{
		"CUT",
		ActionCut, nullptr, "Cut Selected Objects", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"COPY",
		ActionCopy, nullptr, "Copy Selected Objects", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"PASTE",
		ActionPaste, nullptr, "Paste Selected Objects", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"DELETE_SELECTED",
		ActionDeleteSelected, nullptr, "Delete selected", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"MOVE_SELECTED",
		ActionMoveSelected, nullptr, "Move selected", Action::mapedit_keys, NONE, true, true, true, false
	},
	{
		"WRITE_MINIMAP",
		ActionWriteMiniMap, nullptr, "Write minimap", Action::mapedit_keys, NONE, false, true, true, false
	},
	{ "REPAINT", ActionRepaint, nullptr, "Repaint screen", Action::dont_show, NONE, true, true, true, false },
	{ "", nullptr, nullptr, "", Action::dont_show, NONE, false, false, false, false } //terminator
};

const struct {
	const char *s;
	SDL_Keycode k;
} SDLKeyStringTable[] = {
	{"BACKSPACE", SDLK_BACKSPACE},
	{"TAB",       SDLK_TAB},
	{"ENTER",     SDLK_RETURN},
	{"PAUSE",     SDLK_PAUSE},
	{"ESC",       SDLK_ESCAPE},
	{"SPACE",     SDLK_SPACE},
	{"DEL",       SDLK_DELETE},
	{"KP0",       SDLK_KP_0},
	{"KP1",       SDLK_KP_1},
	{"KP2",       SDLK_KP_2},
	{"KP3",       SDLK_KP_3},
	{"KP4",       SDLK_KP_4},
	{"KP5",       SDLK_KP_5},
	{"KP6",       SDLK_KP_6},
	{"KP7",       SDLK_KP_7},
	{"KP8",       SDLK_KP_8},
	{"KP9",       SDLK_KP_9},
	{"KP0",       SDLK_KP_0},
	{"KP.",       SDLK_KP_PERIOD},
	{"KP/",       SDLK_KP_DIVIDE},
	{"KP*",       SDLK_KP_MULTIPLY},
	{"KP-",       SDLK_KP_MINUS},
	{"KP+",       SDLK_KP_PLUS},
	{"KP_ENTER",  SDLK_KP_ENTER},
	{"UP",        SDLK_UP},
	{"DOWN",      SDLK_DOWN},
	{"RIGHT",     SDLK_RIGHT},
	{"LEFT",      SDLK_LEFT},
	{"INSERT",    SDLK_INSERT},
	{"HOME",      SDLK_HOME},
	{"END",       SDLK_END},
	{"PAGEUP",    SDLK_PAGEUP},
	{"PAGEDOWN",  SDLK_PAGEDOWN},
	{"F1",        SDLK_F1},
	{"F2",        SDLK_F2},
	{"F3",        SDLK_F3},
	{"F4",        SDLK_F4},
	{"F5",        SDLK_F5},
	{"F6",        SDLK_F6},
	{"F7",        SDLK_F7},
	{"F8",        SDLK_F8},
	{"F9",        SDLK_F9},
	{"F10",       SDLK_F10},
	{"F11",       SDLK_F11},
	{"F12",       SDLK_F12},
	{"F13",       SDLK_F13},
	{"F14",       SDLK_F14},
	{"F15",       SDLK_F15},
	{"", SDLK_UNKNOWN} // terminator
};


using ParseKeyMap = std::map<std::string, SDL_Keycode>;
using ParseActionMap = std::map<std::string, const Action *>;

static ParseKeyMap keys;
static ParseActionMap actions;


KeyBinder::KeyBinder() {
	FillParseMaps();
}

void KeyBinder::AddKeyBinding(SDL_Keycode key, int mod, const Action *action,
                              int nparams, const int *params) {
	SDL_Keysym k;
	ActionType a;
	k.scancode = static_cast<SDL_Scancode>(0);
	k.sym      = key;
	k.mod      = static_cast<SDL_Keymod>(mod);
	a.action    = action;
	int i;  // For MSVC
	for (i = 0; i < c_maxparams && i < nparams; i++)
		a.params[i] = params[i];
	for (i = nparams; i < c_maxparams; i++)
		a.params[i] = -1;

	bindings[k] = a;
}

bool KeyBinder::DoAction(ActionType const &a, bool press) const {
	if (!cheat() && (a.action->key_type == Action::cheat_keys
	                 || a.action->key_type == Action::mapedit_keys))
		return true;
	if (a.action->game != NONE && a.action->game != Game::get_game_type())
		return true;

	// Restrict key actions in dont_move mode
	if (!a.action->allow_during_dont_move
	        && Game_window::get_instance()->main_actor_dont_move())
		return true;

	// Restrict keys if avatar is sleeping/paralyzed/unconscious/dead
	if (!a.action->allow_if_cant_act
	        && !Game_window::get_instance()->main_actor_can_act())
		return true;
	if (!a.action->allow_if_cant_act_charmed
	        && !Game_window::get_instance()->main_actor_can_act_charmed())
		return true;
	if (press) {
#ifndef USE_EXULTSTUDIO
		// Display unsupported message if no ES support.
		if (a.action->key_type == Action::mapedit_keys) {
			Scroll_gump *scroll;
			scroll = new Scroll_gump();

			scroll->add_text("Map editor functionality is disabled in this version of Exult.\n");
			scroll->add_text("If you want or need this functionality, you should install a snapshot, as they have support for Exult Studio.\n");
			scroll->add_text("If no snapshots are available for your platform, you will have to compile it on your own.\n");

			scroll->paint();
			do {
				int x, y;
				Get_click(x, y, Mouse::hand, nullptr, false, scroll);
			} while (scroll->show_next_page());
			Game_window::get_instance()->paint();
			delete scroll;
			return true;
		}
#endif
		a.action->func(a.params);
	} else {
#ifndef USE_EXULTSTUDIO
		// Do nothing when releasing mapedit keys if no ES support.
		if (a.action->key_type == Action::mapedit_keys)
			return true;
#endif
		if (a.action->func_release != nullptr)
			a.action->func_release(a.params);
	}

	return true;
}

KeyMap::const_iterator KeyBinder::TranslateEvent(SDL_Event const &ev) const {
	SDL_Keysym key = ev.key.keysym;

	if (ev.type != SDL_KEYDOWN && ev.type != SDL_KEYUP)
		return bindings.end();

	key.mod = KMOD_NONE;
	if (ev.key.keysym.mod & KMOD_SHIFT)
		key.mod = static_cast<SDL_Keymod>(key.mod | KMOD_SHIFT);
	if (ev.key.keysym.mod & KMOD_CTRL)
		key.mod = static_cast<SDL_Keymod>(key.mod | KMOD_CTRL);
#ifdef MACOSX
	// map Meta to Alt on OS X
	if (ev.key.keysym.mod & KMOD_GUI)
		key.mod = static_cast<SDL_Keymod>(key.mod | KMOD_ALT);
#else
	if (ev.key.keysym.mod & KMOD_ALT)
		key.mod = static_cast<SDL_Keymod>(key.mod | KMOD_ALT);
#endif

	return bindings.find(key);
}

bool KeyBinder::HandleEvent(SDL_Event const &ev) const {
	auto sdlkey_index = TranslateEvent(ev);
	if (sdlkey_index != bindings.end())
		return DoAction(sdlkey_index->second, ev.type == SDL_KEYDOWN);

	return false;
}

bool KeyBinder::IsMotionEvent(SDL_Event const &ev) const {
	auto sdlkey_index = TranslateEvent(ev);
	if (sdlkey_index == bindings.end())
		return false;
	ActionType const &act = sdlkey_index->second;
	return act.action->is_movement;
}

void KeyBinder::ShowHelp() const {
	Scroll_gump *scroll;
	scroll = new Scroll_gump();

	for (const auto& iter : keyhelp)
		scroll->add_text(iter.c_str());

	scroll->paint();
	do {
		int x;
		int y;
		Get_click(x, y, Mouse::hand, nullptr, false, scroll);
	} while (scroll->show_next_page());
	Game_window::get_instance()->paint();
	delete scroll;
}

void KeyBinder::ShowCheatHelp() const {
	Scroll_gump *scroll;
	scroll = new Scroll_gump();

	for (const auto& iter : cheathelp)
		scroll->add_text(iter.c_str());

	scroll->paint();
	do {
		int x;
		int y;
		Get_click(x, y, Mouse::hand, nullptr, false, scroll);
	} while (scroll->show_next_page());
	Game_window::get_instance()->paint();
	delete scroll;
}

void KeyBinder::ShowMapeditHelp() const {
	Scroll_gump *scroll;
	scroll = new Scroll_gump();

	for (const auto& iter : mapedithelp)
		scroll->add_text(iter.c_str());

	scroll->paint();
	do {
		int x;
		int y;
		Get_click(x, y, Mouse::hand, nullptr, false, scroll);
	} while (scroll->show_next_page());
	Game_window::get_instance()->paint();
	delete scroll;
}

void KeyBinder::ShowBrowserKeys() const {
	auto *scroll = new Scroll_gump();
	scroll->add_text("Esc - Exits the shape browser");
	scroll->add_text("down - Increase shape by 1");
	scroll->add_text("S - Increase shape by 1");
	scroll->add_text("up - Decrease shape by 1");
	scroll->add_text("Shift-S - Decrease shape by 1");
	scroll->add_text("Page down - Increase shape by 20");
	scroll->add_text("J - Increase shape by 20");
	scroll->add_text("Page up - Decrease shape by 20");
	scroll->add_text("Shift-J - Decrease shape by 20");
	scroll->add_text("right - Increase frame by 1");
	scroll->add_text("F - Increase frame by 1");
	scroll->add_text("left - Decrease frame by 1");
	scroll->add_text("Shift-F - Decrease frame by 1");
	scroll->add_text("V - Increase vga file by 1");
	scroll->add_text("Shift-V - Decrease vga file by 1");
	scroll->add_text("P - Increase palette by 1");
	scroll->add_text("Shift-P - Decrease palette by 1");
	scroll->add_text("X - Increase xform by 1");
	scroll->add_text("Shift-X - Decrease xform by 1");
	char returned_key[200];
	if (last_created_key.empty())
		strcpy(returned_key, "Error: No key assigned");
	else {
		strcpy(returned_key, "");   // prevent garbage text
		int extra_keys = 0;
		for (const auto& iter : last_created_key) {
			if (extra_keys >= 5)
				continue;
			else if (extra_keys > 0)
				strcat(returned_key, " or ");
			strcat(returned_key, iter.c_str());
			extra_keys += 1;
		}
	}
	strcat(returned_key, " - when pressed in game will create the last shape viewed in shapes.vga.");
	scroll->add_text(returned_key);
	scroll->paint();
	do {
		int x;
		int y;
		Get_click(x, y, Mouse::hand, nullptr, false, scroll);
	} while (scroll->show_next_page());
	Game_window::get_instance()->paint();
	delete scroll;
}

void KeyBinder::ParseText(char *text, int len) {
	char *ptr;
	char *end;
	const char LF = '\n';

	ptr = text;

	// last (useful) line must end with LF
	while ((ptr - text) < len && (end = strchr(ptr, LF)) != nullptr) {
		*end = '\0';
		ParseLine(ptr);
		ptr = end + 1;
	}
}

static void skipspace(string &s) {
	size_t i = s.find_first_not_of(chardata.whitespace);
	if (i && i != string::npos)
		s.erase(0, i);
}


void KeyBinder::ParseLine(char *line) {
	size_t i;
	SDL_Keysym k;
	ActionType a;
	k.sym      = SDLK_UNKNOWN;
	k.mod      = KMOD_NONE;
	string s = line;
	string u;
	string d;
	string desc;
	string keycode;
	bool show;

	skipspace(s);

	// comments and empty lines
	if (s.length() == 0 || s[0] == '#')
		return;

	u = s;
	to_uppercase(u);

	// get key
	while (s.length() && !isspace(static_cast<unsigned char>(s[0]))) {
		// check modifiers
		//    if (u.compare("ALT-",0,4) == 0) {
		if (u.substr(0, 4) == "ALT-") {
			k.mod = static_cast<SDL_Keymod>(k.mod | KMOD_ALT);
			s.erase(0, 4);
			u.erase(0, 4);
			//    } else if (u.compare("CTRL-",0,5) == 0) {
		} else if (u.substr(0, 5) == "CTRL-") {
			k.mod = static_cast<SDL_Keymod>(k.mod | KMOD_CTRL);
			s.erase(0, 5);
			u.erase(0, 5);
			//    } else if (u.compare("SHIFT-",0,6) == 0) {
		} else if (u.substr(0, 6) == "SHIFT-") {
			k.mod = static_cast<SDL_Keymod>(k.mod | KMOD_SHIFT);
			s.erase(0, 6);
			u.erase(0, 6);
		} else {
			i = s.find_first_of(chardata.whitespace);

			keycode = s.substr(0, i);
			s.erase(0, i);
			string t(keycode);
			to_uppercase(t);

			if (t.length() == 0) {
				cerr << "Keybinder: parse error in line: " << s << endl;
				return;
			} else if (t.length() == 1) {
				// translate 1-letter keys straight to SDL_Keycode
				auto c = static_cast<unsigned char>(t[0]);
				if (std::isgraph(c) && c != '%' && c != '{' && c != '|' && c != '}' && c != '~') {
					c = std::tolower(c);	// need lowercase
					k.sym = static_cast<SDL_Keycode>(c);
				} else {
					cerr << "Keybinder: unsupported key: " << keycode << endl;
				}
			} else {
				// lookup in table
				auto key_index = keys.find(t);
				if (key_index != keys.end()) {
					k.sym = (*key_index).second;
				} else {
					cerr << "Keybinder: unsupported key: " << keycode << endl;
					return;
				}
			}
		}
	}

	if (k.sym == SDLK_UNKNOWN) {
		cerr << "Keybinder: parse error in line: " << s << endl;
		return;
	}

	// get function
	skipspace(s);

	i = s.find_first_of(chardata.whitespace);
	string t = s.substr(0, i);
	s.erase(0, i);
	to_uppercase(t);

	auto action_index = actions.find(t);
	if (action_index != actions.end()) {
		a.action = (*action_index).second;
	} else {
		cerr << "Keybinder: unsupported action: " << t << endl;
		return;
	}

	// get params
	skipspace(s);

	int np = 0;
	// Special case.
	if (!strcmp(a.action->s, "CALL_USECODE")) {
		// Want to allow function name.
		if (s.length() && s[0] != '#') {
			i = s.find_first_of(chardata.whitespace);
			string t = s.substr(0, i);
			s.erase(0, i);
			skipspace(s);

			int p = atoi(t.c_str());
			if (!p) {
				// No conversion? Try as function name.
				Usecode_machine *usecode =
				    Game_window::get_instance()->get_usecode();
				p = usecode->find_function(t.c_str());
			} else if (p < 0) {
				p = -1;
			}
			a.params[np++] = p;
		}
	}
	while (s.length() && s[0] != '#' && np < c_maxparams) {
		i = s.find_first_of(chardata.whitespace);
		string t = s.substr(0, i);
		s.erase(0, i);
		skipspace(s);

		int p = atoi(t.c_str());
		a.params[np++] = p;
	}

	// read optional help comment
	if (s.length() >= 1 && s[0] == '#') {
		if (s.length() >= 2 && s[1] == '-') {
			show = false;
		} else {
			s.erase(0, 1);
			skipspace(s);
			d = s;
			// Always show if there is a comment.
			show = true;
		}
	} else {
		// Action::dont_show doesn't have default display names, so do not
		// show them if they don't have a comment.
		d = a.action->desc;
		show = a.action->key_type != Action::dont_show;
	}

	if (show) {
		desc = "";
		if (k.mod & KMOD_CTRL)
			desc += "Ctrl-";
#ifdef MACOSX
		if (k.mod & KMOD_ALT)
			desc += "Cmd-";
#else
		if (k.mod & KMOD_ALT)
			desc += "Alt-";
#endif
		if (k.mod & KMOD_SHIFT)
			desc += "Shift-";
		desc += keycode;
		if (!strcmp(a.action->s, "CREATE_ITEM") && a.params[0] == -1)
			last_created_key.push_back(desc);

		desc += " - " + d;

		// add to help list
		if (a.action->key_type == Action::cheat_keys)
			cheathelp.push_back(desc);
		else if (a.action->key_type == Action::mapedit_keys)
			mapedithelp.push_back(desc);
		else    // Either Action::normal_keys or Action::dont_show with comment.
			keyhelp.push_back(desc);
	}

	// bind key
	AddKeyBinding(k.sym, k.mod, a.action, np, a.params);
}

void KeyBinder::LoadFromFileInternal(const char *filename) {
	ifstream keyfile;

	U7open(keyfile, filename, true);
	char temp[1024]; // 1024 should be long enough
	while (!keyfile.eof()) {
		keyfile.getline(temp, 1024);
		if (keyfile.gcount() >= 1023) {
			cerr << "Keybinder: parse error: line too long. Skipping rest of file."
			     << endl;
			return;
		}
		ParseLine(temp);
	}
	keyfile.close();
}

void KeyBinder::LoadFromFile(const char *filename) {

	Flush();

	cout << "Loading keybindings from file " << filename << endl;
	LoadFromFileInternal(filename);
}

void KeyBinder::LoadFromPatch() {
	if (U7exists(PATCH_KEYS)) {
		cout << "Loading patch keybindings" << endl;
		LoadFromFileInternal(PATCH_KEYS);
	}
}

void KeyBinder::LoadDefaults() {
	Flush();

	cout << "Loading default keybindings" << endl;

	const str_int_pair &resource = game->get_resource("config/defaultkeys");

	U7object txtobj(resource.str, resource.num);
	size_t len;
	auto txt = txtobj.retrieve(len);
	if (txt && len > 0)
		ParseText(reinterpret_cast<char*>(txt.get()), len);
}

// codes used in keybindings-files. (use uppercase here)
void KeyBinder::FillParseMaps() {
	int i;  // For MSVC
	for (i = 0; strlen(SDLKeyStringTable[i].s) > 0; i++)
		keys[SDLKeyStringTable[i].s] = SDLKeyStringTable[i].k;

	for (i = 0; strlen(ExultActions[i].s) > 0; i++)
		actions[ExultActions[i].s] = &(ExultActions[i]);
}
