// Copyright (c) 2024 Codemongous LLC. All Rights Reserved.

#include "SSourcifyResults.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetToolsModule.h"
#include "Containers/Ticker.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "Engine/Blueprint.h"
#include "FileHelpers.h"
#include "Framework/Notifications/NotificationManager.h"
#include "HAL/PlatformApplicationMisc.h"
#include "ILiveCodingModule.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Misc/MessageDialog.h"
#include "ObjectTools.h"
#include "PackageTools.h"
#include "PropertyCustomizationHelpers.h"
#include "Serialization/FindReferencersArchive.h"
#include "SourceCodeNavigation.h"
#include "Sourcify.h"
#include "SourcifyCore.h"
#include "SourcifyCoreUEWrapperModule.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UI/SSourcifyExpanderArrow.h"
#include "UI/SSourcifyImageTexture.h"
#include "UnrealEdMisc.h"
#include "UObject/PackageReload.h"
#include "UObject/UObjectIterator.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/SListView.h"
#include "Widgets/Views/STableRow.h"

#define LOCTEXT_NAMESPACE "SourcifyResultsWindow"

using namespace Sourcify;

namespace SSourcifyResultsInternal
{
	static constexpr int32 AssetThumbnailSize = 32;
	static constexpr float SubRowPaddingLeft = 20.f;
	static constexpr float SubRowPaddingTop = 0.f;
	static constexpr float SubRowPaddingRight = 5.f;
	static constexpr float SubRowPaddingBottom = 0.f;

	static constexpr const TCHAR* TitleUnknown = TEXT("No Sourcification Results to Display");
	static constexpr const TCHAR* TitleSucceeded = TEXT("Sourcification Succeeded");
	static constexpr const TCHAR* TitleSucceededNoAction = TEXT("Sourcification Succeeded with No Action Required");
	static constexpr const TCHAR* TitleCompleted = TEXT("Sourcification Completed");
	static constexpr const TCHAR* TitleFailed = TEXT("Sourcification Failed");
	static constexpr const TCHAR* TitleWarning = TEXT("Sourcification Warning");

	static constexpr const TCHAR* CompilationErrorsSectionText = TEXT("The following Blueprints failed to compile and require attention:");
	static constexpr const TCHAR* NonSourcifiedAssetIniReferencesSectionText = TEXT("These asset references must be removed in the following INI files and replaced with their new, Sourcified types:");
	static constexpr const TCHAR* SourcifiedBPSectionText = TEXT("The following Blueprints were successfully Sourcified and reparented to new C++ classes:");
	static constexpr const TCHAR* PackagesContainingSourcifiedReferencesSectionText = TEXT("The following assets contain references that were updated to newly Sourcified types:");
	static constexpr const TCHAR* PackagesThatShouldBeDeletedSectionText = TEXT("The following assets were fully sourcified and should be deleted:");
	static constexpr const TCHAR* ErrorMessageSectionText = TEXT("The following errors occurred during Sourcification:");
	static constexpr const TCHAR* WarningMessageSectionText = TEXT("The following warnings occurred during Sourcification:");
	static const FString FunctionRenameSectionText = TEXT("Function Renames (name conflict resolution)");

	FMargin GetRowThumbnailPadding()
	{
		return FMargin(SubRowPaddingLeft, SubRowPaddingTop, SubRowPaddingRight, SubRowPaddingBottom);
	}

	void MarkReinstrancedClassesAsGarbage(TArray<UObject*> ObjectsToDelete)
	{
		FFindReferencersArchive FindRefsArchive(nullptr, ObjectsToDelete);

		auto MarkClassesAsGarbage = [&FindRefsArchive](UObject* CurObject)
			{
				if (!IsValidChecked(CurObject) || CurObject->IsUnreachable())
				{
					return;
				}

				if (UClass* CurClass = Cast<UClass>(CurObject))
				{
					if (CurClass->HasAnyClassFlags(EClassFlags::CLASS_NewerVersionExists))
					{
						FindRefsArchive.ResetPotentialReferencer(CurObject);

						TMap<UObject*, int32> CurNumReferencesMap;
						if (FindRefsArchive.GetReferenceCounts(CurNumReferencesMap) > 0)
						{
							CurClass->MarkAsGarbage();
						}
					}
				}
			};

		for (FThreadSafeObjectIterator ObjIter; ObjIter; ++ObjIter)
		{
			MarkClassesAsGarbage(*ObjIter);
		}
	}
}

