#include "controlconfig/controlsconfig.h"
#include "io/joy.h"
#include "io/joy_ff.h"
#include "io/timer.h"
#include "mod_table/mod_table.h"
#include "options/Option.h"
#include "osapi/osapi.h"

#include <algorithm>
#include <array>
#include <bitset>
#include <memory>
#include <utility>

#include "libs/jansson.h"

using namespace io::joystick;
using namespace os::events;

namespace {

typedef std::unique_ptr<Joystick> JoystickPtr;

SCP_vector<JoystickPtr> joysticks;	//!< All joysticks detected by SDL
std::array<Joystick*, CID_JOY_MAX> pJoystick = {nullptr};	//!< The 4 joysticks FSO are using. pJoystick[0] replaces CurrentJoystick

bool initialized = false;

/**
* @brief Compatibility conversion from Button index to HatPosition
*/
inline
HatPosition hatBtnToEnum(int in) {
	Assertion(in >= JOY_NUM_BUTTONS, "Invalid button value passed to hatBtnToEnum: %i", in);

	in -= JOY_NUM_BUTTONS;

	switch (in) {
	case 0:
		return HAT_DOWN;

	case 1:
		return HAT_UP;

	case 2:
		return HAT_LEFT;

	case 3:
		return HAT_RIGHT;

	case 4:
		return HAT_LEFTDOWN;

	case 5:
		return HAT_LEFTUP;

	case 6:
		return HAT_RIGHTDOWN;

	case 7:
		return HAT_RIGHTUP;

	default:
		// Invalid index
		return HAT_CENTERED;
	}
};

SCP_string getJoystickGUID(SDL_Joystick* stick)
{
	auto guid = SDL_JoystickGetGUID(stick);

	const size_t GUID_STR_SIZE = 33;
	SCP_string joystickGUID;
	joystickGUID.resize(GUID_STR_SIZE);

	SDL_JoystickGetGUIDString(guid, &joystickGUID[0], static_cast<int>(joystickGUID.size()));

	// Remove trailing \0
	joystickGUID.resize(GUID_STR_SIZE - 1);

	// Make sure the GUID is upper case
	SCP_toupper(joystickGUID);

	return joystickGUID;
}

bool joystickMatchesGuid(Joystick* testStick, const SCP_string& guid, int id)
{
	if (guid.empty()) {
		// Only use the id
		return id == testStick->getDeviceId();
	}

	SCP_string guidStr(guid);
	SCP_toupper(guidStr);

	if (testStick->getGUID() != guidStr) {
		return false; // GUID doesn't match
	}

	// Build a list of all joysticks with the right guid
	size_t num_sticks = 0;
	for (auto& stick : joysticks) {
		if (stick->getGUID() == guidStr) {
			++num_sticks;
		}
	}

	if (num_sticks == 0) {
		// Not the right GUID
		return false;
	}
	if (num_sticks == 1) {
		// Only one option -> this is the right stick
		return true;
	}

	// Multiple sticks -> check if the device id is the same
	return testStick->getDeviceId() == id;
}

/**
 * Returns True if the given stick should be assigned the given cid
 *
 * @param[in] testStick	The joystick ptr to test
 * @param[in] cid       The cid to test this against
 *
 * @details
 *   Should cid be CID_JOY0, it also checks for legacy .ini keys.
 *   Should Joy0ID key be null, legacy keys will be tried
 */
bool isPlayerJoystick(Joystick* testStick, short cid) {
	SCP_string GUID;
	int Id;

	SCP_string key_guid;
	SCP_string key_id;

	// Select key strings according to cid
	switch (cid) {
		case CID_JOY0:
			key_guid = "Joy0GUID";
			key_id = "Joy0ID";
			break;
		case CID_JOY1:
			key_guid = "Joy1GUID";
			key_id = "Joy1ID";
			break;
		case CID_JOY2:
			key_guid = "Joy2GUID";
			key_id = "Joy2ID";
			break;
		case CID_JOY3:
			key_guid = "Joy3GUID";
			key_id = "Joy3ID";
			break;
		case CID_NONE:
		case CID_KEYBOARD:
		case CID_MOUSE:
			return false;
		default:
			mprintf(("Unknown CID %i\n", cid));
			return false;
	}

	GUID = os_config_read_string(nullptr, key_guid.c_str(), "");
	Id = static_cast<int>(os_config_read_uint(nullptr, key_id.c_str(), 0));

	if ((cid == CID_JOY0) && GUID.empty()) {
		// Check legacy keys
		GUID = os_config_read_string(nullptr, "CurrentJoystickGUID", "");
		Id = static_cast<int>(os_config_read_uint(nullptr, "CurrentJoystick", 0));
	}

	return joystickMatchesGuid(testStick, GUID, Id);
}

/**
 * Reads/deserializes the given json line and retrieves the joystick that it references
 */
Joystick* joystick_deserialize(const json_t* value)
{
	const char* guid;
	int id;

	json_error_t err;
	if (json_unpack_ex((json_t*)value, &err, 0, "{s:s, s:i}", "guid", &guid, "id", &id) != 0) {
		// throw json_exception(err);
		// Changed by wookieejedi.
		// If errors detected then return nullptr (ie, no joystick),
		// because if we throw errors instead of returning nullptr
		// then the listening functions short circuits and does not run.
		return nullptr;
	}

	for (auto& test_stick : joysticks) {
		if (joystickMatchesGuid(test_stick.get(), guid, id)) {
			return test_stick.get();
		}
	}

	return nullptr;
}

/**
 * Converts/serializes joystick reference data into a json line
 */
json_t* joystick_serializer(Joystick* joystick)
{
	if (joystick == nullptr) {
		return json_null();
	}

	return json_pack("{sssi}", "guid", joystick->getGUID().c_str(), "id", joystick->getDeviceId());
}

/**
 * Assigns the given cid to the given stick
 *
 * @param[in] stick  The stick that is being assigned
 * @param[in] cid    The cid being assigned, must be a valid short between CID_JOY0 and CID_JOY_MAX
 */
void setPlayerJoystick(Joystick* stick, short cid)
{
	Assert((cid >= CID_JOY0) && (cid < CID_JOY_MAX));
	pJoystick[cid] = stick;

	if (cid == CID_JOY0) {
		joy_ff_shutdown();
	}

	if (pJoystick[cid] != nullptr) {
		mprintf(("  Using '%s' as Joy-%i\n", pJoystick[cid]->getName().c_str(), cid));
		mprintf(("\n"));
		mprintf(("  Number of axes: %d\n", pJoystick[cid]->numAxes()));
		mprintf(("  Number of buttons: %d\n", pJoystick[cid]->numButtons()));
		mprintf(("  Number of hats: %d\n", pJoystick[cid]->numHats()));
		mprintf(("  Number of trackballs: %d\n", pJoystick[cid]->numBalls()));
		mprintf(("\n"));

		if (cid == CID_JOY0) {
			joy_ff_init();
		}
	} else {
		mprintf((" Joystick %i removed\n", cid));
	}
}

/**
 * Scripting callback for displaying the given joystick's name
 */
SCP_string joystick_display(Joystick* stick)
{
	if (stick == nullptr) {
		return XSTR("None", 1673);
	}
	return stick->getName();
}

/**
 * Scripting callback for enumerating a SCP_vector with all available joysticks
 */
SCP_vector<Joystick*> joystick_enumerator()
{
	SCP_vector<Joystick*> out;
	out.push_back(nullptr);
	for (auto& stick : joysticks) {
		out.push_back(stick.get());
	}
	return out;
}

/**
 * Joystick options for the new menu system
 * These should be displayed as a dropdown box type widget
 */
auto JoystickOption = options::OptionBuilder<Joystick*>("Input.Joystick",
                     std::pair<const char*, int>{"Joystick 0", 1705},
                     std::pair<const char*, int>{"The current joystick 0", 1706})
                     .category(std::make_pair("Input", 1827))                    // Category this option shows up in the scripting heirachy
                     .deserializer(joystick_deserialize)   // callback for json to C++
                     .serializer(joystick_serializer)      // callback for C++ to json
                     .display(joystick_display)            // callback for constructing the display label
                     .enumerator(joystick_enumerator)      // callback for enumerating/constructing the values this option may take
                     .level(options::ExpertLevel::Beginner)
                     .default_val(nullptr)                 // initial/default value for this option
                     .flags({options::OptionFlags::ForceMultiValueSelection})
                     .importance(100)
                     .change_listener([](Joystick* joy, bool) {
                         setPlayerJoystick(joy, CID_JOY0);
                         return true;
                     })
                     .finish();

auto JoystickOption1 = options::OptionBuilder<Joystick*>("Input.Joystick1",
                     std::pair<const char*, int>{"Joystick 1", 1707},
                     std::pair<const char*, int>{"The current joystick 1", 1708})
                     .category(std::make_pair("Input", 1827))
                     .deserializer(joystick_deserialize)
                     .serializer(joystick_serializer)
                     .display(joystick_display)
                     .enumerator(joystick_enumerator)
                     .level(options::ExpertLevel::Beginner)
                     .default_val(nullptr)
                     .flags({ options::OptionFlags::ForceMultiValueSelection })
                     .importance(90)
                     .change_listener([](Joystick* joy, bool) {
                         setPlayerJoystick(joy, CID_JOY1);
                         return true;
                     })
                     .finish();

auto JoystickOption2 = options::OptionBuilder<Joystick*>("Input.Joystick2",
                     std::pair<const char*, int>{"Joystick 2", 1709},
                     std::pair<const char*, int>{"The current joystick 2", 1710})
                     .category(std::make_pair("Input", 1827))
                     .deserializer(joystick_deserialize)
                     .serializer(joystick_serializer)
                     .display(joystick_display)
                     .enumerator(joystick_enumerator)
                     .level(options::ExpertLevel::Beginner)
                     .default_val(nullptr)
                     .flags({ options::OptionFlags::ForceMultiValueSelection })
                     .importance(80)
                     .change_listener([](Joystick* joy, bool) {
                         setPlayerJoystick(joy, CID_JOY2);
                         return true;
                     })
                     .finish();

auto JoystickOption3 = options::OptionBuilder<Joystick*>("Input.Joystick3",
                     std::pair<const char*, int>{"Joystick 3", 1711},
                     std::pair<const char*, int>{"The current joystick 3", 1712})
                     .category(std::make_pair("Input", 1827))
                     .deserializer(joystick_deserialize)
                     .serializer(joystick_serializer)
                     .display(joystick_display)
                     .enumerator(joystick_enumerator)
                     .level(options::ExpertLevel::Beginner)
                     .default_val(nullptr)
                     .flags({ options::OptionFlags::ForceMultiValueSelection })
                     .importance(70)
                     .change_listener([](Joystick* joy, bool) {
                         setPlayerJoystick(joy, CID_JOY3);
                         return true;
                     })
                     .finish();

HatPosition convertSDLHat(int val)
{
	switch (val)
	{
		case SDL_HAT_CENTERED:
			return HAT_CENTERED;
		case SDL_HAT_UP:
			return HAT_UP;
		case SDL_HAT_RIGHT:
			return HAT_RIGHT;
		case SDL_HAT_DOWN:
			return HAT_DOWN;
		case SDL_HAT_LEFT:
			return HAT_LEFT;
		case SDL_HAT_RIGHTUP:
			return HAT_RIGHTUP;
		case SDL_HAT_RIGHTDOWN:
			return HAT_RIGHTDOWN;
		case SDL_HAT_LEFTUP:
			return HAT_LEFTUP;
		case SDL_HAT_LEFTDOWN:
			return HAT_LEFTDOWN;
		default:
			return HAT_CENTERED;
	}
}

void enumerateJoysticks(SCP_vector<JoystickPtr>& outVec)
{
	auto num = SDL_NumJoysticks();
	outVec.clear();
	outVec.reserve(static_cast<size_t>(num));

	mprintf(("Printing joystick info:\n"));

	for (auto i = 0; i < num; ++i) {
			try
			{
				auto ptr = JoystickPtr(new Joystick(i));
				ptr->printInfo();

				outVec.push_back(std::move(ptr));
			}
			catch (const std::exception &e)
			{
				mprintf(("  An error occured while attempting to enumerate joystick %i.\n", i));
				mprintf(("    %s\n", e.what()));
				mprintf(("    %s\n", SDL_GetError()));
				SDL_ClearError();
			}
	}
}

Joystick *getJoystickForID(SDL_JoystickID id)
{
	for (auto iter = joysticks.begin(); iter != joysticks.end(); ++iter)
	{
		if ((*iter)->getID() == id)
		{
			return (*iter).get();
		}
	}

	return nullptr;
}

bool axis_event_handler(const SDL_Event &evt)
{
	auto stick = getJoystickForID(evt.jaxis.which);

	if (stick != nullptr)
	{
		stick->handleJoyEvent(evt);
	}

	return true;
}

bool ball_event_handler(const SDL_Event &evt)
{
	auto stick = getJoystickForID(evt.jball.which);

	if (stick != nullptr)
	{
		stick->handleJoyEvent(evt);
	}

	return true;
}

bool button_event_handler(const SDL_Event &evt)
{
	auto stick = getJoystickForID(evt.jbutton.which);

	if (stick != nullptr)
	{
		stick->handleJoyEvent(evt);
	}

	return true;
}

bool hat_event_handler(const SDL_Event &evt)
{
	auto stick = getJoystickForID(evt.jhat.which);

	if (stick != nullptr)
	{
		stick->handleJoyEvent(evt);
	}

	return true;
}

bool device_event_handler(const SDL_Event &evt)
{
	auto &joyDeviceEvent = evt.jdevice;

	auto evtType = evt.type;

	if (evtType == SDL_JOYDEVICEADDED)
	{
		// A new joystick has been added, add it to our list and check if it's the configured joystick
		Joystick *addedStick;
		{
			auto device = SDL_JoystickOpen(joyDeviceEvent.which);

			if (device == nullptr)
			{
				mprintf(("Failed to open connecting joystick: %s\n", SDL_GetError()));
				return true;
			}

			// Add the new device to our list
			auto added = JoystickPtr(new Joystick(joyDeviceEvent.which));

			for (auto iter = joysticks.begin(); iter != joysticks.end(); ++iter)
			{
				if ((*iter)->getID() == added->getID())
				{
					// Already have this stick
					*(*iter) = std::move(*added);
					return true;
				}
			}

			addedStick = added.get();
			// This is a new stick so we can output it's information
			mprintf(("A new joystick has been connected:\n"));
			addedStick->printInfo();

			joysticks.push_back(std::move(added));
		}

		// Is true if all of the pJoysticks are bound
		bool all_bound = std::all_of(pJoystick.begin(), pJoystick.end(), [](Joystick* pJoy){ return pJoy != nullptr; });

		if (all_bound)
		{
			// We already have all valid joystick devices, bail early
			return true;
		}	// Else, at least one player stick is unbound

		// Bind the added stick, if we want it
		if (Using_in_game_options)
		{
			// New menu system lets players select joysticks in-game
			Joystick* value[CID_JOY_MAX];
			value[0] = JoystickOption->getValue();
			value[1] = JoystickOption1->getValue();
			value[2] = JoystickOption2->getValue();
			value[3] = JoystickOption3->getValue();

			
			for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
			{
				if (value[i] == addedStick)
				{
					mprintf(("Joystick %i connected\n", i));
					setPlayerJoystick(addedStick, i);
					break;
				}
			}
		}
		else
		{
			// Legacy system requires setting joysticks in the launcher
			for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
			{
				if (isPlayerJoystick(addedStick, i))
				{
					mprintf(("Joystick %i connected\n", i));
					setPlayerJoystick(addedStick, i);
					break;
				}
			}
		}
	}
	else if (evtType == SDL_JOYDEVICEREMOVED)
	{
		bool any_bound = std::any_of(pJoystick.begin(), pJoystick.end(), [](Joystick* pJoy){ return pJoy != nullptr; });

		if (!any_bound)
		{
			// Nothing bound, ignore
			return true;
		}

		// Else, Find if any of our joysticks were lost
		for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
		{
			if (joyDeviceEvent.which == pJoystick[i]->getID())
			{
				// We just lost our joystick, reset the data
				mprintf(("Joystick %i disconnected\n", i));
				setPlayerJoystick(nullptr, i);
				break;
			}
		}
	}

	return true;
}
} // namespace

