#include "HyperTwistUX/HyperTwistPlayerControllerBase.h"

#include "Components/AudioComponent.h"
#include "Blueprint/UserWidget.h"
#include "GameFramework/InputSettings.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistRecognition/HyperTwistSpeechLibrary.h"
#include "HyperTwistUX/HyperTwistCoachAssistantWidget.h"
#include "HyperTwistUX/HyperTwistDictationCaptureComponent.h"
#include "HyperTwistUX/HyperTwistPauseMenuWidget.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "JsonObjectConverter.h"
#include "Misc/ScopeExit.h"
#include "Sound/SoundWaveProcedural.h"
#include "TimerManager.h"

namespace HyperTwistPlayerControllerBaseInternal
{
	uint16 ReadLittleEndianUint16(const TArray<uint8>& Bytes, const int32 Offset)
	{
		if (Offset < 0 || Offset + 1 >= Bytes.Num())
		{
			return 0;
		}
		return static_cast<uint16>(Bytes[Offset])
			| (static_cast<uint16>(Bytes[Offset + 1]) << 8);
	}

	uint32 ReadLittleEndianUint32(const TArray<uint8>& Bytes, const int32 Offset)
	{
		if (Offset < 0 || Offset + 3 >= Bytes.Num())
		{
			return 0;
		}
		return static_cast<uint32>(Bytes[Offset])
			| (static_cast<uint32>(Bytes[Offset + 1]) << 8)
			| (static_cast<uint32>(Bytes[Offset + 2]) << 16)
			| (static_cast<uint32>(Bytes[Offset + 3]) << 24);
	}

	bool TryReadPcm16WaveFormat(
		const TArray<uint8>& Bytes,
		int32& OutSampleRateHz,
		int32& OutChannelCount
	)
	{
		OutSampleRateHz = 0;
		OutChannelCount = 0;
		if (Bytes.Num() < 44
			|| Bytes[0] != 'R'
			|| Bytes[1] != 'I'
			|| Bytes[2] != 'F'
			|| Bytes[3] != 'F'
			|| Bytes[8] != 'W'
			|| Bytes[9] != 'A'
			|| Bytes[10] != 'V'
			|| Bytes[11] != 'E')
		{
			return false;
		}

		bool bFoundFormat = false;
		bool bFoundAudio = false;
		int32 Offset = 12;
		while (Offset + 8 <= Bytes.Num())
		{
			const uint32 ChunkSize = ReadLittleEndianUint32(Bytes, Offset + 4);
			const int64 ChunkDataOffset = static_cast<int64>(Offset) + 8;
			const int64 ChunkEndOffset = ChunkDataOffset + ChunkSize;
			if (ChunkEndOffset > Bytes.Num())
			{
				return false;
			}

			if (Bytes[Offset] == 'f'
				&& Bytes[Offset + 1] == 'm'
				&& Bytes[Offset + 2] == 't'
				&& Bytes[Offset + 3] == ' ')
			{
				if (ChunkSize < 16)
				{
					return false;
				}
				const uint16 AudioFormat =
					ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset));
				const uint16 ChannelCount =
					ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset) + 2);
				const uint32 SampleRate =
					ReadLittleEndianUint32(Bytes, static_cast<int32>(ChunkDataOffset) + 4);
				const uint16 BitsPerSample =
					ReadLittleEndianUint16(Bytes, static_cast<int32>(ChunkDataOffset) + 14);
				if (AudioFormat != 1
					|| BitsPerSample != 16
					|| ChannelCount == 0
					|| ChannelCount > 8
					|| SampleRate < 8000
					|| SampleRate > 192000)
				{
					return false;
				}
				OutChannelCount = ChannelCount;
				OutSampleRateHz = SampleRate;
				bFoundFormat = true;
			}
			else if (Bytes[Offset] == 'd'
				&& Bytes[Offset + 1] == 'a'
				&& Bytes[Offset + 2] == 't'
				&& Bytes[Offset + 3] == 'a')
			{
				bFoundAudio = ChunkSize > 0;
			}

			Offset = static_cast<int32>(ChunkEndOffset + (ChunkSize & 1u));
		}
		return bFoundFormat && bFoundAudio;
	}

	FString BuildVoiceNarrationUrl(const FString& Endpoint)
	{
		FString Result = Endpoint.TrimStartAndEnd();
		if (Result.EndsWith(TEXT("/voice/narrate"), ESearchCase::IgnoreCase))
		{
			return Result;
		}
		while (Result.EndsWith(TEXT("/")))
		{
			Result.LeftChopInline(1, EAllowShrinking::No);
		}
		return Result.IsEmpty()
			? FString()
			: Result + TEXT("/voice/narrate");
	}
}