void SSourcifyResults::Construct(const FArguments& InArgs)
{
	check(InArgs._SourcifyResults);
	if (!InArgs._SourcifyResults)
	{
		return;
	}

	auto IsPackageARedirector = [](TObjectPtr<UPackage> PackagePtr) -> bool
		{
			TArray<UObject*> Objects;
			ForEachObjectWithPackage(PackagePtr, [&Objects](UObject* Object)
				{
					if (Object->IsAsset())
					{
						Objects.Emplace(Object);
					}
					return true;
				}, false);

			check(!Objects.IsEmpty());
			UObject* Asset = Objects[0];

			return Asset->IsA<UObjectRedirector>();
		};

	ParentTab = InArgs._OwnerTab;
	SourcifyCore = InArgs._SourcifyCore;

	Results::EResultType SourcificationResult = InArgs._SourcifyResults->Type;
	FString ResultsTitle;
	FSlateColor ResultsTitleColor;

	// Load Sourcify Logo
	TSoftObjectPtr<UTexture2D> SourcifyLogo(FSoftObjectPath(TEXT("/Sourcify/Sourcify/UI/Sourcify-Logo-forDarkBG_64.Sourcify-Logo-forDarkBG_64")));
	UTexture2D* SourcifyLogoTexture = SourcifyLogo.LoadSynchronous();

	// Build local UE data structures from input Sourcify results struct
	if (SourcificationResult == Results::EResultType::Success && InArgs._SourcifyResults->NonSourcifiedAssetIniReferences.size() > 0)
	{
		// These are the ini references that are still using the non-sourcified asset and require manual fix-up
		for (const auto& [AssetPath, IniInfo] : InArgs._SourcifyResults->NonSourcifiedAssetIniReferences)
		{
			TSharedPtr<ResultsWindowIniReference> IniReference(new ResultsWindowIniReference);
			FString AssetPathStr = AssetPath.c_str();
			TArray<FIniReference> IniReferences;
			for (const auto& [Ini, Section, Key] : IniInfo)
			{
				IniReferences.Emplace(Ini.c_str(), Section.c_str(), Key.c_str());
			}

			IniReference->Key = AssetPathStr;
			IniReference->Value = IniReferences;
			NonSourcifiedAssetIniReferences.Emplace(IniReference);
		}
	}
	
	// Lambda used by slate to generate a row for the ini references
	auto GenerateRowFromResultsWindowIniReference = [this](TSharedPtr<ResultsWindowIniReference> Item, const TSharedRef<STableViewBase>& OwnerTable) -> TSharedRef<ITableRow>
		{
			// I feel shame
			if (!OutputLog.Contains(SSourcifyResultsInternal::NonSourcifiedAssetIniReferencesSectionText))
			{
				OutputLog += TEXT("\n");
				OutputLog += SSourcifyResultsInternal::NonSourcifiedAssetIniReferencesSectionText + FString(TEXT("\n"));
			}
			
			// Group the ini references by ini file
			TMap<FString, TArray<TPair<FString, FString>>> IniReferences;
			for (const auto& IniInfoRef : Item->Value)
			{
				IniReferences.FindOrAdd(IniInfoRef.Ini).Emplace(IniInfoRef.Section, IniInfoRef.Key);
			}

			// Build the string to display in the row
			FString RowString;
			for (const auto& IniReference : IniReferences)
			{
				RowString.Appendf(TEXT("\t%s.ini:\n"), *IniReference.Key);
				for (const auto& [Section, Key] : IniReference.Value)
				{
					RowString.Appendf(TEXT("\t\t[%s]\n\t\t\t%s\n"), *Section, *Key);
				}
			}
			// Get rid of trailing newline
			RowString.LeftChopInline(1);
			
			OutputLog.Appendf(TEXT("%s\n%s\n"), *Item->Key, *RowString);
			
			FSoftObjectPath SoftObjectPath(Item->Key);
			UObject* Asset = SoftObjectPath.TryLoad();
			check(Asset);
			
			FString DisplayName = Asset->GetName();

			FLinearColor AssetTypeColor;
			FAssetThumbnailConfig ThumbnailConfig;
			TSharedPtr<FAssetThumbnail> AssetThumbnail = MakeAssetThumbnail(AssetTypeColor, ThumbnailConfig, Asset);

			return SNew(STableRow<TSharedPtr<ResultsWindowIniReference>>, OwnerTable)
						.Visibility(EVisibility::SelfHitTestInvisible)
				[
					SNew(SBorder)
						.BorderImage(FAppStyle::GetBrush("NoBorder"))
						.Visibility(EVisibility::SelfHitTestInvisible)
						[
							SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.VAlign(VAlign_Top)
								.HAlign(HAlign_Left)
								.AutoWidth()
								.Padding(SSourcifyResultsInternal::GetRowThumbnailPadding())
								[
									SNew(SBorder)
										.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
										.Padding(2)
										.BorderBackgroundColor(AssetTypeColor)
										.OnMouseDoubleClick_Lambda([this, Asset](const FGeometry& InGeometry, const FPointerEvent& InEvent)->FReply
											{
												GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Asset);
												return FReply::Handled();
											})
										[
											SNew(SBox)
												.WidthOverride(AssetThumbnail->GetSize().X)
												.HeightOverride(AssetThumbnail->GetSize().Y)
												[
													AssetThumbnail->MakeThumbnailWidget(ThumbnailConfig)
												]
										]
								]
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Fill)
								.AutoWidth()
								[
									SNew(SVerticalBox)
										+ SVerticalBox::Slot()
										.HAlign(HAlign_Left)
										.AutoHeight()
										[
											SNew(STextBlock)
												.Text(FText::FromString(DisplayName))
												.ColorAndOpacity(AssetTypeColor)
										]
 										+ SVerticalBox::Slot()
 										.HAlign(HAlign_Left)
 										.AutoHeight()
										.Padding(FMargin(0, 0, 0, 5))
 										[
											SNew(STextBlock)
												.Text(FText::FromString(RowString))
										]
								]
						]
				];
		};

	// These are the assets that were Sourcified, thus reparented to C++ classes
	if (SourcificationResult == Results::EResultType::Success && InArgs._SourcifyResults->SourcifiedBPs.size() > 0)
	{
		OutputLog += TEXT("\n");
		OutputLog += SSourcifyResultsInternal::SourcifiedBPSectionText + FString(TEXT("\n"));

		for (const auto& BP : InArgs._SourcifyResults->SourcifiedBPs)
		{
			TSharedPtr<FPackageReference> BPRef(new FPackageReference);
			BPRef->PackagePtr = FSourcifyCoreUEWrapper::Unwrap(BP.PackagePtr);
			if (IsPackageARedirector(BPRef->PackagePtr))
			{
				continue;
			}
			if (BP.SourcifiedClassName)
			{
				BPRef->SourcifiedClassName = BP.SourcifiedClassName->c_str();
			}
			SourcifiedBPs.Emplace(BPRef);

			OutputLog += BPRef->PackagePtr->GetName() + TEXT("\n");
		}
	}
	
	// These are the assets that contain references that were updated to newly Sourcified types
	if (SourcificationResult == Results::EResultType::Success && InArgs._SourcifyResults->PackagesContainingSourcifiedReferences.size() > 0)
	{
		OutputLog += TEXT("\n");
		OutputLog += SSourcifyResultsInternal::PackagesContainingSourcifiedReferencesSectionText + FString(TEXT("\n"));

		for (const auto& Package : InArgs._SourcifyResults->PackagesContainingSourcifiedReferences)
		{
			TSharedPtr<FPackageReference> BPRef(new FPackageReference);
			BPRef->PackagePtr = FSourcifyCoreUEWrapper::Unwrap(Package.PackagePtr);
			if (IsPackageARedirector(BPRef->PackagePtr))
			{
				continue;
			}
			if (Package.SourcifiedClassName)
			{
				BPRef->SourcifiedClassName = Package.SourcifiedClassName->c_str();
			}
			PackagesContainingSourcifiedReferences.Emplace(BPRef);

			OutputLog += BPRef->PackagePtr->GetName() + TEXT("\n");
		}
	}
	
	// These are the assets that were fully sourcified and should be deleted (enums and structs)
	if (SourcificationResult == Results::EResultType::Success && InArgs._SourcifyResults->PackagesThatShouldBeDeleted.size() > 0)
	{
		OutputLog += TEXT("\n");
		OutputLog += SSourcifyResultsInternal::PackagesThatShouldBeDeletedSectionText + FString(TEXT("\n"));

		for (const auto& Package : InArgs._SourcifyResults->PackagesThatShouldBeDeleted)
		{
			TSharedPtr<FPackageReference> BPRef(new FPackageReference);
			BPRef->PackagePtr = FSourcifyCoreUEWrapper::Unwrap(Package.PackagePtr);
			if (IsPackageARedirector(BPRef->PackagePtr))
			{
				continue;
			}
			if (Package.SourcifiedClassName)
			{
				BPRef->SourcifiedClassName = Package.SourcifiedClassName->c_str();
			}
			PackagesThatShouldBeDeleted.Emplace(BPRef);

			OutputLog += BPRef->PackagePtr->GetName() + TEXT("\n");
		}
	}

	// Group FunctionNameConflictRenames by ClassName
	{
		TMap<FString, int32> ClassNameToGroupIndex;
		for (const auto& Rename : InArgs._SourcifyResults->FunctionNameConflictRenames)
		{
			FString ClassName = FString(Rename.ClassName.c_str());
			int32* IndexPtr = ClassNameToGroupIndex.Find(ClassName);
			if (!IndexPtr)
			{
				int32 NewIndex = RenameClassGroups.Num();
				RenameClassGroups.Add({ ClassName, {} });
				ClassNameToGroupIndex.Add(ClassName, NewIndex);
				IndexPtr = &ClassNameToGroupIndex[ClassName];
			}

			TSharedPtr<FRenameReference> Ref = MakeShared<FRenameReference>();
			Ref->OriginalName = FString(Rename.OriginalName.c_str());
			Ref->NewName = FString(Rename.NewName.c_str());
			Ref->ConflictingParentClassName = FString(Rename.ConflictingParentClassName.c_str());
			Ref->ConflictingParentHeaderPath = FString(Rename.ConflictingParentHeaderPath.c_str());
			Ref->ConflictingParentLineNumber = Rename.ConflictingParentLineNumber;
			RenameClassGroups[*IndexPtr].Renames.Add(Ref);
		}
	}

	if (RenameClassGroups.Num() > 0)
	{
		int32 TotalRenames = 0;
		for (const auto& Group : RenameClassGroups)
		{
			TotalRenames += Group.Renames.Num();
		}
		OutputLog += FString::Printf(TEXT("\nFunction Renames (%d %s across %d %s)\n"),
			TotalRenames, TotalRenames == 1 ? TEXT("rename") : TEXT("renames"),
			RenameClassGroups.Num(), RenameClassGroups.Num() == 1 ? TEXT("class") : TEXT("classes"));
		for (const auto& Group : RenameClassGroups)
		{
			OutputLog += FString::Printf(TEXT("  %s (%d %s):\n"), *Group.ClassName,
				Group.Renames.Num(), Group.Renames.Num() == 1 ? TEXT("rename") : TEXT("renames"));
			for (const auto& Rename : Group.Renames)
			{
				OutputLog += FString::Printf(TEXT("    %s -> %s"), *Rename->OriginalName, *Rename->NewName);
				if (!Rename->ConflictingParentClassName.IsEmpty())
				{
					OutputLog += FString::Printf(TEXT("  (conflicts with %s::%s"),
						*Rename->ConflictingParentClassName, *Rename->OriginalName);
					if (Rename->ConflictingParentLineNumber > 0 && !Rename->ConflictingParentHeaderPath.IsEmpty())
					{
						OutputLog += FString::Printf(TEXT(", %s:%d"), *Rename->ConflictingParentHeaderPath, Rename->ConflictingParentLineNumber);
					}
					OutputLog += TEXT(")");
				}
				OutputLog += TEXT("\n");
			}
		}
	}

	// Helper lambda to check if a package contains a Blueprint with compilation errors
	auto CheckBlueprintCompilationStatus = [this](TObjectPtr<UPackage> PackagePtr) -> bool
		{
			TArray<UBlueprint*> Blueprints;
			ForEachObjectWithPackage(PackagePtr, [&Blueprints](UObject* Object)
				{
					if (UBlueprint* Blueprint = Cast<UBlueprint>(Object))
					{
						Blueprints.Emplace(Blueprint);
					}
					return true;
				}, false);

			for (UBlueprint* Blueprint : Blueprints)
			{
				if (!Blueprint)
				{
					continue;
				}

				FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipSave);
				if (Blueprint->Status == EBlueprintStatus::BS_Error)
				{
					TSharedPtr<FCompilationErrorReference> ErrorRef(new FCompilationErrorReference);
					ErrorRef->PackagePtr = PackagePtr;
					ErrorRef->BlueprintName = Blueprint->GetName();
					BlueprintsWithCompilationErrors.Emplace(ErrorRef);
					return true;
				}
			}

			return false;
		};

	// Check all Sourcified BPs and referencers for compilation errors
	if (SourcificationResult == Results::EResultType::Success)
	{
		for (const TSharedPtr<FPackageReference>& BPRef : SourcifiedBPs)
		{
			CheckBlueprintCompilationStatus(BPRef->PackagePtr);
		}

		for (const TSharedPtr<FPackageReference>& BPRef : PackagesContainingSourcifiedReferences)
		{
			CheckBlueprintCompilationStatus(BPRef->PackagePtr);
		}

		// Collect all touched packages for potential undo operation
		for (const TSharedPtr<FPackageReference>& BPRef : SourcifiedBPs)
		{
			AllTouchedPackages.AddUnique(BPRef->PackagePtr);
		}
		for (const TSharedPtr<FPackageReference>& BPRef : PackagesContainingSourcifiedReferences)
		{
			AllTouchedPackages.AddUnique(BPRef->PackagePtr);
		}
		for (const TSharedPtr<FPackageReference>& BPRef : PackagesThatShouldBeDeleted)
		{
			AllTouchedPackages.AddUnique(BPRef->PackagePtr);
		}
	}

	// Determine the results title based on compilation status
	if (SourcificationResult == Results::EResultType::Unknown)
	{
		ResultsTitle = SSourcifyResultsInternal::TitleUnknown;
		ResultsTitleColor = FSlateColor(FLinearColor::Gray);
	}
	else if (SourcificationResult == Results::EResultType::Success)
	{
		if (!BlueprintsWithCompilationErrors.IsEmpty())
		{
			// Sourcification completed but some BPs have compilation errors
			ResultsTitle = SSourcifyResultsInternal::TitleCompleted;
			ResultsTitleColor = FSlateColor(FLinearColor::Yellow);

			OutputLog += TEXT("\n");
			OutputLog += SSourcifyResultsInternal::CompilationErrorsSectionText + FString(TEXT("\n"));
			for (const TSharedPtr<FCompilationErrorReference>& ErrorRef : BlueprintsWithCompilationErrors)
			{
				OutputLog += ErrorRef->BlueprintName + TEXT("\n");
			}
		}
		else
		{
			ResultsTitle = SSourcifyResultsInternal::TitleSucceeded;
			ResultsTitleColor = FSlateColor(FLinearColor::Green);

			if (Sourcify::IFileBackupManager* BackupManager = SourcifyCore->GetFileBackupManager())
			{
				if (BackupManager->IsSessionActive())
				{
					// Commit the backup transaction as Sourcification succeeded
					Sourcify::BackupResult CommitResult = BackupManager->CommitAndCleanup(true/* bDeleteBackupFiles */);
					if (!CommitResult.Success)
					{
						UE_LOG(LogSourcify, Warning, TEXT("Failed to commit Sourcify backup transaction after successful Sourcification: %s"), *FString(CommitResult.ErrorMessage.c_str()));
					}
				}
			}
		}
	}
	else if (SourcificationResult == Results::EResultType::NoAction)
	{
		ResultsTitle = SSourcifyResultsInternal::TitleSucceededNoAction;
		ResultsTitleColor = FSlateColor(FLinearColor::Green);
	}
	else if (SourcificationResult == Results::EResultType::Fail)
	{
		ResultsTitle = SSourcifyResultsInternal::TitleFailed;
		ResultsTitleColor = FSlateColor(FLinearColor::Red);
	}
	else if (SourcificationResult == Results::EResultType::Warn)
	{
		ResultsTitle = SSourcifyResultsInternal::TitleWarning;
		ResultsTitleColor = FSlateColor(FLinearColor::Yellow);
	}
	else
	{
		checkNoEntry();
	}

	OutputLog = ResultsTitle + TEXT("\n") + OutputLog;
	
	auto GenerateRowFromFPackageReference = [this](TSharedPtr<FPackageReference> Item, const TSharedRef<STableViewBase>& OwnerTable, bool DisplayParentName = false) -> TSharedRef<ITableRow>
		{
			TArray<UObject*> Objects;
			ForEachObjectWithPackage(Item->PackagePtr, [&Objects](UObject* Object)
				{
					if (Object->IsAsset() && !Object->IsA<UClass>())
					{
						Objects.Emplace(Object);
					}
					return true;
				}, false);

			check(!Objects.IsEmpty());
			TWeakObjectPtr <UObject> Asset = Objects[0];

			FString ParentClassName;
			if (Item->SourcifiedClassName)
			{
				ParentClassName = Item->SourcifiedClassName.GetValue();
			}
			else
			{
				ParentClassName = TEXT("Unknown");
			}

			FString DisplayName = Asset->GetName();
			if (DisplayParentName)
			{
				constexpr TCHAR RightArrow = 0x2192;
				ParentClassName = FString::Printf(TEXT(" %c %s"), RightArrow, *ParentClassName);
			}

			FSoftObjectPath SoftObjectPath(Asset.Get());
			FText PathName = FText::FromString(SoftObjectPath.ToString());

			FLinearColor AssetTypeColor;
			FAssetThumbnailConfig ThumbnailConfig;
			TSharedPtr<FAssetThumbnail> AssetThumbnail = MakeAssetThumbnail(AssetTypeColor, ThumbnailConfig, Asset.Get());

			return SNew(STableRow<TSharedPtr<FPackageReference>>, OwnerTable)
						.Visibility(EVisibility::SelfHitTestInvisible)
				[
					SNew(SBorder)
						.BorderImage(FAppStyle::GetBrush("NoBorder"))
						.Visibility(EVisibility::SelfHitTestInvisible)
						[
							SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Left)
								.VAlign(VAlign_Top)
								.AutoWidth()
								.Padding(SSourcifyResultsInternal::GetRowThumbnailPadding())
								[
									SNew(SBorder)
										.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
										.Padding(2)
										.BorderBackgroundColor(AssetTypeColor)
										.OnMouseDoubleClick_Lambda([this, Asset](const FGeometry& InGeometry, const FPointerEvent& InEvent)->FReply
											{
												GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Asset.Get());
												return FReply::Handled();
											})
										[
											SNew(SBox)
												.WidthOverride(AssetThumbnail->GetSize().X)
												.HeightOverride(AssetThumbnail->GetSize().Y)
												[
													AssetThumbnail->MakeThumbnailWidget(ThumbnailConfig)
												]
										]
								]
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Fill)
								.AutoWidth()
								[
									SNew(SVerticalBox)
										+ SVerticalBox::Slot()
										.HAlign(HAlign_Left)
										.AutoHeight()
										[
											SNew(STextBlock)
												.Text(FText::FromString(DisplayName))
												.ColorAndOpacity(AssetTypeColor)
										]
										+ SVerticalBox::Slot()
										.HAlign(HAlign_Left)
										.AutoHeight()
										[
											PropertyCustomizationHelpers::MakeBrowseButton(FSimpleDelegate::CreateLambda([this, Asset]() -> void
												{
													if (Asset.IsValid())
													{
														TArray<UObject*> Assets = { Asset.Get() };
														GEditor->SyncBrowserToObjects(Assets);
													}
												}), PathName)
										]
								]
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Fill)
								.AutoWidth()
								[
									SNew(STextBlock)
										.Text(FText::FromString(ParentClassName))
										.Visibility(DisplayParentName ? EVisibility::Visible : EVisibility::Collapsed)
								]
						]
				];
		};

	auto GenerateRowFromPackageThatShouldBeDeleted = [this](TSharedPtr<FPackageReference> Item, const TSharedRef<STableViewBase>& OwnerTable, bool DisplayParentName = false) -> TSharedRef<ITableRow>
		{
			bool bIsDeleted = Item->bIsDeleted;
			FString DisplayName;
			FText PathName;
			FLinearColor AssetTypeColor;
			TSharedPtr<FAssetThumbnail> AssetThumbnail;
			FAssetThumbnailConfig ThumbnailConfig;

			// Use cached data if available, otherwise generate and cache it
			if (Item->bDisplayDataCached)
			{
				DisplayName = Item->CachedDisplayName;
				PathName = FText::FromString(Item->CachedPathName);
				AssetTypeColor = Item->CachedAssetTypeColor;
				AssetThumbnail = Item->CachedThumbnail;
			}
			else
			{
				TArray<UObject*> Objects;
				ForEachObjectWithPackage(Item->PackagePtr, [&Objects](UObject* Object)
					{
						if (Object->IsAsset() && !Object->IsA<UClass>())
						{
							Objects.Emplace(Object);
						}
						return true;
					}, false);

				check(!Objects.IsEmpty());
				UObject* Asset = Objects[0];

				DisplayName = Asset->GetName();
				FSoftObjectPath SoftObjectPath(Asset);
				PathName = FText::FromString(SoftObjectPath.ToString());
				AssetThumbnail = MakeAssetThumbnail(AssetTypeColor, ThumbnailConfig, Asset);

				Item->bDisplayDataCached = true;
				Item->CachedDisplayName = DisplayName;
				Item->CachedPathName = SoftObjectPath.ToString();
				Item->CachedAssetTypeColor = AssetTypeColor;
				Item->CachedThumbnail = AssetThumbnail;
				Item->CachedAsset = Asset;
			}

			FString ParentClassName;
			if (Item->SourcifiedClassName)
			{
				ParentClassName = Item->SourcifiedClassName.GetValue();
			}
			else
			{
				ParentClassName = TEXT("Unknown");
			}

			if (DisplayParentName)
			{
				constexpr TCHAR RightArrow = 0x2192;
				ParentClassName = FString::Printf(TEXT(" %c %s"), RightArrow, *ParentClassName);
			}

			// Check if this asset can be safely deleted (not referenced by assets outside PackagesThatShouldBeDeleted)
			bool bCanDelete = CanDeletePackage(Item);

			// Create a custom row that tracks mouse hover
			class SDeleteablePackageTableRow : public STableRow<TSharedPtr<FPackageReference>>
			{
			public:
				TSharedPtr<bool> bIsHovered = MakeShared<bool>(false);

				virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override
				{
					STableRow::OnMouseEnter(MyGeometry, MouseEvent);
					*bIsHovered = true;
				}

				virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override
				{
					STableRow::OnMouseLeave(MouseEvent);
					*bIsHovered = false;
				}
			};

			TSharedRef<SDeleteablePackageTableRow> TableRow = SNew(SDeleteablePackageTableRow, OwnerTable);
			TSharedPtr<bool> IsRowHovered = TableRow->bIsHovered;

			FLinearColor RowTextColor = bIsDeleted ? FLinearColor::Gray : AssetTypeColor;

			TableRow->SetContent(
				SNew(SBorder)
					.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
					.BorderBackgroundColor_Lambda([IsRowHovered, bIsDeleted]()
						{
							if (bIsDeleted)
							{
								return FLinearColor(0.2f, 0.2f, 0.2f, 0.3f);
							}
							return *IsRowHovered ? FLinearColor(1.f, 1.f, 1.f, 0.1f) : FLinearColor::Transparent;
						})
					.ColorAndOpacity(bIsDeleted ? FLinearColor(0.5f, 0.5f, 0.5f, 1.0f) : FLinearColor::White)
					[
						SNew(SHorizontalBox)
							+ SHorizontalBox::Slot()
						.HAlign(HAlign_Left)
						.VAlign(VAlign_Top)
						.AutoWidth()
						.Padding(SSourcifyResultsInternal::GetRowThumbnailPadding())
						[
							SNew(SBorder)
								.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
								.Padding(2)
								.BorderBackgroundColor(RowTextColor)
								.OnMouseDoubleClick_Lambda([this, Item](const FGeometry& InGeometry, const FPointerEvent& InEvent)->FReply
									{
										if (!Item->bIsDeleted && Item->CachedAsset.IsValid())
										{
											GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Item->CachedAsset.Get());
										}
										return FReply::Handled();
									})
								[
									SNew(SBox)
										.WidthOverride(AssetThumbnail->GetSize().X)
										.HeightOverride(AssetThumbnail->GetSize().Y)
										[
											AssetThumbnail->MakeThumbnailWidget(ThumbnailConfig)
										]
								]
						]
						+ SHorizontalBox::Slot()
						.HAlign(HAlign_Left)
						.VAlign(VAlign_Center)
						.AutoWidth()
						[
							SNew(SVerticalBox)
								+ SVerticalBox::Slot()
								.HAlign(HAlign_Left)
								.AutoHeight()
								[
									SNew(SHorizontalBox)
										+ SHorizontalBox::Slot()
										.HAlign(HAlign_Left)
										.VAlign(VAlign_Center)
										.AutoWidth()
										[
											SNew(STextBlock)
												.Text(FText::FromString(DisplayName))
												.ColorAndOpacity(RowTextColor)
										]
										+ SHorizontalBox::Slot()
										.HAlign(HAlign_Left)
										.VAlign(VAlign_Center)
										.AutoWidth()
										[
											SNew(STextBlock)
												.Text(FText::FromString(ParentClassName))
												.Visibility(DisplayParentName ? EVisibility::Visible : EVisibility::Collapsed)
												.ColorAndOpacity(bIsDeleted ? FLinearColor::Gray : FLinearColor::White)
										]
								]
								+ SVerticalBox::Slot()
								.HAlign(HAlign_Left)
								.AutoHeight()
								[
									PropertyCustomizationHelpers::MakeBrowseButton(FSimpleDelegate::CreateLambda([this, Item]() -> void
										{
											if (!Item->bIsDeleted && Item->CachedAsset.IsValid())
											{
												TArray<UObject*> Assets = { Item->CachedAsset.Get() };
												GEditor->SyncBrowserToObjects(Assets);
											}
										}), PathName, !bIsDeleted)
								]
						]
						+ SHorizontalBox::Slot()
						.HAlign(HAlign_Right)
						.VAlign(VAlign_Center)
						.FillWidth(1.0f)
						.Padding(10, 0, 5, 0)
						[
							SNew(SButton)
								.Text(FText::FromString(bIsDeleted ? TEXT("Deleted") : TEXT("Delete")))
								.ToolTipText(bIsDeleted
									? FText::FromString(TEXT("This asset has been deleted"))
									: (bCanDelete 
										? FText::FromString(TEXT("Delete this asset (it is no longer referenced by other assets)"))
										: FText::FromString(TEXT("Cannot delete: this asset is still referenced by other assets"))))
								.IsEnabled(bCanDelete && !bIsDeleted)
								.OnClicked_Lambda([this, Item]() -> FReply
									{
										DeleteSinglePackage(Item);
										return FReply::Handled();
									})
						]
					]
				);

			return TableRow;
		};

	// Errors and no-action messages
	if (SourcificationResult == Results::EResultType::Fail || SourcificationResult == Results::EResultType::Warn)
	{
		FString ErrorMessage = InArgs._SourcifyResults->Message.c_str();
		TSharedPtr<FErrorReference> ErrorRef(new FErrorReference);
		ErrorRef->Error = ErrorMessage;
		if (SourcificationResult == Results::EResultType::Fail)
		{
			SourcificationErrors.Emplace(ErrorRef);
		}
		else
		{
			SourcificationWarnings.Emplace(ErrorRef);
		}
		OutputLog += TEXT("\n") + ErrorMessage + TEXT("\n");
	}

	// Lambda used by slate to generate a row for an error
	auto GenerateRowFromResultsErrorReference = [this](TSharedPtr<FErrorReference> Item, const TSharedRef<STableViewBase>& OwnerTable) -> TSharedRef<ITableRow>
		{
			return SNew(STableRow<TSharedPtr<FErrorReference>>, OwnerTable)
						.Visibility(EVisibility::SelfHitTestInvisible)
				[
					SNew(STextBlock)
						.Visibility(EVisibility::SelfHitTestInvisible)
						.Text(FText::FromString(Item->Error))
				];
		};

	// Lambda used by slate to generate a row for a compilation error
	auto GenerateRowFromCompilationErrorReference = [this](TSharedPtr<FCompilationErrorReference> Item, const TSharedRef<STableViewBase>& OwnerTable) -> TSharedRef<ITableRow>
		{
			TArray<UObject*> Objects;
			ForEachObjectWithPackage(Item->PackagePtr, [&Objects](UObject* Object)
				{
					if (Object->IsAsset() && !Object->IsA<UClass>())
					{
						Objects.Emplace(Object);
					}
					return true;
				}, false);

			TWeakObjectPtr<UObject> Asset = Objects.IsEmpty() ? nullptr : Objects[0];
			
			FLinearColor AssetTypeColor = FLinearColor::Red;
			FAssetThumbnailConfig ThumbnailConfig;
			TSharedPtr<FAssetThumbnail> AssetThumbnail;
			if (Asset.IsValid())
			{
				AssetThumbnail = MakeAssetThumbnail(AssetTypeColor, ThumbnailConfig, Asset.Get());
				AssetTypeColor = FLinearColor::Red; // Override to red for errors
			}

			FSoftObjectPath SoftObjectPath(Asset.Get());
			FText PathName = FText::FromString(SoftObjectPath.ToString());

			return SNew(STableRow<TSharedPtr<FCompilationErrorReference>>, OwnerTable)
						.Visibility(EVisibility::SelfHitTestInvisible)
				[
					SNew(SBorder)
						.BorderImage(FAppStyle::GetBrush("NoBorder"))
						.Visibility(EVisibility::SelfHitTestInvisible)
						[
							SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Left)
								.VAlign(VAlign_Top)
								.AutoWidth()
								.Padding(SSourcifyResultsInternal::GetRowThumbnailPadding())
								[
									SNew(SBorder)
										.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
										.Padding(2)
										.BorderBackgroundColor(AssetTypeColor)
										.OnMouseDoubleClick_Lambda([this, Asset](const FGeometry& InGeometry, const FPointerEvent& InEvent)->FReply
											{
												if (Asset.IsValid())
												{
													GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Asset.Get());
												}
												return FReply::Handled();
											})
										[
											SNew(SBox)
												.WidthOverride(AssetThumbnail.IsValid() ? AssetThumbnail->GetSize().X : SSourcifyResultsInternal::AssetThumbnailSize)
												.HeightOverride(AssetThumbnail.IsValid() ? AssetThumbnail->GetSize().Y : SSourcifyResultsInternal::AssetThumbnailSize)
												[
													AssetThumbnail.IsValid() ? AssetThumbnail->MakeThumbnailWidget(ThumbnailConfig) : SNullWidget::NullWidget
												]
										]
								]
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Fill)
								.AutoWidth()
								[
									SNew(SVerticalBox)
										+ SVerticalBox::Slot()
										.HAlign(HAlign_Left)
										.AutoHeight()
										[
											SNew(STextBlock)
												.Text(FText::FromString(Item->BlueprintName))
												.ColorAndOpacity(AssetTypeColor)
										]
										+ SVerticalBox::Slot()
										.HAlign(HAlign_Left)
										.AutoHeight()
										[
											PropertyCustomizationHelpers::MakeBrowseButton(FSimpleDelegate::CreateLambda([this, Asset]() -> void
												{
													if (Asset.IsValid())
													{
														TArray<UObject*> Assets = { Asset.Get() };
														GEditor->SyncBrowserToObjects(Assets);
													}
												}), PathName)
										]
								]
						]
				];
		};

	auto GenerateRowFromRenameReference = [this](TSharedPtr<FRenameReference> Item, const TSharedRef<STableViewBase>& OwnerTable) -> TSharedRef<ITableRow>
		{
			constexpr TCHAR RightArrow = 0x2192;
			FString MainText = FString::Printf(TEXT("%s  %c  %s"), *Item->OriginalName, RightArrow, *Item->NewName);

			FString ConflictText;        // "Conflicts with ACharacter::OnMovementModeChanged"
			FString FileLocationText;    // "(Character.h:924)"
			FString FullHeaderPath;      // Absolute path for opening in IDE
			int32 LineNumber = -1;

			if (!Item->ConflictingParentClassName.IsEmpty())
			{
				ConflictText = FString::Printf(TEXT("Conflicts with %s::%s"),
					*Item->ConflictingParentClassName, *Item->OriginalName);
				if (Item->ConflictingParentLineNumber > 0)
				{
					FString HeaderFileName = FPaths::GetCleanFilename(Item->ConflictingParentHeaderPath);
					FileLocationText = FString::Printf(TEXT("(%s:%d)"), *HeaderFileName, Item->ConflictingParentLineNumber);
					FullHeaderPath = Item->ConflictingParentHeaderPath;
					LineNumber = Item->ConflictingParentLineNumber;
				}
			}

			TSharedPtr<SVerticalBox> Content = SNew(SVerticalBox)
				+ SVerticalBox::Slot().AutoHeight().Padding(4, 2)
				[
					SNew(STextBlock)
						.Text(FText::FromString(MainText))
				];

			if (!ConflictText.IsEmpty())
			{
				TSharedPtr<SHorizontalBox> DetailRow = SNew(SHorizontalBox)
					+ SHorizontalBox::Slot()
					.AutoWidth()
					[
						SNew(STextBlock)
							.Text(FText::FromString(ConflictText))
							.ColorAndOpacity(FSlateColor(FLinearColor(0.5f, 0.5f, 0.5f)))
							.Font(FAppStyle::GetFontStyle("SmallFont"))
					];

				if (!FileLocationText.IsEmpty())
				{
					DetailRow->AddSlot()
						.AutoWidth()
						.Padding(6, 0, 0, 0)
						[
							SNew(SHyperlink)
								.Text(FText::FromString(FileLocationText))
								.TextStyle(&FAppStyle::Get().GetWidgetStyle<FTextBlockStyle>("SmallText"))
								.OnNavigate_Lambda([FullHeaderPath, LineNumber]()
									{
										FSourceCodeNavigation::OpenSourceFile(FullHeaderPath, LineNumber);
									})
						];
				}

				Content->AddSlot().AutoHeight().Padding(16, 0, 4, 2)
				[
					DetailRow.ToSharedRef()
				];
			}

			return SNew(STableRow<TSharedPtr<FRenameReference>>, OwnerTable)
				.Visibility(EVisibility::SelfHitTestInvisible)
				[
					Content.ToSharedRef()
				];
		};

	// Build the function renames section (nested expanders per class group)
	TSharedPtr<SVerticalBox> RenameGroupsBox = SNew(SVerticalBox);
	for (FRenameClassGroup& Group : RenameClassGroups)
	{
		FString GroupHeaderText = FString::Printf(TEXT("%s  (%d %s)"),
			*Group.ClassName, Group.Renames.Num(),
			Group.Renames.Num() == 1 ? TEXT("rename") : TEXT("renames"));

		RenameGroupsBox->AddSlot().AutoHeight().Padding(SSourcifyResultsInternal::SubRowPaddingLeft, 0, 0, 0)
		[
			SNew(SSourcifyExpanderArrow).ArrowText(GroupHeaderText).bIsSubSection(true)
			[
				SNew(SBorder)
					.BorderImage(FAppStyle::GetBrush("NoBorder"))
					[
						SNew(SListView<TSharedPtr<FRenameReference>>)
							.ListItemsSource(&Group.Renames)
							.OnGenerateRow_Lambda(GenerateRowFromRenameReference)
							.SelectionMode(ESelectionMode::None)
							.Visibility(EVisibility::SelfHitTestInvisible)
					]
			]
		];
	}

	ChildSlot
		[
			SNew(SBorder)
				.BorderBackgroundColor(FColor(36, 36, 36))
				[
					SNew(SVerticalBox)
						+ SVerticalBox::Slot()
						.AutoHeight()
						.HAlign(HAlign_Fill)
						.VAlign(VAlign_Top)
						.Padding(5)
						[
							SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Left)
								.VAlign(VAlign_Center)
								.AutoWidth()
								[
									SNew(STextBlock)
										.Text(FText::FromString(ResultsTitle))
										.Font(FCoreStyle::GetDefaultFontStyle("Regular", 18))
										.ColorAndOpacity(ResultsTitleColor)
								]
								+ SHorizontalBox::Slot()
								.HAlign(HAlign_Right)
								.FillWidth(1)
								[
									SNew(SBorder)
										.BorderImage(FAppStyle::GetBrush("WhiteBrush"))
										.BorderBackgroundColor(FLinearColor(0.3f, 0.3f, 0.3f))
										.Padding(3)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("Brushes.Panel"))
												[
													SNew(SImageTexture, SourcifyLogoTexture)
												]
										]
									
								]
						]
						+ SVerticalBox::Slot()
						.VAlign(VAlign_Fill)
						.FillHeight(1.f)
						[
							SNew(SScrollBox)
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::CompilationErrorsSectionText)
										.Visibility(BlueprintsWithCompilationErrors.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<FCompilationErrorReference>>)
														.ListItemsSource(&BlueprintsWithCompilationErrors)
														.OnGenerateRow_Lambda(GenerateRowFromCompilationErrorReference)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::NonSourcifiedAssetIniReferencesSectionText)
										.Visibility(NonSourcifiedAssetIniReferences.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<ResultsWindowIniReference>>)
														.ListItemsSource(&NonSourcifiedAssetIniReferences)
														.OnGenerateRow_Lambda(GenerateRowFromResultsWindowIniReference)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::SourcifiedBPSectionText)
										.Visibility(SourcifiedBPs.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<FPackageReference>>)
														.ListItemsSource(&SourcifiedBPs)
														.OnGenerateRow_Lambda(GenerateRowFromFPackageReference, true)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::PackagesContainingSourcifiedReferencesSectionText)
										.Visibility(PackagesContainingSourcifiedReferences.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<FPackageReference>>)
														.ListItemsSource(&PackagesContainingSourcifiedReferences)
														.OnGenerateRow_Lambda(GenerateRowFromFPackageReference)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::PackagesThatShouldBeDeletedSectionText)
										.Visibility(PackagesThatShouldBeDeleted.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SAssignNew(PackagesThatShouldBeDeletedListView, SListView<TSharedPtr<FPackageReference>>)
														.ListItemsSource(&PackagesThatShouldBeDeleted)
														.OnGenerateRow_Lambda(GenerateRowFromPackageThatShouldBeDeleted, true)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::ErrorMessageSectionText)
										.Visibility(SourcificationErrors.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<FErrorReference>>)
														.ListItemsSource(&SourcificationErrors)
														.OnGenerateRow_Lambda(GenerateRowFromResultsErrorReference)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::WarningMessageSectionText)
										.Visibility(SourcificationWarnings.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											SNew(SBorder)
												.BorderImage(FAppStyle::GetBrush("NoBorder"))
												[
													SNew(SListView<TSharedPtr<FErrorReference>>)
														.ListItemsSource(&SourcificationWarnings)
														.OnGenerateRow_Lambda(GenerateRowFromResultsErrorReference)
														.SelectionMode(ESelectionMode::None)
														.Visibility(EVisibility::SelfHitTestInvisible)
												]
										]
								]
								+ SScrollBox::Slot()
								.HAlign(HAlign_Fill)
								.Padding(2)
								[
									SNew(SSourcifyExpanderArrow).ArrowText(SSourcifyResultsInternal::FunctionRenameSectionText)
										.Visibility(RenameClassGroups.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible)
										[
											RenameGroupsBox.ToSharedRef()
										]
								]
						]
						+ SVerticalBox::Slot()
						.VAlign(VAlign_Bottom)
						.HAlign(HAlign_Center)
						.Padding(2)
						.AutoHeight()
						[
							SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.AutoWidth()
								.Padding(5, 0)
								[
									SNew(SButton)
										.Text(FText::FromString(TEXT("Undo Sourcification")))
										.ToolTipText(FText::FromString(TEXT("Restore all modified files to their original state and trigger a recompile")))
										.OnClicked(this, &SSourcifyResults::OnUndoButtonClick)
										.Visibility(this, &SSourcifyResults::GetUndoButtonVisibility)
								]
								+ SHorizontalBox::Slot()
								.AutoWidth()
								.Padding(5, 0)
								[
									SNew(SButton)
										.Text(FText::FromString(TEXT("Delete All Unreferenced")))
										.ToolTipText(FText::FromString(TEXT("Delete all assets that were fully sourcified and are no longer referenced by other assets")))
										.OnClicked(this, &SSourcifyResults::OnDeleteAllUnreferencedButtonClick)
										.Visibility(this, &SSourcifyResults::GetDeleteAllButtonVisibility)
								]
								+ SHorizontalBox::Slot()
								.AutoWidth()
								.Padding(5, 0)
								[
									SNew(SButton)
										.Text(FText::FromString(TEXT("Copy Results to Clipboard")))
										.OnClicked(this, &SSourcifyResults::OnCopyToClipboardButtonClick)
								]
						]
				]

		];
}