namespace io
{
namespace joystick
{
	Joystick::Joystick(int device_id) :
		_device_id(device_id)
	{
		_joystick = SDL_JoystickOpen(device_id);

		if (_joystick == nullptr) {
			SCP_stringstream msg;
			msg << "Failed to open joystick with id " << device_id << ", get a coder!";
			throw std::runtime_error(msg.str());
		}

		fillValues();
	}

	Joystick::Joystick(Joystick &&other) noexcept :
			_joystick(nullptr)
	{
		*this = std::move(other);
	}

	Joystick::~Joystick()
	{
		if (_joystick != nullptr)
		{
			SDL_JoystickClose(_joystick);
			_joystick = nullptr;
		}
	}

	Joystick &Joystick::operator=(Joystick &&other) noexcept
	{
		if (this == &other)
			return *this;

		std::swap(_device_id, other._device_id);
		std::swap(_joystick, other._joystick);

		fillValues();

		return *this;
	}

	bool Joystick::isAttached() const
	{
		return SDL_JoystickGetAttached(_joystick) == SDL_TRUE;
	}

	bool Joystick::isHaptic() const
	{
		return _isHaptic;
	}

	bool Joystick::isGamepad() const
	{
		return _isGamepad;
	}

	Sint16 Joystick::getAxis(int index) const
	{
		Assertion(index >= 0 && index < numAxes(), "Invalid index %d!", index);

		return _axisValues[index];
	}