AHyperTwistPlayerControllerBase::AHyperTwistPlayerControllerBase()
{
	bShowMouseCursor = true;
	bEnableClickEvents = true;
	bEnableMouseOverEvents = true;
	GlobalDictationCapture = CreateDefaultSubobject<UHyperTwistDictationCaptureComponent>(
		TEXT("GlobalDictationCapture"));
}

void AHyperTwistPlayerControllerBase::BeginPlay()
{
	Super::BeginPlay();
	if (GlobalDictationCapture != nullptr)
	{
		GlobalDictationCapture->OnStateChanged.AddDynamic(
			this,
			&AHyperTwistPlayerControllerBase::HandleGlobalDictationStateChanged);
		GlobalDictationCapture->OnTranscriptReady.AddDynamic(
			this,
			&AHyperTwistPlayerControllerBase::HandleGlobalDictationTranscriptReady);
	}
	ReloadAndApplyPlayerPreferences(false);
	ApplyGameAndUiInputMode();
	if (bEnableGlobalAssistantPanel
		&& PlayerPreferences.bAssistantPanelEnabled
		&& PlayerPreferences.bAssistantPanelOpenByDefault
		&& GetWorld() != nullptr)
	{
		TWeakObjectPtr<AHyperTwistPlayerControllerBase> WeakThis(this);
		GetWorld()->GetTimerManager().SetTimerForNextTick(
			[WeakThis]()
			{
				if (WeakThis.IsValid())
				{
					WeakThis->ShowAssistantPanel();
				}
			});
	}
}

void AHyperTwistPlayerControllerBase::EndPlay(
	const EEndPlayReason::Type EndPlayReason
)
{
	StopCoachNarration();
	CancelGlobalDictation();
	Super::EndPlay(EndPlayReason);
}

void AHyperTwistPlayerControllerBase::SetupInputComponent()
{
	Super::SetupInputComponent();
	if (InputComponent == nullptr)
	{
		return;
	}

	PlayerPreferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	const FKey ConfiguredPauseKey =
		UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
			PlayerPreferences,
			TEXT("navigation.pause"));
	FInputKeyBinding& EscapeBinding = InputComponent->BindKey(
		EKeys::Escape,
		IE_Pressed,
		this,
		&AHyperTwistPlayerControllerBase::HandlePauseShortcut);
	EscapeBinding.bExecuteWhenPaused = true;
	if (ConfiguredPauseKey.IsValid() && ConfiguredPauseKey != EKeys::Escape)
	{
		FInputKeyBinding& PauseBinding = InputComponent->BindKey(
			ConfiguredPauseKey,
			IE_Pressed,
			this,
			&AHyperTwistPlayerControllerBase::HandlePauseShortcut);
		PauseBinding.bExecuteWhenPaused = true;
	}

	const FKey MainMenuKey =
		UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
			PlayerPreferences,
			TEXT("navigation.main-menu"));
	if (MainMenuKey.IsValid())
	{
		FInputKeyBinding& MainMenuBinding = InputComponent->BindKey(
			MainMenuKey,
			IE_Pressed,
			this,
			&AHyperTwistPlayerControllerBase::HandleMainMenuShortcut);
		MainMenuBinding.bExecuteWhenPaused = true;
	}

	const FKey AssistantKey =
		UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
			PlayerPreferences,
			TEXT("assistant.toggle"));
	if (AssistantKey.IsValid())
	{
		InputComponent->BindKey(
			AssistantKey,
			IE_Pressed,
			this,
			&AHyperTwistPlayerControllerBase::HandleAssistantShortcut);
	}

	if (PlayerPreferences.bSpeechInputEnabled)
	{
		const FKey DictationKey =
			UHyperTwistPlayerSettingsLibrary::ResolveKeyBinding(
				PlayerPreferences,
				TEXT("speech.push-to-talk"));
		if (DictationKey.IsValid())
		{
			InputComponent->BindKey(
				DictationKey,
				IE_Pressed,
				this,
				&AHyperTwistPlayerControllerBase::HandleGlobalDictationPressed);
			InputComponent->BindKey(
				DictationKey,
				IE_Released,
				this,
				&AHyperTwistPlayerControllerBase::HandleGlobalDictationReleased);
		}
	}
}

