//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Linux Joystick implementation for inputsystem.dll
//
//===========================================================================//

/* For force feedback testing. */
#include "inputsystem.h"
#include "tier1/convar.h"
#include "tier0/icommandline.h"

#include "SDL.h"
#include "SDL_gamecontroller.h"
#include "SDL_haptic.h"

// NOTE: This has to be the last file included!
#include "tier0/memdbgon.h"

static ButtonCode_t ControllerButtonToButtonCode( SDL_GameControllerButton button );
static AnalogCode_t ControllerAxisToAnalogCode( SDL_GameControllerAxis axis );
static int JoystickSDLWatcher( void *userInfo, SDL_Event *event );

ConVar joy_axisbutton_threshold( "joy_axisbutton_threshold", "0.3", FCVAR_ARCHIVE, "Analog axis range before a button press is registered." );
ConVar joy_axis_deadzone( "joy_axis_deadzone", "0.2", FCVAR_ARCHIVE, "Dead zone near the zero point to not report movement." );

static void joy_active_changed_f( IConVar *var, const char *pOldValue, float flOldValue );
ConVar joy_active( "joy_active", "-1", FCVAR_NONE, "Which of the connected joysticks / gamepads to use (-1 means first found)", &joy_active_changed_f);

static void joy_gamecontroller_config_changed_f( IConVar *var, const char *pOldValue, float flOldValue );
ConVar joy_gamecontroller_config( "joy_gamecontroller_config", "", FCVAR_ARCHIVE, "Game controller mapping (passed to SDL with SDL_HINT_GAMECONTROLLERCONFIG), can also be configured in Steam Big Picture mode.", &joy_gamecontroller_config_changed_f );

void SearchForDevice()
{
	int newJoystickId = joy_active.GetInt();
	CInputSystem *pInputSystem = (CInputSystem *)g_pInputSystem;

	if ( !pInputSystem )
	{
		return;
	}
	// -1 means "first available."
	if ( newJoystickId < 0 )
	{
		pInputSystem->JoystickHotplugAdded(0);
		return;
	}

	for ( int device_index = 0; device_index < SDL_NumJoysticks(); ++device_index )
	{
		SDL_Joystick *joystick = SDL_JoystickOpen(device_index);
		if ( joystick == NULL )
		{
			continue;
		}

		int joystickId = SDL_JoystickInstanceID(joystick);
		SDL_JoystickClose(joystick);

		if ( joystickId == newJoystickId )
		{
			pInputSystem->JoystickHotplugAdded(device_index);
			break;
		}
	}
}

//---------------------------------------------------------------------------------------
// Switch our active joystick to another device
//---------------------------------------------------------------------------------------
void joy_active_changed_f( IConVar *var, const char *pOldValue, float flOldValue )
{
	SearchForDevice();
}

//---------------------------------------------------------------------------------------
// Reinitialize the game controller layer when the joy_gamecontroller_config is updated.
//---------------------------------------------------------------------------------------
void joy_gamecontroller_config_changed_f( IConVar *var, const char *pOldValue, float flOldValue )
{
	CInputSystem *pInputSystem = (CInputSystem *)g_pInputSystem;
	if ( pInputSystem && SDL_WasInit(SDL_INIT_GAMECONTROLLER) )
	{
		bool oldValuePresent = pOldValue && ( strlen( pOldValue ) > 0 );
		bool newValuePresent = ( strlen( joy_gamecontroller_config.GetString() ) > 0 );
		if ( !oldValuePresent && !newValuePresent )
		{
			return;
		}

		// We need to reinitialize the whole thing (i.e. undo CInputSystem::InitializeJoysticks and then call it again)
		// due to SDL_GameController only reading the SDL_HINT_GAMECONTROLLERCONFIG on init.
		pInputSystem->ShutdownJoysticks();
		pInputSystem->InitializeJoysticks();
	}
}