	bool Joystick::isButtonDown(int index) const
	{
		Assertion(index >= 0 && index < numButtons(), "Invalid index %d!", index);

		return _button[index].DownTimestamp.isValid();
	}

	float Joystick::getButtonDownTime(int index) const
	{
		Assertion(index >= 0 && index < numButtons(), "Invalid index %d!", index);

		if (_button[index].DownTimestamp.isValid())
		{
			auto diff = ui_timestamp_since(_button[index].DownTimestamp);

			return static_cast<float>(diff) / MILLISECONDS_PER_SECOND;
		}

		return 0.0f;
	}

	int Joystick::getButtonDownCount(int index, bool reset)
	{
		Assertion(index >= 0 && index < numButtons(), "Invalid index %d!", index);

		auto val = _button[index].DownCount;

		if (reset)
		{
			_button[index].DownCount = 0;
		}

		return val;
	}

	coord2d Joystick::getBall(int index) const
	{
		Assertion(index >= 0 && index < numBalls(), "Invalid index %d!", index);

		return _ballValues[index];
	}

	HatPosition Joystick::getHatPosition(int index) const
	{
		Assertion(index >= 0 && index < numHats(), "Invalid index %d!", index);

		return _hat[index].Value;
	}

	int Joystick::getHatDownCount(int index, HatPosition pos, bool ext, bool reset) {
		Assertion(index >= 0 && index < numHats(), "Invalid index %d!", index);

		int val;

		if (pos == HAT_CENTERED) {
			return 0;
		}

		if (ext) {
			// Use 8-position
			val = _hat[index].DownCount8[pos];

			if (reset) {
				_hat[index].DownCount8[pos] = 0;
			}
		} else {
			// Use 4-position
			val = _hat[index].DownCount4[pos];

			if (reset) {
				_hat[index].DownCount4[pos] = 0;
			}
		}

		return val;
	}