bool AHyperTwistPlayerControllerBase::ShowPauseMenu()
{
	if (!bEnableGlobalPauseMenu)
	{
		return false;
	}
	PrepareForModalUi();
	HideAssistantPanel();
	if (ActivePauseMenuWidget != nullptr)
	{
		if (!ActivePauseMenuWidget->IsInViewport())
		{
			ActivePauseMenuWidget->AddToViewport(PauseMenuZOrder);
		}
		return true;
	}

	TSubclassOf<UHyperTwistPauseMenuWidget> ResolvedClass = PauseMenuWidgetClass;
	if (*ResolvedClass == nullptr)
	{
		ResolvedClass = UHyperTwistPauseMenuWidget::StaticClass();
	}
	ActivePauseMenuWidget = CreateWidget<UHyperTwistPauseMenuWidget>(
		this,
		ResolvedClass);
	if (ActivePauseMenuWidget == nullptr
		|| !ActivePauseMenuWidget->PreparePauseSurface())
	{
		ActivePauseMenuWidget = nullptr;
		return false;
	}

	ActivePauseMenuWidget->SetPuzzleContext(
		GetPauseMenuTitle(),
		GetPauseMenuSubtitle());
	ActivePauseMenuWidget->OnResumeRequested.AddDynamic(
		this,
		&AHyperTwistPlayerControllerBase::HandlePauseResumeRequested);
	ActivePauseMenuWidget->OnMainMenuRequested.AddDynamic(
		this,
		&AHyperTwistPlayerControllerBase::HandlePauseMainMenuRequested);
	ActivePauseMenuWidget->OnQuitRequested.AddDynamic(
		this,
		&AHyperTwistPlayerControllerBase::HandlePauseQuitRequested);
	ActivePauseMenuWidget->AddToViewport(PauseMenuZOrder);
	SetPause(true);
	bShowMouseCursor = true;
	FInputModeUIOnly InputMode;
	InputMode.SetWidgetToFocus(ActivePauseMenuWidget->TakeWidget());
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);
	ActivePauseMenuWidget->SetKeyboardFocus();
	return true;
}

void AHyperTwistPlayerControllerBase::HidePauseMenu()
{
	if (ActivePauseMenuWidget != nullptr)
	{
		ActivePauseMenuWidget->RemoveFromParent();
		ActivePauseMenuWidget = nullptr;
	}
	SetPause(false);
	ReloadAndApplyPlayerPreferences(false);
	ApplyGameAndUiInputMode();
}

void AHyperTwistPlayerControllerBase::ReturnToMainMenu()
{
	PrepareForModalUi();
	SetPause(false);
	HideAssistantPanel();
	if (ActivePauseMenuWidget != nullptr)
	{
		ActivePauseMenuWidget->RemoveFromParent();
		ActivePauseMenuWidget = nullptr;
	}
	const FString Options = FString::Printf(
		TEXT("game=%s"),
		UHyperTwistPlayerSettingsLibrary::GetMainMenuGameModeClassPath());
	UGameplayStatics::OpenLevel(
		this,
		FName(UHyperTwistPlayerSettingsLibrary::GetMainMenuMapAssetPath()),
		true,
		Options);
}

void AHyperTwistPlayerControllerBase::QuitToDesktop()
{
	PrepareForModalUi();
	HideAssistantPanel();
	UKismetSystemLibrary::QuitGame(
		this,
		this,
		EQuitPreference::Quit,
		false);
}