FReply SSourcifyResults::OnCopyToClipboardButtonClick()
{
	FPlatformApplicationMisc::ClipboardCopy(*OutputLog);

	return FReply::Handled();
}

TSharedPtr<FAssetThumbnail> SSourcifyResults::MakeAssetThumbnail(FLinearColor& OutAssetColor, FAssetThumbnailConfig& OutThumbnailConfig, UObject* InObject)
{
	FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
	TSharedPtr<IAssetTypeActions> AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(InObject->GetClass()).Pin();
	if (AssetTypeActions.IsValid())
	{
		OutAssetColor = AssetTypeActions->GetTypeColor();
	}
	FAssetThumbnailConfig ThumbnailConfig;
	ThumbnailConfig.AssetTypeColorOverride = OutAssetColor;
	ThumbnailConfig.bAllowFadeIn = true;
	ThumbnailConfig.bForceGenericThumbnail = false;
	ThumbnailConfig.HighlightedText = FText::FromString(InObject->GetPathName());
	return MakeShareable(new FAssetThumbnail(InObject, SSourcifyResultsInternal::AssetThumbnailSize, SSourcifyResultsInternal::AssetThumbnailSize, nullptr));
}

EVisibility SSourcifyResults::GetUndoButtonVisibility() const
{
	// Only show Undo button if there are compilation errors and undo hasn't been performed yet
	if (UndoPerformed)
	{
		return EVisibility::Collapsed;
	}
	
	if (!BlueprintsWithCompilationErrors.IsEmpty() && SourcifyCore != nullptr)
	{
		Sourcify::IFileBackupManager* BackupManager = SourcifyCore->GetFileBackupManager();
		if (BackupManager != nullptr)
		{
			return EVisibility::Visible;
		}
	}
	return EVisibility::Collapsed;
}