	float Joystick::getHatDownTime(int index, HatPosition pos, bool ext) const
	{
		Assertion(index >= 0 && index < numHats(), "Invalid index %d!", index);

		if (pos == HAT_CENTERED) {
			return 0.0f;
		}

		UI_TIMESTAMP val;

		if (ext) {
			// Use 8-position
			val = _hat[index].DownTimestamp8[pos];

		} else {
			// Use 4-position
			val = _hat[index].DownTimestamp4[pos];
		}

		if (!val.isValid()) {
			return 0.0f;
		}

		auto diff = ui_timestamp_since(val);

		return static_cast<float>(diff) / MILLISECONDS_PER_SECOND;
	}

	int Joystick::numAxes() const
	{
		return static_cast<int>(_axisValues.size());
	}

	int Joystick::numBalls() const
	{
		return static_cast<int>(_ballValues.size());
	}

	int Joystick::numButtons() const
	{
		return static_cast<int>(_button.size());
	}

	int Joystick::numHats() const
	{
		return static_cast<int>(_hat.size());
	}

	SCP_string Joystick::getGUID() const
	{
		return _guidStr;
	}

	SCP_string Joystick::getName() const
	{
		return _name;
	}

	SDL_JoystickID Joystick::getID() const
	{
		return _id;
	}