bool AHyperTwistPlayerControllerBase::ShowAssistantPanel()
{
	ReloadAndApplyPlayerPreferences(false);
	if (!bEnableGlobalAssistantPanel
		|| !PlayerPreferences.bAssistantPanelEnabled
		|| ActivePauseMenuWidget != nullptr
		|| (GetWorld() != nullptr && GetWorld()->IsPaused()))
	{
		return false;
	}
	PrepareForModalUi();
	if (ActiveAssistantPanelWidget != nullptr)
	{
		if (!ActiveAssistantPanelWidget->IsInViewport())
		{
			ActiveAssistantPanelWidget->AddToViewport(AssistantPanelZOrder);
		}
		return true;
	}

	TSubclassOf<UHyperTwistCoachAssistantWidget> ResolvedClass =
		AssistantPanelWidgetClass;
	if (*ResolvedClass == nullptr)
	{
		ResolvedClass = UHyperTwistCoachAssistantWidget::StaticClass();
	}
	ActiveAssistantPanelWidget = CreateWidget<UHyperTwistCoachAssistantWidget>(
		this,
		ResolvedClass);
	if (ActiveAssistantPanelWidget == nullptr
		|| !ActiveAssistantPanelWidget->PrepareAssistantSurface())
	{
		ActiveAssistantPanelWidget = nullptr;
		return false;
	}

	ActiveAssistantPanelWidget->SetPuzzleContext(
		GetPauseMenuTitle(),
		GetPauseMenuSubtitle());
	ActiveAssistantPanelWidget->OnCloseRequested.AddDynamic(
		this,
		&AHyperTwistPlayerControllerBase::HandleAssistantCloseRequested);
	ActiveAssistantPanelWidget->AddToViewport(AssistantPanelZOrder);
	bShowMouseCursor = true;
	FInputModeGameAndUI InputMode;
	InputMode.SetWidgetToFocus(ActiveAssistantPanelWidget->TakeWidget());
	InputMode.SetHideCursorDuringCapture(false);
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);
	ActiveAssistantPanelWidget->SetKeyboardFocus();
	return true;
}

void AHyperTwistPlayerControllerBase::HideAssistantPanel()
{
	CancelGlobalDictation();
	StopCoachNarration();
	if (ActiveAssistantPanelWidget != nullptr)
	{
		ActiveAssistantPanelWidget->RemoveFromParent();
		ActiveAssistantPanelWidget = nullptr;
	}
	if (ActivePauseMenuWidget == nullptr
		&& (GetWorld() == nullptr || !GetWorld()->IsPaused()))
	{
		ApplyGameAndUiInputMode();
	}
}

void AHyperTwistPlayerControllerBase::ToggleAssistantPanel()
{
	if (ActiveAssistantPanelWidget != nullptr)
	{
		HideAssistantPanel();
	}
	else
	{
		ShowAssistantPanel();
	}
}

bool AHyperTwistPlayerControllerBase::IsAssistantPanelOpen() const
{
	return ActiveAssistantPanelWidget != nullptr
			&& ActiveAssistantPanelWidget->IsInViewport();
}

bool AHyperTwistPlayerControllerBase::BeginGlobalDictation()
{
	ReloadAndApplyPlayerPreferences(false);
	if (GlobalDictationCapture == nullptr
		|| !PlayerPreferences.bSpeechInputEnabled
		|| !PlayerPreferences.bAssistantPanelEnabled
		|| !ShowAssistantPanel())
	{
		return false;
	}

	StopCoachNarration();
	FString FailureReason;
	const bool bStarted = GlobalDictationCapture->BeginCapture(FailureReason);
	bToggleDictationCaptureActive = bStarted
		&& PlayerPreferences.SpeechActivationMode == TEXT("toggle");
	return bStarted;
}

bool AHyperTwistPlayerControllerBase::EndGlobalDictation()
{
	if (GlobalDictationCapture == nullptr
		|| !GlobalDictationCapture->IsCapturing())
	{
		bToggleDictationCaptureActive = false;
		return false;
	}

	FString Transcript;
	FString FailureReason;
	const bool bCompleted =
		GlobalDictationCapture->EndCapture(Transcript, FailureReason);
	bToggleDictationCaptureActive = false;
	return bCompleted;
}

void AHyperTwistPlayerControllerBase::CancelGlobalDictation()
{
	if (GlobalDictationCapture != nullptr
		&& GlobalDictationCapture->IsCapturing())
	{
		GlobalDictationCapture->CancelCapture();
	}
	bToggleDictationCaptureActive = false;
}

void AHyperTwistPlayerControllerBase::ToggleGlobalDictation()
{
	if (IsGlobalDictationActive())
	{
		EndGlobalDictation();
	}
	else
	{
		BeginGlobalDictation();
	}
}

bool AHyperTwistPlayerControllerBase::IsGlobalDictationActive() const
{
	return GlobalDictationCapture != nullptr
		&& GlobalDictationCapture->IsCapturing();
}