FReply SSourcifyResults::OnUndoButtonClick()
{
	if (!SourcifyCore)
	{
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Unable to undo: SourcifyCore is not available.")));
		return FReply::Handled();
	}

	Sourcify::IFileBackupManager* BackupManager = SourcifyCore->GetFileBackupManager();
	if (!BackupManager)
	{
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Unable to undo: No backup manager available.")));
		return FReply::Handled();
	}

	EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, 
		FText::FromString(TEXT("Are you sure you want to undo the Sourcification?\n\nThis will:\n- Restore all modified source files from backup\n- Reload all touched packages\n- Trigger a Live Coding compile\n\nThis operation cannot be undone.")));
	
	if (Result != EAppReturnType::Yes)
	{
		return FReply::Handled();
	}

	UE_LOG(LogSourcify, Log, TEXT("Undo: Restoring backups..."));
	Sourcify::BackupResult RestoreResult = BackupManager->RestoreAllBackups();
	if (!RestoreResult.Success)
	{
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(
			TEXT("Failed to restore backups: %s"), RestoreResult.ErrorMessage.c_str())));
		return FReply::Handled();
	}

	Sourcify::BackupResult CleanupResult = BackupManager->CommitAndCleanup(true);
	if (!CleanupResult.Success)
	{
		UE_LOG(LogSourcify, Warning, TEXT("Undo: Backup cleanup had issues: %s"), CleanupResult.ErrorMessage.c_str());
	}

	UE_LOG(LogSourcify, Log, TEXT("Undo: Reloading packages after restore..."));
	if (!AllTouchedPackages.IsEmpty())
	{
		TArray<FString> PackagePaths;
		for (TObjectPtr<UPackage> PackagePtr : AllTouchedPackages)
		{
			if (PackagePtr)
			{
				PackagePaths.Add(PackagePtr->GetName());
			}
		}

		FText ErrorMessage;
		if (!UPackageTools::ReloadPackages(AllTouchedPackages, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive))
		{
			FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(
				TEXT("Failed to reload packages after restore: %s"), *ErrorMessage.ToString())));
			return FReply::Handled();
		}
 
		AllTouchedPackages.Empty();
		for (const FString& PackagePath : PackagePaths)
		{
			AllTouchedPackages.Add(UPackageTools::LoadPackage(PackagePath));
		}

		if (!UPackageTools::ReloadPackages(AllTouchedPackages, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive))
		{
			FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(
				TEXT("Failed to reload packages after restore: %s"), *ErrorMessage.ToString())));
			return FReply::Handled();
		}
	}