//-----------------------------------------------------------------------------
// Handle the events coming from the GameController SDL subsystem.
//-----------------------------------------------------------------------------
int JoystickSDLWatcher( void *userInfo, SDL_Event *event )
{
	CInputSystem *pInputSystem = (CInputSystem *)userInfo;
	Assert(pInputSystem != NULL);
	Assert(event != NULL);

	if ( event == NULL || pInputSystem == NULL )
	{
		Warning("No input system\n");
		return 1;
	}

	switch ( event->type )
	{
		case SDL_CONTROLLERAXISMOTION:
		case SDL_CONTROLLERBUTTONDOWN:
		case SDL_CONTROLLERBUTTONUP:
		case SDL_CONTROLLERDEVICEADDED:
		case SDL_CONTROLLERDEVICEREMOVED:
			break;
		default:
			return 1;
	}

	// This is executed on the same thread as SDL_PollEvent, as PollEvent
	// updates the joystick subsystem, which then calls SDL_PushEvent for
	// the various events below.  PushEvent invokes this callback.
	// SDL_PollEvent is called in PumpWindowsMessageLoop which is coming
	// from PollInputState_Linux, so there's no worry about calling
	// PostEvent (which doesn't seem to be thread safe) from other threads.
	Assert(ThreadInMainThread());

	switch ( event->type )
	{
		case SDL_CONTROLLERAXISMOTION:
		{
			pInputSystem->JoystickAxisMotion(event->caxis.which, event->caxis.axis, event->caxis.value);
			break;
		}

		case SDL_CONTROLLERBUTTONDOWN:
			pInputSystem->JoystickButtonPress(event->cbutton.which, event->cbutton.button);
			break;
		case SDL_CONTROLLERBUTTONUP:
			pInputSystem->JoystickButtonRelease(event->cbutton.which, event->cbutton.button);
			break;

		case SDL_CONTROLLERDEVICEADDED:
			pInputSystem->JoystickHotplugAdded(event->cdevice.which);
			break;
		case SDL_CONTROLLERDEVICEREMOVED:
			pInputSystem->JoystickHotplugRemoved(event->cdevice.which);
			SearchForDevice();
			break;
	}

	return 1;
}

//-----------------------------------------------------------------------------
// Initialize all joysticks
//-----------------------------------------------------------------------------
void CInputSystem::InitializeJoysticks( void )
{
	if ( m_bJoystickInitialized )
	{
		ShutdownJoysticks();
	}

	// assume no joystick
	m_nJoystickCount = 0;
	memset( m_pJoystickInfo, 0, sizeof( m_pJoystickInfo ) );
	for ( int i = 0; i < MAX_JOYSTICKS; ++i )
	{
		m_pJoystickInfo[ i ].m_nDeviceId = -1;
	}

	// abort startup if user requests no joystick
	if ( CommandLine()->FindParm("-nojoy") ) return;

	const char *controllerConfig = joy_gamecontroller_config.GetString();
	if ( strlen(controllerConfig) > 0 )
	{
		DevMsg("Passing joy_gamecontroller_config to SDL ('%s').\n", controllerConfig);
		// We need to pass this hint to SDL *before* we init the gamecontroller subsystem, otherwise it gets ignored.
		SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG, controllerConfig);
	}

	if ( SDL_InitSubSystem( SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC ) == -1 )
	{
		Warning("Joystick init failed -- SDL_Init(SDL_INIT_GAMECONTROLLER|SDL_INIT_HAPTIC) failed: %s.\n", SDL_GetError());
		return;
	}

	m_bJoystickInitialized = true;

	SDL_AddEventWatch(JoystickSDLWatcher, this);

	const int totalSticks = SDL_NumJoysticks();
	for ( int i = 0; i < totalSticks; i++ )
	{
		if ( SDL_IsGameController(i) )
		{
			JoystickHotplugAdded(i);
		} 
		else
		{
			SDL_JoystickGUID joyGUID = SDL_JoystickGetDeviceGUID(i);
			char szGUID[sizeof(joyGUID.data)*2 + 1];
			SDL_JoystickGetGUIDString(joyGUID, szGUID, sizeof(szGUID));

			Msg("Found joystick '%s' (%s), but no recognized controller configuration for it.\n", SDL_JoystickNameForIndex(i), szGUID);
		}
	}

	if ( totalSticks < 1 )
	{
		Msg("Did not detect any valid joysticks.\n");
	}
}

void CInputSystem::ShutdownJoysticks()
{
	if ( !m_bJoystickInitialized )
	{
		return;
	}

	SDL_DelEventWatch( JoystickSDLWatcher, this );
	if ( m_pJoystickInfo[ 0 ].m_pDevice != NULL )
	{
		JoystickHotplugRemoved( m_pJoystickInfo[ 0 ].m_nDeviceId );
	}
	SDL_QuitSubSystem( SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC );

	m_bJoystickInitialized = false;
}