bool AHyperTwistPlayerControllerBase::NarrateCoachResponse(
	const FString& ResponseText
)
{
	ReloadAndApplyPlayerPreferences(false);
	const FString SafeResponse = ResponseText.TrimStartAndEnd();
	if (!PlayerPreferences.bCoachNarrationEnabled || SafeResponse.IsEmpty())
	{
		return false;
	}

	FString FailureReason;
	if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
			PlayerPreferences.VoiceEndpoint,
			PlayerPreferences.bAllowCloudProviders,
			FailureReason))
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; narration is blocked by the provider privacy policy."),
			true);
		return false;
	}
	if (PlayerPreferences.VoiceModel.IsEmpty()
		|| PlayerPreferences.VoiceId.IsEmpty())
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; choose a voice model and voice in Settings."),
			true);
		return false;
	}

	FString Credential;
	const bool bHasCredential =
		UHyperTwistPlayerSettingsLibrary::LoadProviderCredential(
			TEXT("voice-api-key"),
			Credential);
	const bool bCredentialRequired =
		PlayerPreferences.VoiceProviderId == TEXT("openai")
		|| PlayerPreferences.VoiceProviderId == TEXT("elevenlabs");
	ON_SCOPE_EXIT
	{
		if (!Credential.IsEmpty())
		{
			Credential.Reset(Credential.Len());
		}
	};
	if (bCredentialRequired && (!bHasCredential || Credential.IsEmpty()))
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; save a protected voice API key to enable narration."),
			true);
		return false;
	}

	FHyperTwistNarrationSynthesisRequest Request =
		UHyperTwistContractLibrary::MakeSampleNarrationSynthesisRequest();
	Request.RequestId =
		FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphensLower);
	const FString NarrationText = SafeResponse.Left(6000);
	Request.ScriptText = NarrationText;
	Request.SubtitleSeedText = NarrationText;
	Request.VoiceProfileId = PlayerPreferences.VoiceId;
	Request.LengthScale = 1.0f / FMath::Max(PlayerPreferences.VoiceSpeed, 0.1f);
	Request.OrchestrationProfile.ModelBindingId = PlayerPreferences.VoiceModel;

	FString RequestBody;
	if (!FJsonObjectConverter::UStructToJsonObjectString(
			FHyperTwistNarrationSynthesisRequest::StaticStruct(),
			&Request,
			RequestBody,
			0,
			0))
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; narration request preparation failed."),
			true);
		return false;
	}

	const FString RequestUrl =
		HyperTwistPlayerControllerBaseInternal::BuildVoiceNarrationUrl(
			PlayerPreferences.VoiceEndpoint);
	if (RequestUrl.IsEmpty())
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; configure a narration endpoint."),
			true);
		return false;
	}

	StopCoachNarration();
	ActiveCoachNarrationRequest = FHttpModule::Get().CreateRequest();
	ActiveCoachNarrationRequest->SetURL(RequestUrl);
	ActiveCoachNarrationRequest->SetVerb(TEXT("POST"));
	ActiveCoachNarrationRequest->SetHeader(TEXT("Accept"), TEXT("audio/wav"));
	ActiveCoachNarrationRequest->SetHeader(
		TEXT("Content-Type"),
		TEXT("application/json"));
	if (!Credential.IsEmpty())
	{
		ActiveCoachNarrationRequest->SetHeader(
			TEXT("Authorization"),
			FString::Printf(TEXT("Bearer %s"), *Credential));
	}
	ActiveCoachNarrationRequest->SetContentAsString(RequestBody);
	ActiveCoachNarrationRequest->SetTimeout(
		PlayerPreferences.CoachRequestTimeoutSeconds);

	TWeakObjectPtr<AHyperTwistPlayerControllerBase> WeakThis(this);
	ActiveCoachNarrationRequest->OnProcessRequestComplete().BindLambda(
		[WeakThis](
			FHttpRequestPtr CompletedRequest,
			FHttpResponsePtr Response,
			const bool bSucceeded)
		{
			AHyperTwistPlayerControllerBase* Controller = WeakThis.Get();
			if (Controller == nullptr
				|| Controller->ActiveCoachNarrationRequest.Get()
					!= CompletedRequest.Get())
			{
				return;
			}
			Controller->HandleCoachNarrationResponse(
				bSucceeded && Response.IsValid(),
				Response.IsValid() ? Response->GetResponseCode() : 0,
				Response.IsValid() ? Response->GetContent() : TArray<uint8>());
		});

	ReportCoachNarrationStatus(TEXT("Preparing spoken response..."), false);
	const bool bStarted = ActiveCoachNarrationRequest->ProcessRequest();
	if (!bStarted)
	{
		ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
		ActiveCoachNarrationRequest.Reset();
		ReportCoachNarrationStatus(
			TEXT("Text response ready; narration service could not start."),
			true);
	}
	return bStarted;
}