	void Joystick::fillValues()
	{
		_name.assign(SDL_JoystickName(_joystick));
		_guidStr = getJoystickGUID(_joystick);
		_id = SDL_JoystickInstanceID(_joystick);
		_isHaptic = SDL_JoystickIsHaptic(_joystick);
		_isGamepad = SDL_IsGameController(_device_id) == SDL_TRUE;

		// Initialize values of the axes
		auto numSticks = SDL_JoystickNumAxes(_joystick);
		if (numSticks >= 0) {
			_axisValues.resize(static_cast<size_t>(numSticks));
			for (auto i = 0; i < numSticks; ++i) {
				_axisValues[i] = SDL_JoystickGetAxis(_joystick, i);
			}

		} else {
			_axisValues.resize(0);
			mprintf(("Failed to get number of axes for joystick %s: %s\n", _name.c_str(), SDL_GetError()));
		}

		// Initialize ball values
		auto ballNum = SDL_JoystickNumBalls(_joystick);
		if (ballNum >= 0) {
			_ballValues.resize(static_cast<size_t>(ballNum));

			for (auto i = 0; i < ballNum; ++i) {
				coord2d coord;
				if (SDL_JoystickGetBall(_joystick, i, &coord.x, &coord.y)) {
					mprintf(("Failed to get ball %d value for joystick %s: %s\n", i, _name.c_str(), SDL_GetError()));
				}
			}

		} else {
			mprintf(("Failed to get number of ball axes for joystick %s: %s\n", _name.c_str(), SDL_GetError()));
			_ballValues.resize(0);
		}

		// Initialize buttons
		auto buttonNum = SDL_JoystickNumButtons(_joystick);
		if (buttonNum >= 0) {
			_button.resize(static_cast<size_t>(buttonNum));
			for (auto i = 0; i < buttonNum; ++i) {
				if (SDL_JoystickGetButton(_joystick, i) == 1) {
					_button[i].DownTimestamp = ui_timestamp();
				
				} else {
					_button[i].DownTimestamp = UI_TIMESTAMP::invalid();
				}
			}

		} else {
			_button.resize(0);
			mprintf(("Failed to get number of buttons for joystick %s: %s\n", _name.c_str(), SDL_GetError()));
		}

		// Initialize hats
		auto hatNum = SDL_JoystickNumHats(_joystick);
		if (hatNum >= 0) {
			_hat.resize(static_cast<size_t>(hatNum));
			for (auto i = 0; i < hatNum; ++i) {
				std::bitset<4> hatset = SDL_JoystickGetHat(_joystick, i);
				auto hatval = convertSDLHat(SDL_JoystickGetHat(_joystick, i));
				_hat[i].Value = hatval;

				// Reset timestampts
				for (auto j = 0; j < 4; ++j) {
					_hat[i].DownTimestamp4[j] = UI_TIMESTAMP::invalid();
				}
				for (auto j = 0; j < 8; ++j) {
					_hat[i].DownTimestamp8[j] = UI_TIMESTAMP::invalid();
				}

				if (_hat[i].Value != HAT_CENTERED) {
				// Set the 4-pos timestamp(s)
				if ((hatset[HAT_DOWN])) {
					_hat[i].DownTimestamp4[HAT_DOWN] = ui_timestamp();
				}
				if ((hatset[HAT_UP])) {
					_hat[i].DownTimestamp4[HAT_UP] = ui_timestamp();
				}
				if ((hatset[HAT_LEFT])) {
					_hat[i].DownTimestamp4[HAT_LEFT] = ui_timestamp();
				}
				if ((hatset[HAT_RIGHT])) {
					_hat[i].DownTimestamp4[HAT_RIGHT] = ui_timestamp();
				}

				// Set the 8-pos timestamp
					_hat[i].DownTimestamp8[hatval] = ui_timestamp();
				}
			}
		} else {
			_hat.resize(0);
			mprintf(("Failed to get number of hats for joystick %s: %s", _name.c_str(), SDL_GetError()));
		}
	}