// Update the joy_xcontroller_found convar to force CInput::JoyStickMove to re-exec 360controller-linux.cfg
static void SetJoyXControllerFound( bool found )
{
	static ConVarRef xcontrollerVar( "joy_xcontroller_found" );
	static ConVarRef joystickVar( "joystick" );
	if ( xcontrollerVar.IsValid() )
	{
		xcontrollerVar.SetValue(found);
	}

	if ( found && joystickVar.IsValid() )
	{
		joystickVar.SetValue(true);
	}
}

void CInputSystem::JoystickHotplugAdded( int joystickIndex )
{
	// SDL_IsGameController doesn't bounds check its inputs.
	if ( joystickIndex < 0 || joystickIndex >= SDL_NumJoysticks() )
	{
		return;
	}

	if ( !SDL_IsGameController(joystickIndex) )
	{
		Warning("Joystick is not recognized by the game controller system. You can configure the controller in Steam Big Picture mode.\n");
		return;
	}

	SDL_Joystick *joystick = SDL_JoystickOpen(joystickIndex);
	if ( joystick == NULL )
	{
		Warning("Could not open joystick %i: %s", joystickIndex, SDL_GetError());
		return;
	}

	int joystickId = SDL_JoystickInstanceID(joystick);
	SDL_JoystickClose(joystick);

	int activeJoystick = joy_active.GetInt();
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( activeJoystick < 0 )
	{
		// Only opportunistically open devices if we don't have one open already.
		if ( info.m_nDeviceId != -1 )
		{
			Msg("Detected supported joystick #%i '%s'. Currently active joystick is #%i.\n", joystickId, SDL_JoystickNameForIndex(joystickIndex), info.m_nDeviceId);
			return;
		}
	}
	else if ( activeJoystick != joystickId )
	{
		Msg("Detected supported joystick #%i '%s'. Currently active joystick is #%i.\n", joystickId, SDL_JoystickNameForIndex(joystickIndex), activeJoystick);
		return;
	}

	if ( info.m_nDeviceId != -1 )
	{
		// Don't try to open the device we already have open.
		if ( info.m_nDeviceId == joystickId )
		{
			return;
		}

		DevMsg("Joystick #%i already initialized, removing it first.\n", info.m_nDeviceId);
		JoystickHotplugRemoved(info.m_nDeviceId);
	}

	Msg("Initializing joystick #%i and making it active.\n", joystickId);

	SDL_GameController *controller = SDL_GameControllerOpen(joystickIndex);
	if ( controller == NULL )
	{
		Warning("Failed to open joystick %i: %s\n", joystickId, SDL_GetError());
		return;
	}

	// XXX: This will fail if this is a *real* hotplug event (and not coming from the initial InitializeJoysticks call).
	// That's because the SDL haptic subsystem currently doesn't do hotplugging. Everything but haptics will work fine.
	SDL_Haptic *haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(controller));
	if ( haptic == NULL || SDL_HapticRumbleInit(haptic) != 0 )
	{
		Warning("Unable to initialize rumble for joystick #%i: %s\n", joystickId, SDL_GetError());
		haptic = NULL;
	}

	info.m_pDevice = controller;
	info.m_pHaptic = haptic;
	info.m_nDeviceId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller));
	info.m_nButtonCount = SDL_CONTROLLER_BUTTON_MAX;
	info.m_bRumbleEnabled = false;

	SetJoyXControllerFound(true);
	EnableJoystickInput(0, true);
	m_nJoystickCount = 1;
	m_bXController =  true;

	// We reset joy_active to -1 because joystick ids are never reused - until you restart.
	// Setting it to -1 means that you get expected hotplugging behavior if you disconnect the current joystick.
	joy_active.SetValue(-1);
}

void CInputSystem::JoystickHotplugRemoved( int joystickId )
{
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( info.m_nDeviceId != joystickId )
	{
		DevMsg("Ignoring hotplug remove for #%i, active joystick is #%i.\n", joystickId, info.m_nDeviceId);
		return;
	}

	if ( info.m_pDevice == NULL )
	{
		info.m_nDeviceId = -1;
		DevMsg("Got hotplug remove event for removed joystick #%i, ignoring.\n", joystickId);
		return;
	}

	m_nJoystickCount = 0;
	m_bXController =  false;
	EnableJoystickInput(0, false);
	SetJoyXControllerFound(false);

	SDL_HapticClose((SDL_Haptic *)info.m_pHaptic);
	SDL_GameControllerClose((SDL_GameController *)info.m_pDevice);

	info.m_pHaptic = NULL;
	info.m_pDevice = NULL;
	info.m_nButtonCount = 0;
	info.m_nDeviceId = -1;
	info.m_bRumbleEnabled = false;

	Msg("Joystick %i removed.\n", joystickId);
}

