// Georgy Treshchev 2026 All Rights Reserved.

#include "RuntimeVoskModelSettingsDetails.h"

#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "DetailWidgetRow.h"
#include "IDetailGroup.h"
#include "RuntimeVoskModelLibrary.h"
#include "RuntimeSpeechRecognizerVoskEditor.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SBox.h"
#include "Misc/EngineVersionComparison.h"
#include "Misc/MessageDialog.h"

#define LOCTEXT_NAMESPACE "RuntimeSpeechRecognizerVoskSettings"

TSharedRef<IDetailCustomization> FRuntimeSpeechRecognizerVoskSettingsDetails::MakeInstance()
{
	TSharedRef<FRuntimeSpeechRecognizerVoskSettingsDetails> DetailsInstance = MakeShareable(new FRuntimeSpeechRecognizerVoskSettingsDetails());
	TWeakPtr<FRuntimeSpeechRecognizerVoskSettingsDetails> WeakDetailsInstancePtr = DetailsInstance;

	FRuntimeSpeechRecognizerVoskEditorModule& EditorModule = FModuleManager::LoadModuleChecked<FRuntimeSpeechRecognizerVoskEditorModule>("RuntimeSpeechRecognizerVoskEditor");

	auto RefreshOnGameThread = [WeakDetailsInstancePtr]()
	{
		if (WeakDetailsInstancePtr.IsValid())
		{
			WeakDetailsInstancePtr.Pin()->ForceRefreshDetails();
		}
	};

#if !UE_VERSION_OLDER_THAN(5, 3, 0)
	EditorModule.OnModelDownloadProgress.AddSPLambda(DetailsInstance.ToSharedPtr().Get(), [WeakDetailsInstancePtr](const FString& ModelId, float Progress)
	{
		if (WeakDetailsInstancePtr.IsValid()) WeakDetailsInstancePtr.Pin()->SetProgress(ModelId, Progress);
	});
	EditorModule.OnModelDownloadComplete.AddSPLambda(DetailsInstance.ToSharedPtr().Get(), [RefreshOnGameThread](const FString&) { RefreshOnGameThread(); });
	EditorModule.OnModelDownloadCanceled.AddSPLambda(DetailsInstance.ToSharedPtr().Get(), [RefreshOnGameThread](const FString&) { RefreshOnGameThread(); });
	EditorModule.OnModelOperationFailed.AddSPLambda(DetailsInstance.ToSharedPtr().Get(), [WeakDetailsInstancePtr](const FString&, const FText& ErrorMessage)
	{
		if (WeakDetailsInstancePtr.IsValid())
		{
			FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage);
			WeakDetailsInstancePtr.Pin()->ForceRefreshDetails();
		}
	});
#else
	EditorModule.OnModelDownloadProgress.AddLambda([WeakDetailsInstancePtr](const FString& ModelId, float Progress)
	{
		if (WeakDetailsInstancePtr.IsValid()) WeakDetailsInstancePtr.Pin()->SetProgress(ModelId, Progress);
	});
	EditorModule.OnModelDownloadComplete.AddLambda([RefreshOnGameThread](const FString&) { RefreshOnGameThread(); });
	EditorModule.OnModelDownloadCanceled.AddLambda([RefreshOnGameThread](const FString&) { RefreshOnGameThread(); });
	EditorModule.OnModelOperationFailed.AddLambda([WeakDetailsInstancePtr](const FString&, const FText& ErrorMessage)
	{
		if (WeakDetailsInstancePtr.IsValid())
		{
			FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage);
			WeakDetailsInstancePtr.Pin()->ForceRefreshDetails();
		}
	});
#endif

	return DetailsInstance;
}

void FRuntimeSpeechRecognizerVoskSettingsDetails::ForceRefreshDetails()
{
	if (CachedDetailBuilder.IsValid())
	{
		IDetailLayoutBuilder* OldLayout = CachedDetailBuilder.Pin().Get();
		CachedDetailBuilder = nullptr;
		OldLayout->ForceRefreshDetails();
	}
}

void FRuntimeSpeechRecognizerVoskSettingsDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
	IDetailCategoryBuilder& Category = DetailLayout.EditCategory(TEXT("Vosk Speech Models"), LOCTEXT("VoskModelsCategory", "Vosk Speech Models"));
	Category.RestoreExpansionState(true);

	for (const FVoskModelLanguageGroup& LanguageGroup : URuntimeVoskModelLibrary::GetPredefinedVoskModels())
	{
		IDetailGroup& Group = Category.AddGroup(FName(*LanguageGroup.GroupDisplayName), FText::FromString(LanguageGroup.GroupDisplayName), false, true);
		for (const FVoskModelVariant& Variant : LanguageGroup.Variants)
		{
			AddModelRow(Group, Variant);
		}
	}
}

void FRuntimeSpeechRecognizerVoskSettingsDetails::CustomizeDetails(const TSharedPtr<IDetailLayoutBuilder>& DetailBuilder)
{
	CachedDetailBuilder = DetailBuilder;
	CustomizeDetails(*DetailBuilder);
}