	SDL_Joystick *Joystick::getDevice()
	{
		return _joystick;
	}

	void Joystick::handleJoyEvent(const SDL_Event &evt)
	{
		switch (evt.type)
		{
			case SDL_JOYAXISMOTION:
				handleAxisEvent(evt.jaxis);
				break;
			case SDL_JOYBALLMOTION:
				handleBallEvent(evt.jball);
				break;
			case SDL_JOYBUTTONDOWN:
			case SDL_JOYBUTTONUP:
				handleButtonEvent(evt.jbutton);
				break;
			case SDL_JOYHATMOTION:
				handleHatEvent(evt.jhat);
				break;
			default:
				break;
		}
	}

	void Joystick::handleAxisEvent(const SDL_JoyAxisEvent &evt)
	{
		auto axis = evt.axis;

		Assertion(axis < numAxes(), "SDL event contained invalid axis index!");

		_axisValues[axis] = evt.value;
	}

	void Joystick::handleButtonEvent(const SDL_JoyButtonEvent &evt)
	{
		auto button = evt.button;

		Assertion(button < numButtons(), "SDL event contained invalid button index!");

		auto down = evt.state == SDL_PRESSED;

		_button[button].DownTimestamp = down ? ui_timestamp() : UI_TIMESTAMP::invalid();

		if (down)
		{
			++_button[button].DownCount;
		}
	}

	void Joystick::handleHatEvent(const SDL_JoyHatEvent &evt)
	{
		auto hat = evt.hat;

		Assertion(hat < numHats(), "SDL event contained invalid hat index!");

		std::bitset<4> hatset = evt.value;
		auto hatpos = convertSDLHat(evt.value);

		// Set current values
		_hat[hat].Value = hatpos;

		// Reset inactive hat positions
		for (auto i = 0; i < 4; ++i) {
			if (!hatset[i]) {
				_hat[hat].DownTimestamp4[i] = UI_TIMESTAMP::invalid();
			}
		}
		for (auto i = 0; i < 8; ++i) {
			if (i != hatpos) {
				_hat[hat].DownTimestamp8[i] = UI_TIMESTAMP::invalid();
			}
		}

		if (hatpos != HAT_CENTERED) {
			// Set the 4-pos timestamps and down counts if the timestamp is not set
			if ((hatset[HAT_DOWN]) && (!_hat[hat].DownTimestamp4[HAT_DOWN].isValid())) {
				_hat[hat].DownTimestamp4[HAT_DOWN] = ui_timestamp();
				++_hat[hat].DownCount4[HAT_DOWN];
			}
			if ((hatset[HAT_UP]) && (!_hat[hat].DownTimestamp4[HAT_UP].isValid())) {
				_hat[hat].DownTimestamp4[HAT_UP] = ui_timestamp();
				++_hat[hat].DownCount4[HAT_UP];
			}
			if ((hatset[HAT_LEFT]) && (!_hat[hat].DownTimestamp4[HAT_LEFT].isValid())) {
				_hat[hat].DownTimestamp4[HAT_LEFT] = ui_timestamp();
				++_hat[hat].DownCount4[HAT_LEFT];
			}
			if ((hatset[HAT_RIGHT]) && (!_hat[hat].DownTimestamp4[HAT_RIGHT].isValid())) {
				_hat[hat].DownTimestamp4[HAT_RIGHT] = ui_timestamp();
				++_hat[hat].DownCount4[HAT_RIGHT];
			}

			// Set the 8-pos timestamp and down count
			_hat[hat].DownTimestamp8[hatpos] = ui_timestamp();
			++_hat[hat].DownCount8[hatpos];
		}
	}