void CInputSystem::JoystickButtonPress( int joystickId, int button )
{
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( info.m_nDeviceId != joystickId )
	{
		Warning("Not active device input system (%i x %i)\n", info.m_nDeviceId, joystickId);
		return;
	}

	ButtonCode_t buttonCode = ControllerButtonToButtonCode((SDL_GameControllerButton)button);
	PostButtonPressedEvent(IE_ButtonPressed, m_nLastSampleTick, buttonCode, buttonCode);
}

void CInputSystem::JoystickButtonRelease( int joystickId, int button )
{
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( info.m_nDeviceId != joystickId )
	{
		return;
	}

	ButtonCode_t buttonCode = ControllerButtonToButtonCode((SDL_GameControllerButton)button);
	PostButtonReleasedEvent(IE_ButtonReleased, m_nLastSampleTick, buttonCode, buttonCode);
}


void CInputSystem::JoystickAxisMotion( int joystickId, int axis, int value )
{
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( info.m_nDeviceId != joystickId )
	{
		return;
	}

	AnalogCode_t code = ControllerAxisToAnalogCode((SDL_GameControllerAxis)axis);
	if ( code == ANALOG_CODE_INVALID )
	{
		Warning("Invalid code for axis %i\n", axis);
		return;
	}

	ButtonCode_t buttonCode = BUTTON_CODE_NONE;
	switch ( axis )
	{
		case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
			buttonCode = KEY_XBUTTON_RTRIGGER;
			break;
		case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
			buttonCode = KEY_XBUTTON_LTRIGGER;
			break;
	}

	if ( buttonCode != BUTTON_CODE_NONE )
	{
		int pressThreshold = joy_axisbutton_threshold.GetFloat() * 32767;
		int keyIndex = buttonCode - KEY_XBUTTON_LTRIGGER;
		Assert( keyIndex < ARRAYSIZE( m_appXKeys[0] ) && keyIndex >= 0 );

		appKey_t &key = m_appXKeys[0][keyIndex];
		if ( value > pressThreshold )
		{
			if ( key.repeats < 1 )
			{
				PostButtonPressedEvent( IE_ButtonPressed, m_nLastSampleTick, buttonCode, buttonCode );
			}
			key.repeats++;
		}
		else
		{
			PostButtonReleasedEvent( IE_ButtonReleased, m_nLastSampleTick, buttonCode, buttonCode );
			key.repeats = 0;
		}
	}

	int minValue = joy_axis_deadzone.GetFloat() * 32767;
	if ( abs(value) < minValue )
	{
		value = 0;
	}

	InputState_t& state = m_InputState[ m_bIsPolling ];
	state.m_pAnalogDelta[ code ] = value - state.m_pAnalogValue[ code ];
	state.m_pAnalogValue[ code ] = value;
	if ( state.m_pAnalogDelta[ code ] != 0 )
	{
		PostEvent(IE_AnalogValueChanged, m_nLastSampleTick, code, value, 0);
	}
}

//-----------------------------------------------------------------------------
//	Process the event
//-----------------------------------------------------------------------------
void CInputSystem::JoystickButtonEvent( ButtonCode_t button, int sample )
{
	// Not used - we post button events from JoystickButtonPress/Release.
}


//-----------------------------------------------------------------------------
// Update the joystick button state
//-----------------------------------------------------------------------------
void CInputSystem::UpdateJoystickButtonState( int nJoystick )
{
	// We don't sample - we get events posted by SDL_GameController in JoystickSDLWatcher
}


//-----------------------------------------------------------------------------
// Update the joystick POV control
//-----------------------------------------------------------------------------
void CInputSystem::UpdateJoystickPOVControl( int nJoystick )
{
	// SDL GameController does not support joystick POV. Should we poll?
}