#if WITH_LIVE_CODING
	ILiveCodingModule* LiveCoding = FModuleManager::GetModulePtr<ILiveCodingModule>(LIVE_CODING_MODULE_NAME);
	if (LiveCoding && LiveCoding->IsEnabledForSession())
	{
		LiveCoding->Compile();
	}
	else
	{
		UE_LOG(LogSourcify, Warning, TEXT("Undo: Live Coding is not enabled. Please manually compile to apply restored changes."));
	}
#else
	UE_LOG(LogSourcify, Warning, TEXT("Undo: Live Coding is not available. Please manually compile to apply restored changes."));
#endif

	// Check if any Blueprints still fail to compile after restore (due to stale class data in memory)
	bool bAnyBlueprintFailedToCompile = false;
	for (TObjectPtr<UPackage> PackagePtr : AllTouchedPackages)
	{
		if (bAnyBlueprintFailedToCompile)
		{
			break;
		}

		if (!PackagePtr)
		{
			continue;
		}

		TArray<UBlueprint*> Blueprints;
		ForEachObjectWithPackage(PackagePtr, [&Blueprints](UObject* Object)
			{
				if (UBlueprint* Blueprint = Cast<UBlueprint>(Object))
				{
					Blueprints.Emplace(Blueprint);
				}
				return true;
			}, false);

		for (UBlueprint* Blueprint : Blueprints)
		{
			FBlueprintEditorUtils::RefreshAllNodes(Blueprint);
			FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipSave);
			if (Blueprint->Status == EBlueprintStatus::BS_Error)
			{
				bAnyBlueprintFailedToCompile = true;
				UE_LOG(LogSourcify, Warning, TEXT("Undo: Blueprint '%s' still has compilation errors after restore."), *Blueprint->GetName());
				break;
			}
		}
	}

	UndoPerformed = true;

	if (ParentTab)
	{
		ParentTab->RequestCloseTab();
	}

	FNotificationInfo Info(FText::FromString(TEXT("Sourcification Undo Complete")));
	Info.ExpireDuration = 15.0f; // Make sure we outlive the Live Coding compile
	Info.bUseThrobber = false;
	TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
	if (NotificationItem.IsValid())
	{
		NotificationItem->SetCompletionState(SNotificationItem::ECompletionState::CS_Success);
	}

	if (bAnyBlueprintFailedToCompile)
	{
		EAppReturnType::Type RestartResult = FMessageDialog::Open(EAppMsgType::YesNo,
			FText::FromString(TEXT("Some Blueprints still have compilation errors after restore.\n\nThis is likely due to stale class data remaining in memory from the Sourcification.\n\nAn editor restart is recommended to fully clear the stale data.\n\nWould you like to restart the editor now?")));

		if (RestartResult == EAppReturnType::Yes)
		{
			FUnrealEdMisc::Get().RestartEditor(false);
		}
	}

	return FReply::Handled();
}