	void Joystick::handleBallEvent(const SDL_JoyBallEvent &evt)
	{
		auto ball = evt.ball;

		Assertion(ball < numBalls(), "SDL event contained invalid ball index!");

		coord2d newVal;
		newVal.x = evt.xrel;
		newVal.y = evt.yrel;

		_ballValues[ball] = newVal;
	}

	void Joystick::printInfo() {
		mprintf(("  Joystick name: %s\n", getName().c_str()));
		mprintf(("  Joystick GUID: %s\n", getGUID().c_str()));
		mprintf(("  Joystick ID: %d\n", getID()));
		mprintf(("  Joystick device ID: %d\n", _device_id));
	}

	json_t* Joystick::getJSON() {
		json_t* object;

		object = json_pack("{s:s, s:s, s:i, s:i, s:i, s:i, s:i, s:i, s:b}",
			"name", getName().c_str(),          // s:s
			"guid", getGUID().c_str(),          // s:s
			"id", static_cast<int>( getID() ),  // s:i
			"device", _device_id,               // s:i
			"num_axes", numAxes(),              // s:i
			"num_balls", numBalls(),            // s:i
			"num_buttons", numButtons(),        // s:i
			"num_hats", numHats(),              // s:i
			"is_haptic", _isHaptic              // s:b
		);

		return object;
	}

	int Joystick::getDeviceId() const {
		return _device_id;
	}

	bool init(bool no_register)
	{
		using namespace os::events;

		if (initialized)
		{
			return true;
		}

		mprintf(("Initializing Joystick...\n"));

		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0)
		{
			mprintf(("  Could not initialize joystick: %s\n", SDL_GetError()));
			return false;
		}

		// enable event processing of the joystick
		if ((SDL_JoystickEventState(SDL_ENABLE)) != SDL_ENABLE)
		{
			mprintf(("  ERROR: Unable to initialize joystick event processing!\n"));
			SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
			return false;
		}

		if (SDL_NumJoysticks() < 1)
		{
			mprintf(("  No joysticks found\n"));
		}

		// Get the initial list of connected joysticks
		enumerateJoysticks(joysticks);

		if (no_register) {
			// bail before registering anything
			return false;
		}

		// Register all the event handlers
		addEventListener(SDL_JOYAXISMOTION, DEFAULT_LISTENER_WEIGHT, axis_event_handler);
		addEventListener(SDL_JOYBALLMOTION, DEFAULT_LISTENER_WEIGHT, ball_event_handler);

		addEventListener(SDL_JOYBUTTONDOWN, DEFAULT_LISTENER_WEIGHT, button_event_handler);
		addEventListener(SDL_JOYBUTTONUP, DEFAULT_LISTENER_WEIGHT, button_event_handler);

		addEventListener(SDL_JOYHATMOTION, DEFAULT_LISTENER_WEIGHT, hat_event_handler);

		addEventListener(SDL_JOYDEVICEADDED, DEFAULT_LISTENER_WEIGHT, device_event_handler);
		addEventListener(SDL_JOYDEVICEREMOVED, DEFAULT_LISTENER_WEIGHT, device_event_handler);

		// Search for the correct stick
		if (Using_in_game_options)
		{
			// The new options system is in use
			setPlayerJoystick(JoystickOption->getValue(), CID_JOY0);
			setPlayerJoystick(JoystickOption1->getValue(), CID_JOY1);
			setPlayerJoystick(JoystickOption2->getValue(), CID_JOY2);
			setPlayerJoystick(JoystickOption3->getValue(), CID_JOY3);
		}
		else
		{
			// Fall back to the legacy config values
			for (auto& stick : joysticks)
			{
				for (short i = CID_JOY0; i < CID_JOY_MAX; ++i)
				{
					if (isPlayerJoystick(stick.get(), i))
					{
						// Joystick found
						setPlayerJoystick(stick.get(), i);
						break;
					}
				}
			}
		}

