/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 1998-2000, Matthes Bender
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2010-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

/* Gamepad control */

#include "C4Include.h"
#include "C4ForbidLibraryCompilation.h"
#include "platform/C4GamePadCon.h"

#include "game/C4Application.h"
#include "object/C4ObjectCom.h"

#if defined(HAVE_SDL) && !defined(USE_CONSOLE)

#include <SDL.h>

C4GamePadControl::C4GamePadControl()
{
	// SDL2 will only report events when the window has focus, so set
	// this hint as we don't have a window
	SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");

	if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC | SDL_INIT_EVENTS) != 0)
		LogF("SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER): %s", SDL_GetError());
	SDL_GameControllerEventState(SDL_ENABLE);
	if (!GetGamePadCount()) Log("No Gamepad found");
}

C4GamePadControl::~C4GamePadControl()
{
	// All gamepads have to be released before quitting SDL.
	Gamepads.clear();
	SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS);
}

void C4GamePadControl::Execute()
{
#ifdef USE_SDL_MAINLOOP
	if (!Application.isEditor) return;
#endif
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
		case SDL_CONTROLLERAXISMOTION:
		case SDL_CONTROLLERBUTTONDOWN:
		case SDL_CONTROLLERBUTTONUP:
			FeedEvent(event, FEED_BUTTONS);
			break;
		case SDL_JOYDEVICEADDED:
		case SDL_CONTROLLERDEVICEADDED:
		case SDL_CONTROLLERDEVICEREMOVED:
			CheckGamePad(event);
			break;
		}
	}
}

namespace
{
	const int deadZone = 16000;

	// Axis strength uses the full signed 16 bit integer range. As we're
	// splitting axes in left/right and up/down, it's preferable to have
	// symmetrical ranges [0, 2^15 - 1] in both directions.
	inline int32_t abs_strength(int32_t strength)
	{
		return strength >= 0 ? strength : -(strength + 1);
	}
}

void C4GamePadControl::FeedEvent(const SDL_Event& event, int feed)
{
	switch (event.type)
	{
	case SDL_CONTROLLERAXISMOTION:
	{
		C4KeyCode minCode = KEY_Gamepad(KEY_CONTROLLER_Axis(event.caxis.axis, false));
		C4KeyCode maxCode = KEY_Gamepad(KEY_CONTROLLER_Axis(event.caxis.axis, true));
		int32_t value = abs_strength(event.caxis.value);
		uint8_t which = event.caxis.which;
		C4KeyCode keyCode = event.caxis.value >= 0 ? maxCode : minCode;

		auto doInput = [&](C4KeyEventType event, int32_t strength)
		{
			Game.DoKeyboardInput(
			  C4KeyCodeEx(KEY_Gamepad(keyCode), KEYS_None, false, which),
			  event, nullptr, false, strength);
		};

		if (feed & FEED_BUTTONS)
		{
			// Also emulate button presses.
			if (PressedAxis.count(keyCode) && value <= deadZone)
			{
				PressedAxis.erase(keyCode);
				doInput(KEYEV_Up, -1);
			}
			else if (!PressedAxis.count(keyCode) && value > deadZone)
			{
				PressedAxis.insert(keyCode);
				doInput(KEYEV_Down, -1);
			}
		}
		if (feed & FEED_MOVED)
			doInput(KEYEV_Moved, value);

		AxisEvents[keyCode] = event;

		break;
	}
	case SDL_CONTROLLERBUTTONDOWN:
		if (feed & FEED_BUTTONS)
			Game.DoKeyboardInput(
			  C4KeyCodeEx(KEY_Gamepad(KEY_CONTROLLER_Button(event.cbutton.button)), KEYS_None, false, event.cbutton.which),
			  KEYEV_Down);
		break;
	case SDL_CONTROLLERBUTTONUP:
		if (feed & FEED_BUTTONS)
			Game.DoKeyboardInput(
			  C4KeyCodeEx(KEY_Gamepad(KEY_CONTROLLER_Button(event.cbutton.button)), KEYS_None, false, event.cbutton.which),
			  KEYEV_Up);
		break;
	}
}