bool SSourcifyResults::CanDeletePackage(TSharedPtr<FPackageReference> InPackage) const
{
	if (!InPackage || !InPackage->PackagePtr)
	{
		return false;
	}

	TArray<UObject*> Objects;
	ForEachObjectWithPackage(InPackage->PackagePtr, [&Objects](UObject* Object)
		{
			if (Object->IsAsset() && !Object->IsA<UClass>())
			{
				Objects.Emplace(Object);
			}
			return true;
		}, false);

	if (Objects.IsEmpty())
	{
		return false;
	}

	// Build a set of package names that are allowed to reference this asset
	// This includes:
	// 1. Packages that should be deleted (they will be deleted too)
	// 2. Packages containing sourcified references (they were updated during sourcification)
	// 3. Sourcified BPs (they were reparented during sourcification)
	// The asset registry may not be updated until assets are saved, so we need to
	// also allow referencers that were modified during the sourcify run
	TSet<FName> AllowedReferencerNames;
	
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesThatShouldBeDeleted)
	{
		if (PackageRef && PackageRef->PackagePtr)
		{
			AllowedReferencerNames.Add(PackageRef->PackagePtr->GetFName());
		}
	}
	
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesContainingSourcifiedReferences)
	{
		if (PackageRef && PackageRef->PackagePtr)
		{
			AllowedReferencerNames.Add(PackageRef->PackagePtr->GetFName());
		}
	}
	
	for (const TSharedPtr<FPackageReference>& PackageRef : SourcifiedBPs)
	{
		if (PackageRef && PackageRef->PackagePtr)
		{
			AllowedReferencerNames.Add(PackageRef->PackagePtr->GetFName());
		}
	}

	IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
	AssetRegistry.ScanPathsSynchronous({ InPackage->PackagePtr->GetPathName() }, true);
	
	for (UObject* Asset : Objects)
	{
		TArray<FAssetIdentifier> Referencers;
		AssetRegistry.GetReferencers(FAssetIdentifier(Asset->GetOutermost()->GetFName()), Referencers);

		for (const FAssetIdentifier& Referencer : Referencers)
		{
			// Skip self-references
			if (Referencer.PackageName == InPackage->PackagePtr->GetFName())
			{
				continue;
			}

			// If the referencer is not in the allowed list, we can't delete this asset
			if (!AllowedReferencerNames.Contains(Referencer.PackageName))
			{
				return false;
			}
		}
	}

	return true;
}