void FRuntimeSpeechRecognizerVoskSettingsDetails::AddModelRow(IDetailGroup& Group, const FVoskModelVariant& Variant)
{
	const bool bIsOnDisk = URuntimeVoskModelLibrary::IsVoskModelOnDisk(Variant.ModelId);

	FRuntimeSpeechRecognizerVoskEditorModule& EditorModule = FModuleManager::LoadModuleChecked<FRuntimeSpeechRecognizerVoskEditorModule>("RuntimeSpeechRecognizerVoskEditor");
	const bool bIsDownloading = EditorModule.IsDownloadActive(Variant.ModelId);

	const FString SizeString = Variant.ApproximateSizeBytes >= 1024LL * 1024 * 1024
		? FString::Printf(TEXT("~%.2f GB"), static_cast<float>(Variant.ApproximateSizeBytes) / (1024.0f * 1024.0f * 1024.0f))
		: FString::Printf(TEXT("~%.0f MB"), static_cast<float>(Variant.ApproximateSizeBytes) / (1024.0f * 1024.0f));

	const FString DisplayName = FString::Printf(TEXT("%s (%s)"), *Variant.ModelId, *SizeString);
	const FString ModelId = Variant.ModelId;

	TSharedRef<SProgressBar> ProgressBar = SNew(SProgressBar)
		.Percent(0.0f)
#if UE_VERSION_NEWER_THAN(5, 0, 0)
		.Style(FAppStyle::Get(), "ProgressBar")
#endif
		.Visibility(bIsDownloading ? EVisibility::Visible : EVisibility::Collapsed);

	TSharedRef<SButton> DownloadButton = SNew(SButton)
		.HAlign(HAlign_Center)
		.Text(LOCTEXT("DownloadVoskModel", "Download"))
		.Visibility(!bIsOnDisk && !bIsDownloading ? EVisibility::Visible : EVisibility::Collapsed)
		.OnClicked_Lambda([Variant]() -> FReply
		{
			FModuleManager::LoadModuleChecked<FRuntimeSpeechRecognizerVoskEditorModule>("RuntimeSpeechRecognizerVoskEditor").DownloadModel(Variant);
			return FReply::Handled();
		});

	TSharedRef<SButton> CancelButton = SNew(SButton)
		.HAlign(HAlign_Center)
		.Text(LOCTEXT("CancelVoskModelDownload", "Cancel"))
		.Visibility(bIsDownloading ? EVisibility::Visible : EVisibility::Collapsed)
		.OnClicked_Lambda([ModelId]() -> FReply
		{
			FModuleManager::LoadModuleChecked<FRuntimeSpeechRecognizerVoskEditorModule>("RuntimeSpeechRecognizerVoskEditor").CancelDownloadModel(ModelId);
			return FReply::Handled();
		});

	TSharedRef<SButton> DeleteButton = SNew(SButton)
		.HAlign(HAlign_Center)
		.Text(LOCTEXT("DeleteVoskModel", "Delete"))
		.Visibility(bIsOnDisk && !bIsDownloading ? EVisibility::Visible : EVisibility::Collapsed)
		.OnClicked_Lambda([this, ModelId]() -> FReply
		{
			FModuleManager::LoadModuleChecked<FRuntimeSpeechRecognizerVoskEditorModule>("RuntimeSpeechRecognizerVoskEditor").DeleteModel(ModelId);
			ForceRefreshDetails();
			return FReply::Handled();
		});

	FVoskModelWidgets Widgets;
	Widgets.ProgressBar = ProgressBar;
	Widgets.DownloadButton = DownloadButton;
	Widgets.CancelButton = CancelButton;
	Widgets.DeleteButton = DeleteButton;
	ModelIdToWidgetsMap.Add(Variant.ModelId, Widgets);

	Group.AddWidgetRow()
	.NameContent()
	[
		SNew(SBox)
		.ToolTipText(FText::FromString(FString::Printf(TEXT("%s\nLicense: %s"), *Variant.Info, *Variant.License)))
		[
			SNew(STextBlock)
#if !UE_VERSION_OLDER_THAN(5, 1, 0)
			.Font(FAppStyle::GetFontStyle("PropertyWindow.NormalFont"))
#else
			.Font(FEditorStyle::GetFontStyle("PropertyWindow.NormalFont"))
#endif
			.Text(FText::FromString(DisplayName))
		]
	]
	.ValueContent()
	.HAlign(HAlign_Fill)
	[
		SNew(SHorizontalBox)
		+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0) [ DownloadButton ]
		+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0) [ CancelButton ]
		+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0) [ DeleteButton ]
		+ SHorizontalBox::Slot().MaxWidth(160.0f).HAlign(HAlign_Fill).VAlign(VAlign_Center).Padding(8, 0, 0, 0) [ ProgressBar ]
	];
}

void FRuntimeSpeechRecognizerVoskSettingsDetails::SetProgress(const FString& ModelId, float Progress)
{
	if (FVoskModelWidgets* Widgets = ModelIdToWidgetsMap.Find(ModelId))
	{
		if (Widgets->ProgressBar.IsValid())
		{
			Widgets->ProgressBar->SetPercent(Progress);
			Widgets->ProgressBar->SetVisibility(EVisibility::Visible);
		}
		if (Widgets->DownloadButton.IsValid()) Widgets->DownloadButton->SetVisibility(EVisibility::Collapsed);
		if (Widgets->CancelButton.IsValid()) Widgets->CancelButton->SetVisibility(EVisibility::Visible);
	}
}

#undef LOCTEXT_NAMESPACE