void C4GamePadControl::CheckGamePad(const SDL_Event& e)
{
	switch (e.type)
	{
	case SDL_JOYDEVICEADDED:
		// Report that an unsupported joystick device has been detected, to help with controller issues.
		if (!SDL_IsGameController(e.jdevice.which))
			LogF("Gamepad %s isn't supported.", SDL_JoystickNameForIndex(e.jdevice.which));
		break;
	case SDL_CONTROLLERDEVICEADDED:
	{
		auto device = std::make_shared<C4GamePadOpener>(e.cdevice.which);
		Gamepads[device->GetID()] = device;
		LogF("Gamepad #%d connected: %s", device->GetID(), SDL_JoystickNameForIndex(e.cdevice.which));
		break;
	}
	case SDL_CONTROLLERDEVICEREMOVED:
		LogF("Gamepad #%d disconnected.", e.cdevice.which);
		Gamepads.erase(e.cdevice.which);
		break;
	}
}

void C4GamePadControl::DoAxisInput()
{
	for (auto const &e : AxisEvents)
	{
		FeedEvent(e.second, FEED_MOVED);
	}
	AxisEvents.clear();
}

int C4GamePadControl::GetGamePadCount()
{
	// Not all Joysticks are game controllers.
	int count = 0;
	for (int i = 0; i < SDL_NumJoysticks(); i++)
		if (SDL_IsGameController(i))
			count++;
	return count;
}

std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetGamePad(int gamepad)
{
	if (gamepad >= 0)
		for (const auto& p : Gamepads)
			if (gamepad-- == 0)
				return p.second;
	return nullptr;
}

std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetGamePadByID(int32_t id)
{
	auto it = Gamepads.find(id);
	if (it != Gamepads.end())
		return it->second;
	return nullptr;
}

std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetAvailableGamePad()
{
	for (const auto& p : Gamepads)
		if (p.second->GetPlayer() < 0)
			return p.second;
	return nullptr;
}

C4GamePadOpener::C4GamePadOpener(int iGamepad)
{
	int n = iGamepad;
	for (int i = 0; i < SDL_NumJoysticks(); i++)
		if (SDL_IsGameController(i) && n-- == 0)
		{
			controller = SDL_GameControllerOpen(i);
			if (!controller) LogF("SDL: %s", SDL_GetError());
			SDL_Joystick *joystick = SDL_GameControllerGetJoystick(controller);
			haptic = SDL_HapticOpenFromJoystick(joystick);
			if (haptic && SDL_HapticRumbleSupported(haptic))
				SDL_HapticRumbleInit(haptic);
			else
				LogF("Gamepad #%d %s does not support rumbling.", SDL_JoystickInstanceID(joystick), SDL_JoystickName(joystick));
			break;
		}

	if (!controller) LogF("Gamepad %d not available", iGamepad);
}

C4GamePadOpener::~C4GamePadOpener()
{
	if (haptic) SDL_HapticClose(haptic);
	if (controller) SDL_GameControllerClose(controller);
}

int32_t C4GamePadOpener::GetID()
{
	return SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller));
}

bool C4GamePadOpener::IsAttached()
{
	return !!SDL_GameControllerGetAttached(controller);
}

void C4GamePadOpener::PlayRumble(float strength, uint32_t length)
{
	if (SDL_HapticRumbleSupported(haptic))
		SDL_HapticRumblePlay(haptic, strength, length);
}

void C4GamePadOpener::StopRumble()
{
	if (SDL_HapticRumbleSupported(haptic))
		SDL_HapticRumbleStop(haptic);
}

#else

// Dedicated server and everything else with neither Win32 nor SDL.

C4GamePadControl::C4GamePadControl() { Log("WARNING: Engine without Gamepad support"); }
C4GamePadControl::~C4GamePadControl() = default;
void C4GamePadControl::Execute() { }
void C4GamePadControl::DoAxisInput() { }
int C4GamePadControl::GetGamePadCount() { return 0; }
std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetGamePad(int gamepad) { return nullptr; }
std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetGamePadByID(int32_t id) { return nullptr; }
std::shared_ptr<C4GamePadOpener> C4GamePadControl::GetAvailableGamePad() { return nullptr; }

C4GamePadOpener::C4GamePadOpener(int iGamepad) { }
C4GamePadOpener::~C4GamePadOpener() = default;
int32_t C4GamePadOpener::GetID() { return -1; }
bool C4GamePadOpener::IsAttached() { return false; }
void C4GamePadOpener::PlayRumble(float strength, uint32_t length) { }
void C4GamePadOpener::StopRumble() { }

#endif