TArray<TSharedPtr<SSourcifyResults::FPackageReference>> SSourcifyResults::GetDeletablePackages() const
{
	TArray<TSharedPtr<FPackageReference>> DeletablePackages;
	
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesThatShouldBeDeleted)
	{
		if (PackageRef->bIsDeleted)
		{
			continue;
		}
		
		if (CanDeletePackage(PackageRef))
		{
			DeletablePackages.Add(PackageRef);
		}
	}
	
	return DeletablePackages;
}

void SSourcifyResults::PromptToSaveDirtyPackagesAsync(TFunction<void(bool bSuccess)> OnComplete)
{
	TArray<UPackage*> DirtyPackages;
	
	for (const TSharedPtr<FPackageReference>& PackageRef : SourcifiedBPs)
	{
		if (PackageRef && PackageRef->PackagePtr && PackageRef->PackagePtr->IsDirty())
		{
			DirtyPackages.AddUnique(PackageRef->PackagePtr.Get());
		}
	}
	
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesContainingSourcifiedReferences)
	{
		if (PackageRef && PackageRef->PackagePtr && PackageRef->PackagePtr->IsDirty())
		{
			DirtyPackages.AddUnique(PackageRef->PackagePtr.Get());
		}
	}
	
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesThatShouldBeDeleted)
	{
		if (PackageRef && !PackageRef->bIsDeleted && PackageRef->PackagePtr && PackageRef->PackagePtr->IsDirty())
		{
			DirtyPackages.AddUnique(PackageRef->PackagePtr.Get());
		}
	}
	
	if (DirtyPackages.IsEmpty())
	{
		OnComplete(true);
		return;
	}
	
	FText ReasonTitle = FText::FromString(TEXT("Save Required Before Deletion"));
	FText ReasonMessage = FText::FromString(TEXT(
		"Some assets have unsaved changes. These must be saved before deletion to ensure the Asset Registry has updated reference data.\n\n"
		"Without saving first, deleted assets may still appear as referenced, preventing proper cleanup.\n\n"
		"Would you like to save these assets now?"));
	
	EAppReturnType::Type UserChoice = FMessageDialog::Open(EAppMsgType::YesNo, ReasonMessage, ReasonTitle);
	if (UserChoice != EAppReturnType::Yes)
	{
		OnComplete(false);
		return;
	}
	
	const bool PromptUserToSave = true;
	const bool SaveMapPackages = true;
	const bool SaveContentPackages = true;	
	bool bSaveSucceeded = FEditorFileUtils::SaveDirtyPackages(PromptUserToSave, SaveMapPackages, SaveContentPackages);
	
	if (!bSaveSucceeded)
	{
		OnComplete(false);
		return;
	}
	
	TWeakPtr<SSourcifyResults> WeakSelf = SharedThis(const_cast<SSourcifyResults*>(this));
	TArray<FString> PackagePaths;
	for (UPackage* Package : DirtyPackages)
	{
		PackagePaths.Add(Package->GetPathName());
	}
	
	FTSTicker::GetCoreTicker().AddTicker(
		FTickerDelegate::CreateLambda([WeakSelf, PackagePaths, OnComplete](float DeltaTime) -> bool
		{
			if (TSharedPtr<SSourcifyResults> StrongSelf = WeakSelf.Pin())
			{
				IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
				AssetRegistry.ScanPathsSynchronous(PackagePaths, true);				
				OnComplete(true);
			}
			return false; // Don't repeat
		}),
		0.5f // Delay in seconds
	);
}