void AHyperTwistPlayerControllerBase::StopCoachNarration()
{
	if (ActiveCoachNarrationRequest.IsValid())
	{
		ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
		ActiveCoachNarrationRequest->CancelRequest();
		ActiveCoachNarrationRequest.Reset();
	}
	ReleaseCoachNarrationAudio(true);
}

void AHyperTwistPlayerControllerBase::HandleCoachNarrationResponse(
	const bool bTransportSucceeded,
	const int32 StatusCode,
	const TArray<uint8>& AudioBytes
)
{
	if (ActiveCoachNarrationRequest.IsValid())
	{
		ActiveCoachNarrationRequest->OnProcessRequestComplete().Unbind();
		ActiveCoachNarrationRequest.Reset();
	}
	if (!bTransportSucceeded || StatusCode < 200 || StatusCode >= 300)
	{
		ReportCoachNarrationStatus(
			StatusCode > 0
				? FString::Printf(
					TEXT("Text response ready; narration service returned HTTP %d."),
					StatusCode)
				: TEXT("Text response ready; narration service is unavailable."),
			true);
		return;
	}
	if (!TryPlayCoachNarration(AudioBytes))
	{
		ReportCoachNarrationStatus(
			TEXT("Text response ready; narration returned unsupported audio."),
			true);
		return;
	}
	ReportCoachNarrationStatus(TEXT("Playing spoken response..."), false);
}

bool AHyperTwistPlayerControllerBase::TryPlayCoachNarration(
	const TArray<uint8>& AudioBytes
)
{
	int32 SampleRateHz = 0;
	int32 ChannelCount = 0;
	if (GetWorld() == nullptr
		|| !HyperTwistPlayerControllerBaseInternal::TryReadPcm16WaveFormat(
			AudioBytes,
			SampleRateHz,
			ChannelCount))
	{
		return false;
	}

	const TArray<uint8> PcmBytes =
		UHyperTwistSpeechLibrary::ExtractPcm16PayloadFromAudioBytes(
			AudioBytes,
			TEXT("wav-pcm16-mono"));
	if (PcmBytes.IsEmpty())
	{
		return false;
	}

	ReleaseCoachNarrationAudio(true);
	ActiveCoachNarrationSoundWave =
		NewObject<USoundWaveProcedural>(this);
	if (ActiveCoachNarrationSoundWave == nullptr)
	{
		return false;
	}
	ActiveCoachNarrationSoundWave->SetSampleRate(SampleRateHz);
	ActiveCoachNarrationSoundWave->NumChannels = ChannelCount;
	ActiveCoachNarrationSoundWave->SampleByteSize = 2;
	ActiveCoachNarrationSoundWave->Duration =
		static_cast<float>(PcmBytes.Num())
		/ static_cast<float>(SampleRateHz * ChannelCount * 2);
	ActiveCoachNarrationSoundWave->QueueAudio(
		PcmBytes.GetData(),
		PcmBytes.Num());

	ActiveCoachNarrationAudioComponent = UGameplayStatics::SpawnSound2D(
		GetWorld(),
		ActiveCoachNarrationSoundWave);
	if (ActiveCoachNarrationAudioComponent == nullptr)
	{
		ActiveCoachNarrationSoundWave = nullptr;
		return false;
	}
	UHyperTwistSpeechLibrary::RegisterCategorizedAudioComponent(
		ActiveCoachNarrationAudioComponent,
		EHyperTwistManagedAudioCategory::Voice);
	ActiveCoachNarrationAudioComponent->OnAudioFinished.AddDynamic(
		this,
		&AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished);
	return true;
}

void AHyperTwistPlayerControllerBase::ReleaseCoachNarrationAudio(
	const bool bStopPlayback
)
{
	if (ActiveCoachNarrationAudioComponent != nullptr)
	{
		ActiveCoachNarrationAudioComponent->OnAudioFinished.RemoveDynamic(
			this,
			&AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished);
		UHyperTwistSpeechLibrary::UnregisterManagedAudioComponent(
			ActiveCoachNarrationAudioComponent);
		if (bStopPlayback)
		{
			ActiveCoachNarrationAudioComponent->Stop();
		}
		ActiveCoachNarrationAudioComponent = nullptr;
	}
	ActiveCoachNarrationSoundWave = nullptr;
}