//-----------------------------------------------------------------------------
// Purpose: Sample the joystick
//-----------------------------------------------------------------------------
void CInputSystem::PollJoystick( void )
{
	// We only pump the SDL event loop if we're not an SDL app, since otherwise PollInputState_Platform calls into CSDLMgr to pump it.
	// Our state updates happen in events posted by SDL_GameController in JoystickSDLWatcher, so the loop is empty.
#if !defined( USE_SDL )
	SDL_Event event;
	int nEventsProcessed = 0;

	SDL_PumpEvents();
	while ( SDL_PollEvent( &event ) && nEventsProcessed < 100 )
	{
		nEventsProcessed++;
	}
#endif
}

void CInputSystem::SetXDeviceRumble( float fLeftMotor, float fRightMotor, int userId )
{
	JoystickInfo_t& info = m_pJoystickInfo[ 0 ];
	if ( info.m_nDeviceId < 0  || info.m_pHaptic == NULL )
	{
		return;
	}

	float strength = (fLeftMotor + fRightMotor) / 2.f;
	static ConVarRef joystickVar( "joystick" );

	// 0f means "stop".
	bool shouldStop = ( strength < 0.01f );
	// If they've disabled the gamecontroller in settings, never rumble.
	if ( !joystickVar.IsValid() || !joystickVar.GetBool() )
	{
		shouldStop = true;
	}

	if ( shouldStop )
	{
		if ( info.m_bRumbleEnabled )
		{
			SDL_HapticRumbleStop( (SDL_Haptic *)info.m_pHaptic );
			info.m_bRumbleEnabled = false;
			info.m_fCurrentRumble = 0.0f;
		}

		return;
	}

	// If there's little change, then don't change the rumble strength.
	if ( info.m_bRumbleEnabled && abs(info.m_fCurrentRumble - strength) < 0.01f )
	{
		return;
	}

	info.m_bRumbleEnabled = true;
	info.m_fCurrentRumble = strength;

	if ( SDL_HapticRumblePlay((SDL_Haptic *)info.m_pHaptic, strength, SDL_HAPTIC_INFINITY) != 0 )
	{
		Warning("Couldn't play rumble (strength %.1f): %s\n", strength, SDL_GetError());
	}
}

ButtonCode_t ControllerButtonToButtonCode( SDL_GameControllerButton button )
{
	switch ( button )
	{
		case SDL_CONTROLLER_BUTTON_A: // KEY_XBUTTON_A
		case SDL_CONTROLLER_BUTTON_B: // KEY_XBUTTON_B
		case SDL_CONTROLLER_BUTTON_X: // KEY_XBUTTON_X
		case SDL_CONTROLLER_BUTTON_Y: // KEY_XBUTTON_Y
			return JOYSTICK_BUTTON(0, button);

		case SDL_CONTROLLER_BUTTON_BACK:
			return KEY_XBUTTON_BACK;
		case SDL_CONTROLLER_BUTTON_START:
			return KEY_XBUTTON_START;

		case SDL_CONTROLLER_BUTTON_GUIDE:
			return KEY_XBUTTON_BACK; // XXX: How are we supposed to handle this? Steam overlay etc.

		case SDL_CONTROLLER_BUTTON_LEFTSTICK:
			return KEY_XBUTTON_STICK1;
		case SDL_CONTROLLER_BUTTON_RIGHTSTICK:
			return KEY_XBUTTON_STICK2;
		case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
			return KEY_XBUTTON_LEFT_SHOULDER;
		case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
			return KEY_XBUTTON_RIGHT_SHOULDER;

		case SDL_CONTROLLER_BUTTON_DPAD_UP:
			return KEY_XBUTTON_UP;
		case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
			return KEY_XBUTTON_DOWN;
		case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
			return KEY_XBUTTON_LEFT;
		case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
			return KEY_XBUTTON_RIGHT;
	}

	return BUTTON_CODE_NONE;
}

AnalogCode_t ControllerAxisToAnalogCode( SDL_GameControllerAxis axis )
{
	switch ( axis )
	{
		case SDL_CONTROLLER_AXIS_LEFTX:
			return JOYSTICK_AXIS(0, JOY_AXIS_X);
		case SDL_CONTROLLER_AXIS_LEFTY:
			return JOYSTICK_AXIS(0, JOY_AXIS_Y);

		case SDL_CONTROLLER_AXIS_RIGHTX:
			return JOYSTICK_AXIS(0, JOY_AXIS_U);
		case SDL_CONTROLLER_AXIS_RIGHTY:
			return JOYSTICK_AXIS(0, JOY_AXIS_R);

		case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
		case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
			return JOYSTICK_AXIS(0, JOY_AXIS_Z);
	}

	return ANALOG_CODE_INVALID;
}