void SSourcifyResults::DeleteSinglePackage(TSharedPtr<FPackageReference> InPackage)
{
	if (!InPackage || !InPackage->PackagePtr)
	{
		return;
	}

	// Prompt to save dirty packages before deletion (required for asset registry to have updated references)
	// Use async to avoid blocking the main thread
	PromptToSaveDirtyPackagesAsync([this, InPackage](bool bSuccess)
	{
		if (bSuccess)
		{
			ExecuteDeleteSinglePackage(InPackage);
		}
	});
}

void SSourcifyResults::ExecuteDeleteSinglePackage(TSharedPtr<FPackageReference> InPackage)
{
	if (!InPackage || !InPackage->PackagePtr)
	{
		return;
	}

	TArray<UObject*> ObjectsToDelete;
	ForEachObjectWithPackage(InPackage->PackagePtr, [&ObjectsToDelete](UObject* Object)
		{
			if (Object->IsAsset() && !Object->IsA<UClass>())
			{
				ObjectsToDelete.Emplace(Object);
			}
			return true;
		}, false);

	if (ObjectsToDelete.IsEmpty())
	{
		return;
	}

	SSourcifyResultsInternal::MarkReinstrancedClassesAsGarbage(ObjectsToDelete);

	int32 NumDeleted = ObjectTools::DeleteObjects(ObjectsToDelete, true);	
	if (NumDeleted > 0)
	{
		InPackage->bIsDeleted = true;
		
		// Rebuild the list view to regenerate rows with updated state
		if (PackagesThatShouldBeDeletedListView.IsValid())
		{
			PackagesThatShouldBeDeletedListView->RebuildList();
		}
		
		UE_LOG(LogSourcify, Log, TEXT("Deleted asset: %s"), *InPackage->PackagePtr->GetName());
	}
}

EVisibility SSourcifyResults::GetDeleteAllButtonVisibility() const
{
	bool bHasNonDeletedPackages = false;
	for (const TSharedPtr<FPackageReference>& PackageRef : PackagesThatShouldBeDeleted)
	{
		if (!PackageRef->bIsDeleted)
		{
			bHasNonDeletedPackages = true;
			break;
		}
	}
	return bHasNonDeletedPackages ? EVisibility::Visible : EVisibility::Collapsed;
}

FReply SSourcifyResults::OnDeleteAllUnreferencedButtonClick()
{
	TArray<TSharedPtr<FPackageReference>> DeletablePackages = GetDeletablePackages();
	
	if (DeletablePackages.IsEmpty())
	{
		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("No assets can be deleted at this time. All assets are still referenced by other assets in the project.")));
		return FReply::Handled();
	}

	FString Message = FString::Printf(TEXT("Are you sure you want to delete %d asset(s)?\n\nThis operation cannot be undone."), DeletablePackages.Num());
	EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(Message));
	
	if (Result != EAppReturnType::Yes)
	{
		return FReply::Handled();
	}

	// Prompt to save dirty packages before deletion (required for asset registry to have updated references)
	PromptToSaveDirtyPackagesAsync([this, DeletablePackages](bool bSuccess)
	{
		if (bSuccess)
		{
			ExecuteDeleteMultiplePackages(DeletablePackages);
		}
	});

	return FReply::Handled();
}

void SSourcifyResults::ExecuteDeleteMultiplePackages(const TArray<TSharedPtr<FPackageReference>>& DeletablePackages)
{
	TArray<UObject*> AllObjectsToDelete;
	for (const TSharedPtr<FPackageReference>& PackageRef : DeletablePackages)
	{
		if (!PackageRef || !PackageRef->PackagePtr)
		{
			continue;
		}

		ForEachObjectWithPackage(PackageRef->PackagePtr, [&AllObjectsToDelete](UObject* Object)
			{
				if (Object->IsAsset() && !Object->IsA<UClass>())
				{
					AllObjectsToDelete.Emplace(Object);
				}
				return true;
			}, false);
	}

	SSourcifyResultsInternal::MarkReinstrancedClassesAsGarbage(AllObjectsToDelete);

	int32 NumDeleted = ObjectTools::DeleteObjects(AllObjectsToDelete, true);
	if (NumDeleted > 0)
	{
		for (const TSharedPtr<FPackageReference>& PackageRef : DeletablePackages)
		{
			PackageRef->bIsDeleted = true;
		}

		// Rebuild the list view to regenerate rows with updated state
		if (PackagesThatShouldBeDeletedListView.IsValid())
		{
			PackagesThatShouldBeDeletedListView->RebuildList();
		}
		
		UE_LOG(LogSourcify, Log, TEXT("Deleted %d assets"), NumDeleted);
	}
}

#undef LOCTEXT_NAMESPACE