void AHyperTwistPlayerControllerBase::ReportCoachNarrationStatus(
	const FString& Status,
	const bool bError
)
{
	if (ActiveAssistantPanelWidget != nullptr)
	{
		ActiveAssistantPanelWidget->SetNarrationStatus(Status, bError);
	}
}

void AHyperTwistPlayerControllerBase::HandleCoachNarrationFinished()
{
	ReleaseCoachNarrationAudio(false);
	ReportCoachNarrationStatus(TEXT("Spoken response complete."), false);
}

const FHyperTwistPlayerPreferences&
AHyperTwistPlayerControllerBase::GetPlayerPreferences() const
{
	return PlayerPreferences;
}

void AHyperTwistPlayerControllerBase::ReloadAndApplyPlayerPreferences(
	const bool bApplyGraphicsSettings
)
{
	PlayerPreferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
	UHyperTwistPlayerSettingsLibrary::ApplyRuntimePreferences(
		PlayerPreferences,
		bApplyGraphicsSettings);
}

FString AHyperTwistPlayerControllerBase::GetPauseMenuTitle() const
{
	return TEXT("HyperTwist");
}

FString AHyperTwistPlayerControllerBase::GetPauseMenuSubtitle() const
{
	const UWorld* World = GetWorld();
	return World != nullptr
		? FString::Printf(TEXT("%s is paused"), *World->GetMapName())
		: TEXT("Puzzle paused");
}

void AHyperTwistPlayerControllerBase::PrepareForModalUi()
{
	CancelGlobalDictation();
}

void AHyperTwistPlayerControllerBase::ApplyGameAndUiInputMode()
{
	bShowMouseCursor = true;
	bEnableClickEvents = true;
	bEnableMouseOverEvents = true;
	FInputModeGameAndUI InputMode;
	InputMode.SetHideCursorDuringCapture(false);
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);
}

void AHyperTwistPlayerControllerBase::HandlePauseShortcut()
{
	if (!bEnableGlobalPauseMenu)
	{
		return;
	}
	if (ActivePauseMenuWidget != nullptr)
	{
		HidePauseMenu();
	}
	else
	{
		ShowPauseMenu();
	}
}

void AHyperTwistPlayerControllerBase::HandleMainMenuShortcut()
{
	ReturnToMainMenu();
}

void AHyperTwistPlayerControllerBase::HandleAssistantShortcut()
{
	ToggleAssistantPanel();
}

void AHyperTwistPlayerControllerBase::HandleAssistantCloseRequested()
{
	HideAssistantPanel();
}

void AHyperTwistPlayerControllerBase::HandleGlobalDictationPressed()
{
	if (PlayerPreferences.SpeechActivationMode == TEXT("toggle"))
	{
		ToggleGlobalDictation();
		bToggleDictationCaptureActive = IsGlobalDictationActive();
		return;
	}
	BeginGlobalDictation();
}

void AHyperTwistPlayerControllerBase::HandleGlobalDictationReleased()
{
	if (PlayerPreferences.SpeechActivationMode != TEXT("toggle"))
	{
		EndGlobalDictation();
	}
}

void AHyperTwistPlayerControllerBase::HandleGlobalDictationStateChanged(
	const FString& Status,
	const bool bError
)
{
	if (ActiveAssistantPanelWidget != nullptr)
	{
		ActiveAssistantPanelWidget->SetDictationStatus(
			Status,
			IsGlobalDictationActive(),
			bError);
	}
}

void AHyperTwistPlayerControllerBase::HandleGlobalDictationTranscriptReady(
	const FString& Transcript
)
{
	if (Transcript.IsEmpty())
	{
		return;
	}
	if (ActiveAssistantPanelWidget == nullptr && !ShowAssistantPanel())
	{
		return;
	}

	ReloadAndApplyPlayerPreferences(false);
	ActiveAssistantPanelWidget->SubmitDictatedPrompt(
		Transcript,
		PlayerPreferences.SpeechDictationDestination == TEXT("coach-send"));
}

void AHyperTwistPlayerControllerBase::HandlePauseResumeRequested()
{
	HidePauseMenu();
}

void AHyperTwistPlayerControllerBase::HandlePauseMainMenuRequested()
{
	ReturnToMainMenu();
}

void AHyperTwistPlayerControllerBase::HandlePauseQuitRequested()
{
	QuitToDesktop();
}