		bool used = false;
		for (const auto pJoy : pJoystick) {
			if (pJoy != nullptr) {
				used = true;
				break;
			}
		}
		if (!used) {
			mprintf(("  No joystick is being used.\n"));
		}

		initialized = true;

		return true;
	}

	size_t getJoystickCount()
	{
		return joysticks.size();
	}

	Joystick *getJoystick(size_t index)
	{
		Assertion(index < getJoystickCount(), "Invalid joystick index " SIZE_T_ARG "!", index);

		return joysticks[index].get();
	}

	Joystick *getPlayerJoystick(short cid)
	{
		if ((cid >= CID_JOY0) && (cid < CID_JOY_MAX))
		{
			return pJoystick[cid];
		}
		else
		{
			return nullptr;
		}
	}

	int getPlayerJoystickCount() {
		int count = 0;

		for (auto pJoy : pJoystick) {
			if (pJoy != nullptr) {
				count++;
			}
		}

		return count;
	}

	void shutdown()
	{
		joy_ff_shutdown();

		initialized = false;
		std::for_each(pJoystick.begin(), pJoystick.end(), [](Joystick*& pJoy){ pJoy = nullptr; });

		// Automatically frees joystick resources
		joysticks.clear();

		SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
	}

	json_t* getJsonArray() {
		// Do a full init of the SDL_JOYSTICK systems
		init();

		json_t* array = json_array();

		// Get the JSON info of each detected joystick and attach it to array
		for (auto& joystick : joysticks) {
			json_t* object = joystick->getJSON();

			if (object != nullptr) {
				json_array_append_new(array, object);
			}
		}

		// Do a full de-init of the SDL_JOYSTICK systems
		shutdown();

		return array;
	}

	void printJoyJSON() {
		// Create root
		json_t* root = json_object();

		// Get Joystick data as JSON array
		json_t* array = getJsonArray();

		// Attach array to root
		json_object_set_new(root, "joysticks", array);

		// Dump to STDOUT
		json_dumpf(root, stdout, JSON_INDENT(4));

		shutdown();
	}
}
}

// Compatibility for the rest of the engine follows

int joystick_read_raw_axis(short cid, int num_axes, int *axis)
{
	int i;

	auto current = io::joystick::getPlayerJoystick(cid);
	if (!initialized || current == nullptr)
	{
		// fake a return value so that controlconfig doesn't get freaky with no joystick
		for (i = 0; i < num_axes; i++)
		{
			axis[i] = 32768;
		}
		return 0;
	}

	Assert(num_axes <= JOY_NUM_AXES);

	for (i = 0; i < num_axes; i++)
	{
		if (i < current->numAxes())
		{
			axis[i] = current->getAxis(i) + 32768;
		} else
		{
			axis[i] = 32768;
		}
	}

	return 1;
}

float joy_down_time(const CC_bind &bind)
{
	int btn = bind.get_btn();
	if (btn < 0) return 0.f;

	auto current = io::joystick::getPlayerJoystick(bind.get_cid());

	if (current == nullptr)
	{
		return 0.0f;
	}

	if (btn >= JOY_TOTAL_BUTTONS || (btn >= current->numButtons() && btn < JOY_NUM_BUTTONS))
	{
		// Not a valid button
		return 0.f;

	}
	else if (btn >= JOY_NUM_BUTTONS)
	{
		// Is hat
		if (current->numHats() > 0) {
			return current->getHatDownTime(0, hatBtnToEnum(btn), false);
		} else {
			// Don't have any hats to query
			return 0.0f;
		}

	} // Else, Is a button

	return current->getButtonDownTime(btn);
}

int joy_down_count(const CC_bind &bind, int reset_count)
{
	int btn = bind.get_btn();
	if (btn < 0) return 0;

	auto current = io::joystick::getPlayerJoystick(bind.get_cid());

	if (current == nullptr)
	{
		return 0;
	}

	if (btn >= JOY_TOTAL_BUTTONS || (btn >= current->numButtons() && btn < JOY_NUM_BUTTONS))
	{
		// Not a valid button
		return 0;

	}
	else if (btn >= JOY_NUM_BUTTONS)
	{
		// Is hat
		if (current->numHats() > 0) {
			return current->getHatDownCount(0, hatBtnToEnum(btn), false, reset_count != 0);
		} else {
			// Don't have any hats to query
			return 0;
		}

	} // Else, is a button

	return current->getButtonDownCount(btn, reset_count != 0);
}

int joy_down(const CC_bind &bind)
{
	return joy_down_time(bind) > 0.0f;
}

bool joy_present(short cid) {
	if ((cid < CID_JOY0) || (cid >= CID_JOY_MAX)) {
		return false;
	}

	return pJoystick[cid] != nullptr;
}
