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

#include "Sourcify.h"

#include "BlueprintEditorLibrary.h"
#include "ContentBrowserModule.h"
#include "DesktopPlatformModule.h"
#include "EdGraphUtilities.h"
#include "Engine/Blueprint.h"
#include "Engine/UserDefinedEnum.h"
#include "Engine/UserDefinedStruct.h"	
#include "FileHelpers.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Notifications/NotificationManager.h"
#include "HAL/PlatformFileManager.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Interfaces/IPluginManager.h"
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#include "ISourceControlModule.h"
#include "Logging/MessageLog.h"
#include "MessageLogModule.h"
#include "MessageLogInitializationOptions.h"
#include "Misc/EngineVersionComparison.h"
#include "Misc/FileHelper.h"
#include "Misc/MessageDialog.h"
#include "PropertyEditorModule.h"
#include "SGraphPin.h"
#include "SourceControlOperations.h"
#include "SourcifyCore.h"
#include "SourcifyCoreUEWrapperModule.h"
#include "SourcifyCommands.h"
#include "SourcifySettings.h"
#include "SourcifyStyle.h"
#if UE_VERSION_OLDER_THAN(5, 5, 0)
#include "Engine/UserDefinedStruct.h"
#else
#include "StructUtils/UserDefinedStruct.h"
#endif
#include "ToolMenus.h"
#include "UI/SourcifyGraph/EdGraphNode_Sourcify.h"
#include "UI/SourcifyGraph/SourcifyGraphSchema.h"
#include "UI/SourcifyGraph/SSourcifyGraph.h"
#include "UI/SourcifyGraph/SSourcifyNode.h"
#include "UI/SourcifyResults/SSourcifyResults.h"
#include "UI/SourcifySettingsDetails.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/UnrealTypePrivate.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/Docking/SDockTab.h"

#if SOURCIFY_DEBUG
#include "SourcifyDevOnly.h"
#include "SourcifyDevOnlySettings.h"
#endif

#if !SOURCIFY_CORE_SKIP_DLL_LOAD
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include "Windows/HideWindowsPlatformTypes.h"
#endif

DEFINE_LOG_CATEGORY(LogSourcify)

static const FName SourcifyTabName("Sourcify");

#define LOCTEXT_NAMESPACE "FSourcifyModule"

using namespace Sourcify;

namespace SourcifyUsedModules
{
	static const FName _ContentBrowser{ "ContentBrowser" };
}

class FSourcifyGraphPanelNodeFactory : public FGraphPanelNodeFactory
{
	virtual TSharedPtr<SGraphNode> CreateNode(UEdGraphNode* Node) const override
	{
		if (Node->GetSchema()->IsA<USourcifyGraphSchema>())
		{
			if (UEdGraphNode_Sourcify* DependencyNode = Cast<UEdGraphNode_Sourcify>(Node))
			{
				return SNew(SSourcifyNode, DependencyNode);
			}
		}

		return nullptr;
	}
};

class FSourcifyGraphPanelPinFactory : public FGraphPanelPinFactory
{
	virtual TSharedPtr<SGraphPin> CreatePin(UEdGraphPin* InPin) const override
	{
		if (InPin->GetSchema()->IsA<USourcifyGraphSchema>())
		{
			return SNew(SGraphPin, InPin);
		}

		return nullptr;
	}
};

#if !SOURCIFY_CORE_SKIP_DLL_LOAD
// Get the base address of the current module, Windows only
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#endif

namespace SourcifyModuleInternal
{
	static const FName SourcifyMessageLogName{ "Sourcify" };
	static const FString SOURCIFIED_TYPE_SUFFIX{ TEXT("_SOURCIFIED") };

#if !SOURCIFY_CORE_SKIP_DLL_LOAD
	static void* SourcifyCoreDLLHandle = nullptr;
	static FString GetCurrentDLLPath()
	{
		char Path[MAX_PATH];
		if (GetModuleFileNameA(reinterpret_cast<HINSTANCE>(&__ImageBase), Path, MAX_PATH) == 0)
		{
			UE_LOG(LogSourcify, Error, TEXT("Failed to get current DLL path!"));
			return FString();
		}
		return FPaths::GetPath(Path);
	}

	bool LoadSourcifyCoreDLL()
	{
#ifdef SOURCIFY_CORE_DEV
		FString DllDir = GetCurrentDLLPath();
#else
		FString EngineVersionSubdirectory = FString::Printf(TEXT("UE%d.%d"), ENGINE_MAJOR_VERSION, ENGINE_MINOR_VERSION);
		FString DllDir = GetCurrentDLLPath() / TEXT("../../Source/ThirdParty/SourcifyCore/x64") / EngineVersionSubdirectory;
#endif

		bool FailedToFindPluginDirectory = !FPlatformFileManager::Get().GetPlatformFile().DirectoryExists(*DllDir.ToLower());

		FPlatformProcess::PushDllDirectory(*DllDir);

#if UE_BUILD_DEBUG
		FString DllFileName = TEXT("UnrealEditor-SourcifyCore-Win64-Debug");
#elif UE_BUILD_DEVELOPMENT
		FString DllFileName = TEXT("UnrealEditor-SourcifyCore");
#else
		static_assert(false, "SourcifyCore DLL name not defined for this build configuration!");
#endif		

#if PLATFORM_WINDOWS
		DllFileName += TEXT(".dll");
		SourcifyCoreDLLHandle = FPlatformProcess::GetDllHandle(*DllFileName);
#endif

		FPlatformProcess::PopDllDirectory(*DllDir);

		return SourcifyCoreDLLHandle != nullptr;
	}
#endif
}

void FSourcifyModule::StartupModule()
{
#if !SOURCIFY_CORE_SKIP_DLL_LOAD
	if (!SourcifyModuleInternal::LoadSourcifyCoreDLL())
	{
		UE_LOG(LogSourcify, Error, TEXT("Failed to load SourcifyCore DLL!"));
		return;
	}
#endif

	IUE = FSourcifyCoreUEWrapper::Get().CreateEngineInterface();
	SC = Sourcify::SC::CreateCoreInstance(*IUE);

	if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
	{
		SettingsModule->RegisterSettings(TEXT("Project"), TEXT("Plugins"), TEXT("Sourcify"),
			LOCTEXT("Sourcify Settings Name", "Sourcify"),
			LOCTEXT("Sourcify Settings Description", "Configure the Sourcify plugin"),
			GetMutableDefault<USourcifySettings>());
	}

	FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.RegisterCustomClassLayout(USourcifySettings::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FSourcifySettingsDetails::MakeInstance));

	FSourcifyStyle::Initialize();
	FSourcifyStyle::ReloadTextures();

	FSourcifyCommands::Register();

	FContentBrowserModule& contentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(SourcifyUsedModules::_ContentBrowser);
	_ContextMenuExtension = FContentBrowserMenuExtender_SelectedAssets::CreateRaw(this, &FSourcifyModule::ContextMenuBuilder);
	contentBrowserModule.GetAllAssetViewContextMenuExtenders().Add(_ContextMenuExtension);

	// Register Sourcify Graph
	SourcifyGraphTabName = TEXT("SourcifyGraph");
	FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SourcifyGraphTabName, FOnSpawnTab::CreateRaw(this, &FSourcifyModule::SpawnSourcifyGraphTab))
		.SetDisplayName(LOCTEXT("SourcifyGraphTitle", "Sourcify Graph"))
		.SetMenuType(ETabSpawnerMenuType::Hidden);

	// Pins and nodes for graph
	SourcifyGraphPanelPinFactory = MakeShareable(new FSourcifyGraphPanelPinFactory());
	FEdGraphUtilities::RegisterVisualPinFactory(SourcifyGraphPanelPinFactory);

	SourcifyGraphPanelNodeFactory = MakeShareable(new FSourcifyGraphPanelNodeFactory());
	FEdGraphUtilities::RegisterVisualNodeFactory(SourcifyGraphPanelNodeFactory);

	// Register Sourcify Results Window
	SourcifyResultsTabName = TEXT("SourcifyResults");
	FGlobalTabmanager::Get()->RegisterNomadTabSpawner(SourcifyResultsTabName, FOnSpawnTab::CreateRaw(this, &FSourcifyModule::SpawnSourcifyResultsTab))
		.SetDisplayName(LOCTEXT("SourcifyResultsTitle", "Sourcify Results"))
		.SetMenuType(ETabSpawnerMenuType::Hidden);

	FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
	FMessageLogInitializationOptions InitOptions;
	InitOptions.bShowFilters = true;
	InitOptions.bShowPages = false;
	InitOptions.bAllowClear = true;
	MessageLogModule.RegisterLogListing(SourcifyModuleInternal::SourcifyMessageLogName, LOCTEXT("SourcifyMessageLogName", "Sourcify"), InitOptions);

	CheckPluginVersion();
}

void FSourcifyModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.
	ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>(TEXT("Settings"));

	if (SettingsModule)
	{
		SettingsModule->UnregisterSettings(TEXT("Project"), TEXT("Plugins"), TEXT("Sourcify"));
	}

	if (FModuleManager::Get().IsModuleLoaded(SourcifyUsedModules::_ContentBrowser))
	{
		FContentBrowserModule& contentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(SourcifyUsedModules::_ContentBrowser);
		contentBrowserModule.GetAllAssetViewContextMenuExtenders().RemoveAll([&](const FContentBrowserMenuExtender_SelectedAssets& delegate) {return _ContextMenuExtension.GetHandle() == delegate.GetHandle(); });
	}

	UToolMenus::UnRegisterStartupCallback(this);

	UToolMenus::UnregisterOwner(this);

	FSourcifyStyle::Shutdown();

	FSourcifyCommands::Unregister();

	if (FModuleManager::Get().IsModuleLoaded("MessageLog"))
	{
		FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
		MessageLogModule.UnregisterLogListing(SourcifyModuleInternal::SourcifyMessageLogName);	
	}

#if !SOURCIFY_CORE_SKIP_DLL_LOAD
	if (SourcifyModuleInternal::SourcifyCoreDLLHandle)
	{
		FPlatformProcess::FreeDllHandle(SourcifyModuleInternal::SourcifyCoreDLLHandle);
		SourcifyModuleInternal::SourcifyCoreDLLHandle = nullptr;
	}
#endif
}

TSharedRef<class FExtender> FSourcifyModule::ContextMenuBuilder(const TArray<FAssetData>& selectedAssets)
{
	TSharedRef<FExtender> extender = MakeShared<FExtender>();
	extender->AddMenuExtension("CommonAssetActions", EExtensionHook::Before, nullptr, FMenuExtensionDelegate::CreateLambda([this, selectedAssets](FMenuBuilder& menuBuilder) { MakeContextMenu(menuBuilder, selectedAssets); }));
	return extender;
}

void FSourcifyModule::MakeContextMenu(class FMenuBuilder& menuBuilder, const TArray<FAssetData>& selectedAssets)
{
	for (const FAssetData& asset : selectedAssets)
	{
		if (asset.IsRedirector() || (asset.PackageFlags & PKG_FilterEditorOnly) || 
			(!asset.GetClass()->IsChildOf(UBlueprint::StaticClass()) &&
			 !asset.GetClass()->IsChildOf(UUserDefinedEnum::StaticClass()) &&
			 !asset.GetClass()->IsChildOf(UUserDefinedStruct::StaticClass())))
		{
			return;
		}

		if (UBlueprint* BP = Cast<UBlueprint>(asset.GetAsset()))
		{
			if (BP->BlueprintType == EBlueprintType::BPTYPE_MacroLibrary ||
				BP->BlueprintType == EBlueprintType::BPTYPE_FunctionLibrary)
			{
				return;
			}
		}
	}

	menuBuilder.BeginSection("Sourcify", LOCTEXT("SectionName", "Sourcify"));
	{
#if SOURCIFY_DEBUG
		const USourcifyDevOnlySettings* SourcifyDevOnlySettings = GetDefault<USourcifyDevOnlySettings>();
		if (SourcifyDevOnlySettings->EnableDebugMenu)
		{
			menuBuilder.AddMenuEntry(
				LOCTEXT("Sourcify_Scan", "DEBUG - Sourcify Blueprint"),
				LOCTEXT("Sourcify_ScanTooltip", "Sourcifies the Blueprint"),
				FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.ViewOptions"),
				FUIAction(FExecuteAction::CreateLambda([this, selectedAssets]() {ScanBlueprint(selectedAssets); }))
			);
		}
#endif
		menuBuilder.AddMenuEntry(
			LOCTEXT("Sourcify_Graph", "Sourcify Graph"),
			LOCTEXT("Sourcify_GraphTooltip", "Opens the Sourcify Graph"),
			FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.ViewOptions"),
			FUIAction(FExecuteAction::CreateLambda([this, selectedAssets]()
				{
					CheckAndPromptForDefaultCodeExportPath();

					if (TSharedPtr<SDockTab> NewTab = FGlobalTabmanager::Get()->TryInvokeTab(SourcifyGraphTabName))
					{
						TSharedRef<SSourcifyGraph> SourcifyGraph = StaticCastSharedRef<SSourcifyGraph>(NewTab->GetContent());
						SetupGraph(selectedAssets, *SourcifyGraph);
					}
				}))
		);
		menuBuilder.AddMenuSeparator();
	}
	menuBuilder.EndSection();
}

void FSourcifyModule::SetupGraph(const TArray<struct FAssetData>& SelectedAssets, SSourcifyGraph& Graph)
{
	IsSourcifying = true;

	const USourcifySettings* SourcifySettings = GetDefault<USourcifySettings>();
	SourcifySettings->ToSourcifyCore(SC->GetCoreSettings());
#if SOURCIFY_DEBUG
	const USourcifyDevOnlySettings* SourcifyDevOnlySettings = GetDefault<USourcifyDevOnlySettings>();
	SourcifyDevOnlySettings->ToSourcifyCore(SC->GetCoreSettings());
#endif
	const Sourcify::BlueprintDependencies& Deps = SC->PreSourcify(FSourcifyCoreUEWrapper::Get().Wrap(*SelectedAssets[0].GetAsset()));

	if (!Deps.IsValid())
	{
		Graph.RequestClose();
		return;
	}


	auto SetNodeConversionPathAndFile = [this,
#if SOURCIFY_DEBUG
			SourcifyDevOnlySettings,
#endif
		SourcifySettings](auto&& SetNodeConversionPathAndFile, Sourcify::DependencyNode& DepNode) -> void
		{
#if SOURCIFY_DEBUG
			if (SourcifyDevOnlySettings->AddSourcifiedSuffix)
			{	
				// Conversion settings can be refreshed from BP edits when the Sourcify Graph is up. So make sure we don't keep appending debug suffix
				if (DepNode.ConversionSettings.TypeName.find(*SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX) != DepNode.ConversionSettings.TypeName.size() - SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX.Len())
				{
					DepNode.ConversionSettings.TypeName.append(*SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX);
				}

				
			}
#endif
			if(DepNode.ConversionSettings.OutputDirectory.empty())
			{
				DepNode.ConversionSettings.OutputDirectory = *FPaths::ConvertRelativePathToFull(FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()), SourcifySettings->DefaultCodeExportPath.Path);
			}

			for (Sourcify::DependencyNode::ReferenceInfo& Referencee : DepNode.Referencees)
			{
				SetNodeConversionPathAndFile(SetNodeConversionPathAndFile, *Referencee.Node);
			}
		};

	SetNodeConversionPathAndFile(SetNodeConversionPathAndFile, *Deps.DependencyGraph);

	TArray<UPackage*> DirtyPackages;
	FEditorFileUtils::GetDirtyPackages(DirtyPackages);
	if (!DirtyPackages.IsEmpty())
	{
		if (FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(TEXT("All assets must be saved before Sourcification.\n\nSave now?"))) == EAppReturnType::Yes)
		{
			if (FEditorFileUtils::PromptForCheckoutAndSave(DirtyPackages, true, true) != FEditorFileUtils::EPromptReturnCode::PR_Success)
			{
				Graph.RequestClose();
				return;
			}
		}
		else
		{
			Graph.RequestClose();
			return;
		}
	}

	auto ValidateResourcificationMetadataRecursively = [](auto&& ValidateResourcificationMetadataRecursively, Sourcify::DependencyNode& DepNode) -> void
		{
			for (const Sourcify::DependencyNode::ReferenceInfo& Referencee : DepNode.Referencees)
			{
				ValidateResourcificationMetadataRecursively(ValidateResourcificationMetadataRecursively, *Referencee.Node);
			}

			if (!DepNode.CanBeResourcified)
			{
				return;
			}

			ISCW_UBlueprintConstPtr WrappedBP = std::dynamic_pointer_cast<const ISCW_UBlueprint>(DepNode.Object);
			if (!WrappedBP)
			{
				return;
			}
			
			UBlueprint* BP = FSourcifyCoreUEWrapper::Unwrap(WrappedBP);
			if (!BP || !BP->ParentClass)
			{
				return;
			}
			
			// Get the SourcifiedClass metadata from the package
			UPackage* Package = BP->GetPackage();
			if (!Package)
			{
				return;
			}
			
#if UE_VERSION_NEWER_THAN(5, 5, 9) // Using 5.5.9 to denote 5.6 and newer because there was no UE_VERSION_NEWER_THAN_OR_EQUAL in older versions
			const TMap<FName, FString>* MetadataMap = FMetaData::GetMapForObject(BP);
#else
			const TMap<FName, FString>* MetadataMap = UMetaData::GetMapForObject(BP);
#endif
			const FString* SourcifiedClassName = MetadataMap ? MetadataMap->Find(FName(TEXT("SourcifiedClass"))) : nullptr;
			if (!SourcifiedClassName)
			{
				FString Message = FString::Printf(
					TEXT("Blueprint '%s' has a Sourcified parent class '%s', but this may not be the originally sourcified class for this Blueprint.\n\nWould you like to enable Resourcify for Blueprint '%s'?\nNOTE - This only applies to this run. You can change this on %s's node in the graph"),
					*BP->GetName(),
					*BP->ParentClass->GetName(),
					*BP->GetName(),
					*BP->GetName()
				);
				EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(Message));
				DepNode.ConversionSettings.SetResourcify(Result == EAppReturnType::Yes);
				return;
			}
			
			// Compare with the parent class pathname
			FString ParentClassPath = BP->ParentClass->GetPathName();
			if (!SourcifiedClassName->Equals(ParentClassPath))
			{
				// Show dialog asking user if resourcification is intended
				FString Message = FString::Printf(
					TEXT("Blueprint '%s' was originally Sourcified into parent class ('%s'), but its current parent class is ('%s').\n\nDo you want to attempt resourcification into '%s'?\nSelecting 'No' will Sourcify %s into a new class\nNOTE - This only applies to this run. You can change this on %s's node in the graph"),
					*BP->GetName(),
					**SourcifiedClassName,
					*ParentClassPath,
					*ParentClassPath,
					*BP->GetName(),
					*BP->GetName()
				);

				EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(Message));
				DepNode.ConversionSettings.SetResourcify(Result == EAppReturnType::Yes);
			}
		};

	ValidateResourcificationMetadataRecursively(ValidateResourcificationMetadataRecursively, *Deps.DependencyGraph);

	PreSourcifyDependencies = &Deps;
	Graph.FromDependencies(*PreSourcifyDependencies);

	IsSourcifying = false;
	AssetBeingSourcified = SelectedAssets[0].GetAsset();
}

void FSourcifyModule::CheckAndPromptForDefaultCodeExportPath()
{
	const USourcifySettings* Settings = GetDefault<USourcifySettings>();
	if (!Settings)
	{
		return;
	}

	FString DefaultPath = Settings->DefaultCodeExportPath.Path;

	if (!DefaultPath.IsEmpty())
	{
		return;
	}

	FText DialogTitle = LOCTEXT("SetDefaultCodeExportPathTitle", "Sourcify - Set Default Code Export Path");
	FText DialogMessage = LOCTEXT("SetDefaultCodeExportPathMessage", 
		"No default code export path is set. This is the path that is set on each asset for a new Sourcify run.\n\n"
		"Would you like to set a default code export path now?\n\n"
		"Note: This can be changed in Project Settings > Plugins > Sourcify and per asset for each Sourcify run.");

	EAppReturnType::Type Response = FMessageDialog::Open(
		EAppMsgType::YesNo,
		DialogMessage,
		DialogTitle
	);

	if (Response == EAppReturnType::Yes)
	{
		FString SelectedDirectory;
		FString StartPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
		
		bool bDirectorySelected = false;
		bool bValidDirectory = false;

		while (!bValidDirectory)
		{
			bDirectorySelected = false;
			IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
			if (DesktopPlatform)
			{
				void* TopWindowHandle = FSlateApplication::Get().GetActiveTopLevelWindow().IsValid() 
					? FSlateApplication::Get().GetActiveTopLevelWindow()->GetNativeWindow()->GetOSWindowHandle() 
					: nullptr;

				bDirectorySelected = DesktopPlatform->OpenDirectoryDialog(
					TopWindowHandle,
					TEXT("Select Default Code Export Path"),
					StartPath,
					SelectedDirectory
				);
			}

			if (!bDirectorySelected)
			{
				break;
			}

			FConversionSettings TempSettings;
			TempSettings.OutputDirectory = *SelectedDirectory;
			TempSettings.OutputFileName = L"Test"; // Dummy filename for validation

			if (TempSettings.IsDirectoryInAnyModule())
			{
				bValidDirectory = true;

				// Save the selected directory to settings
				USourcifySettings* MutableSettings = GetMutableDefault<USourcifySettings>();
				if (MutableSettings)
				{
					FString ConfigFilePath = MutableSettings->GetDefaultConfigFilename();
					if (ISourceControlModule::Get().IsEnabled())
					{
						ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
						FSourceControlStatePtr FileState = SourceControlProvider.GetState(ConfigFilePath, EStateCacheUsage::ForceUpdate);
						if (FileState.IsValid() && FileState->CanCheckout())
						{
							SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), ConfigFilePath);
						}
					}

					FPaths::MakePathRelativeTo(SelectedDirectory, *FPaths::ProjectDir());
					MutableSettings->DefaultCodeExportPath.Path = SelectedDirectory;
					if (!MutableSettings->TryUpdateDefaultConfigFile())
					{
						FMessageDialog::Open(
							EAppMsgCategory::Error,
							EAppMsgType::Ok,
							LOCTEXT("FailedToSaveSettingsMessage", "Failed to save Sourcify settings to the config file. The file may be read-only or locked by another process."),
							LOCTEXT("FailedToSaveSettingsTitle", "Failed to Save Settings")
						);
					}
				}
			}
			else
			{
				FText ErrorTitle = LOCTEXT("InvalidDirectoryTitle", "Invalid Directory");
				FText ErrorMessage = LOCTEXT("InvalidDirectoryMessage",
					"The selected directory is not within any module.\n\n"
					"Would you like to:\n"
					"- Select a different directory (Yes)\n"
					"- Skip setting a default path (No)");

				EAppReturnType::Type RetryResponse = FMessageDialog::Open(
					EAppMsgType::YesNo,
					ErrorMessage,
					ErrorTitle
				);

				if (RetryResponse == EAppReturnType::No)
				{
					break;
				}
			}
		}
	}
}

#if SOURCIFY_DEBUG
void FSourcifyModule::ScanBlueprint(const TArray<struct FAssetData>& SelectedAssets)
{
	IsSourcifying = true;

	TArray<FModuleStatus> ModuleStatuses;
	FModuleManager::Get().QueryModules(ModuleStatuses);
	FString GameModuleName;
	for (const FModuleStatus& ModuleStatus : ModuleStatuses)
	{
		if (ModuleStatus.bIsGameModule)
		{
			GameModuleName = ModuleStatus.Name;
			break;
		}
	}

	const FString OutputDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / TEXT("Source") / GameModuleName);
	const FString OutputFilename = TEXT("Test");

	FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*(OutputDir / OutputFilename + TEXT(".h")));
	FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*(OutputDir / OutputFilename + TEXT(".cpp")));

	const USourcifySettings* SourcifySettings = GetDefault<USourcifySettings>();
	SourcifySettings->ToSourcifyCore(SC->GetCoreSettings());
	const USourcifyDevOnlySettings* SourcifyDevOnlySettings = GetDefault<USourcifyDevOnlySettings>();
	SourcifyDevOnlySettings->ToSourcifyCore(SC->GetCoreSettings());

	const Sourcify::BlueprintDependencies& Deps = SC->PreSourcify(FSourcifyCoreUEWrapper::Get().Wrap(*SelectedAssets[0].GetAsset()), IgnoreConversionSettingsCache::Yes);

	auto SetNodeConversionPathAndFile = [this, SourcifyDevOnlySettings](auto&& SetNodeConversionPathAndFile, Sourcify::DependencyNode& DepNode, const FString& Dir, const FString& File) -> void
		{
			DepNode.ConversionSettings.OutputDirectory = *Dir;
			DepNode.ConversionSettings.OutputFileName = *File;

			if (SourcifyDevOnlySettings->AddSourcifiedSuffix)
			{
				// Conversion settings can be refreshed from BP edits when the Sourcify Graph is up. So make sure we don't keep appending debug suffix
				if (DepNode.ConversionSettings.TypeName.find(*SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX) != DepNode.ConversionSettings.TypeName.size() - SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX.Len())
				{
					DepNode.ConversionSettings.TypeName.append(*SourcifyModuleInternal::SOURCIFIED_TYPE_SUFFIX);
				}
			}
				
			for (Sourcify::DependencyNode::ReferenceInfo& Referencee : DepNode.Referencees)
			{
				SetNodeConversionPathAndFile(SetNodeConversionPathAndFile, *Referencee.Node, Dir, File);
			}
		};

	SetNodeConversionPathAndFile(SetNodeConversionPathAndFile, *Deps.DependencyGraph, OutputDir, OutputFilename);

	SourcifyResults = SC->Sourcify(nullptr);
	ReportSourcifyResults();
}
#endif

TSharedRef<SDockTab> FSourcifyModule::SpawnSourcifyGraphTab(const FSpawnTabArgs& Args)
{
	TSharedRef<SDockTab> NewTab = SAssignNew(SourcifyGraphTab, SDockTab)
		.TabRole(ETabRole::NomadTab);

	NewTab->SetContent(SAssignNew(SourcifyGraphUI, SSourcifyGraph).OwnerTab(&NewTab.Get()));

	return NewTab;
}

TSharedRef<SDockTab> FSourcifyModule::SpawnSourcifyResultsTab(const FSpawnTabArgs& Args)
{
	TSharedRef<SDockTab> NewTab = SAssignNew(SourcifyResultsTab, SDockTab)
		.TabRole(ETabRole::NomadTab);

	NewTab->SetContent(SNew(SSourcifyResults)
						.OwnerTab(&NewTab.Get()).SourcifyResults(&this->SourcifyResults).SourcifyCore(SC));

	return NewTab;
}

Sourcify::Results FSourcifyModule::Sourcify()
{
	IsSourcifying = true;

	const USourcifySettings* SourcifySettings = GetDefault<USourcifySettings>();
	SourcifySettings->ToSourcifyCore(SC->GetCoreSettings());
#if SOURCIFY_DEBUG
	const USourcifyDevOnlySettings* SourcifyDevOnlySettings = GetDefault<USourcifyDevOnlySettings>();
	SourcifyDevOnlySettings->ToSourcifyCore(SC->GetCoreSettings());
#endif
	SourcifyResults = SC->Sourcify([this](Sourcify::Results Result) { OnSourcifyingComplete.Broadcast(Result); });
	ReportSourcifyResults();

	return SourcifyResults;
}

void FSourcifyModule::ReportSourcifyResults() const
{
	FMessageLog SourcifyMessageLog{ SourcifyModuleInternal::SourcifyMessageLogName };

	FNotificationInfo Info(FText::GetEmpty());
	Info.ExpireDuration = 5.0f;
	Info.bUseThrobber = false;
	TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
	
	if (SourcifyResults.Succeeded())
	{
		FString Message = SourcifyResults.Type == Sourcify::Results::EResultType::Success ? TEXT("Sourcify Succeeded!") : TEXT("Sourcify Succeeded with no action!");

		UE_LOG(LogSourcify, Log, TEXT("%s"), *Message);

		NotificationItem->SetText(FText::FromString(Message));
		NotificationItem->SetCompletionState(SNotificationItem::ECompletionState::CS_Success);

		SourcifyMessageLog.Info()->AddToken(FTextToken::Create(FText::FromString(FDateTime::Now().ToString())))->AddToken(FTextToken::Create(FText::FromString(TEXT(" - ") + Message)));

		if (!SourcifyResults.NonSourcifiedAssetIniReferences.empty())
		{
			SourcifyMessageLog.Info()->AddToken(FTextToken::Create(FText::FromString(TEXT("The following Sourcified assets still being referenced in INI files and require manual clean-up:"))));
			for (const auto& [TypeName, IniReferences] : SourcifyResults.NonSourcifiedAssetIniReferences)
			{
				SourcifyMessageLog.Info()->AddToken(FAssetNameToken::Create(TypeName.c_str(), FText::FromString(FString::Printf(TEXT(" - %s"), TypeName.c_str()))));
				for (const auto& IniReference : IniReferences)
				{
					SourcifyMessageLog.Info()->AddToken(FTextToken::Create(FText::FromString(FString::Printf(TEXT("\t- %s: [%s]\n\t\t\t%s"), IniReference.Ini.c_str(), IniReference.Section.c_str(), IniReference.Key.c_str()))));
				}
			}
		}
	}
	else
	{
		UE_LOG(LogSourcify, Error, TEXT("Sourcify Failed!"));
		UE_LOG(LogSourcify, Error, TEXT("%s"), SourcifyResults.Message.c_str());

		NotificationItem->SetText(FText::FromString(TEXT("Sourcify Failed!")));
		NotificationItem->SetSubText(FText::FromString(SourcifyResults.Message.c_str()));
		NotificationItem->SetCompletionState(SNotificationItem::ECompletionState::CS_Fail);

		SourcifyMessageLog.Error()->AddToken(FTextToken::Create(FText::FromString(FDateTime::Now().ToString())))->AddToken(FTextToken::Create(FText::FromString(FString::Printf(TEXT(" - Sourcify Failed: %s"), SourcifyResults.Message.c_str()))));
		SourcifyMessageLog.Open();
	}

	if (TSharedPtr<SDockTab> NewTab = FGlobalTabmanager::Get()->TryInvokeTab(SourcifyResultsTabName))
	{
		NewTab->SetContent(SNew(SSourcifyResults)
						.OwnerTab(NewTab.Get()).SourcifyResults(&this->SourcifyResults).SourcifyCore(SC));
	}

	PreSourcifyDependencies = nullptr;
	IsSourcifying = false;
}

Sourcify::LicenseStatus FSourcifyModule::GetLicenseStatus() const
{
	return SC->GetLicenseStatus();
}

FString FSourcifyModule::GetLicenseStatusMessage() const
{
	return SC->GetLicenseStatusMessage().c_str();
}

Sourcify::LicenseStatus FSourcifyModule::ActivateLicense(const FString& InKey) const
{
	return SC->ActivateLicense(TCHAR_TO_WCHAR(*InKey));
}

Sourcify::LicenseStatus FSourcifyModule::DeactivateCurrentLicense() const
{
	return SC->DeactivateCurrentLicense();
}

Sourcify::LicenseStatus FSourcifyModule::CheckLicense() const
{
	return SC->CheckLicense();
}

Sourcify::LicenseHolderInfo FSourcifyModule::GetLicenseHolderInfo() const
{
	return SC->GetLicenseHolderInfo();
}

bool FSourcifyModule::IsLicenseActionAllowed() const
{
	return SC->IsLicenseActionAllowed();
}

bool FSourcifyModule::IsTrialSourcificationAllowed() const
{
	if (!PreSourcifyDependencies || !PreSourcifyDependencies->IsValid())
	{
		return false;
	}
	return SC->IsTrialSourcificationAllowed(*PreSourcifyDependencies->DependencyGraph);
}

void FSourcifyModule::RefreshCurrentGraph()
{
	if (!SourcifyGraphUI.IsValid())
	{
		return;
	}

	if (SC->IsPreSourcifying() || IsSourcifying || !AssetBeingSourcified.IsValid())
	{
		return;
	}

	const Sourcify::BlueprintDependencies& Deps = SC->PreSourcify(FSourcifyCoreUEWrapper::Get().Wrap(*AssetBeingSourcified.Get()));
	PreSourcifyDependencies = &Deps;
	SourcifyGraphUI.Pin()->FromDependencies(*PreSourcifyDependencies);
	
}

bool FSourcifyModule::IsNewVersionAvailable() const
{
	return NewVersionAvailable;
}

void FSourcifyModule::CheckPluginVersion()
{
	// Check if we've already checked within the last 24 hours
	const FString TimestampFilePath = FPaths::Combine(FPlatformProcess::UserTempDir(), TEXT("Sourcify"), TEXT("LastVersionCheck.txt"));
	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
	
	TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("Sourcify"));
	FString CurrentPluginVersion;
	
	if (Plugin.IsValid())
	{
		const FPluginDescriptor& Descriptor = Plugin->GetDescriptor();
		FString FullVersionName = Descriptor.VersionName;
		
		int32 PipeIndex = FullVersionName.Find(TEXT("|"));
		if (PipeIndex != INDEX_NONE)
		{
			CurrentPluginVersion = FullVersionName.Left(PipeIndex).TrimEnd();
		}
		else
		{
			CurrentPluginVersion = FullVersionName;
		}
		
		UE_LOG(LogSourcify, Log, TEXT("Current Sourcify version: %s"), *CurrentPluginVersion);
	}
	
	// Check if timestamp file exists and read it
	if (PlatformFile.FileExists(*TimestampFilePath))
	{
		FString FileContent;
		if (FFileHelper::LoadFileToString(FileContent, *TimestampFilePath))
		{
			TArray<FString> Lines;
			FileContent.ParseIntoArrayLines(Lines);
			
			if (Lines.Num() >= 2)
			{
				FDateTime LastCheckTime;
				if (FDateTime::Parse(Lines[0], LastCheckTime))
				{
					FTimespan TimeSinceLastCheck = FDateTime::UtcNow() - LastCheckTime;
					double HoursSinceLastCheck = TimeSinceLastCheck.GetTotalHours();
					if (HoursSinceLastCheck < 24.0)
					{
						UE_LOG(LogSourcify, Log, TEXT("Skipping version check - last checked %.1f hours ago"), HoursSinceLastCheck);
						
						// Use cached version for comparison
						FString CachedLatestVersion = Lines[1].TrimStartAndEnd();
						if (!CurrentPluginVersion.IsEmpty() && !CachedLatestVersion.IsEmpty() && CachedLatestVersion != CurrentPluginVersion)
						{
							NewVersionAvailable = true;
							UE_LOG(LogSourcify, Log, TEXT("Using cached version info: New version available: %s (current: %s)"), *CachedLatestVersion, *CurrentPluginVersion);
						}
						return;
					}
				}
			}
		}
	}
	
	TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
	HttpRequest->SetURL(TEXT("https://codemongous.com/download"));
	HttpRequest->SetVerb(TEXT("GET"));
	
	HttpRequest->OnProcessRequestComplete().BindLambda([this, CurrentPluginVersion, TimestampFilePath](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
	{
		if (bSuccess && Response.IsValid())
		{
			FString ResponseBody = Response->GetContentAsString();
			
			const FString VersionPrefix = TEXT(">Version ");
			int32 VersionIndex = ResponseBody.Find(VersionPrefix);
			
			if (VersionIndex != INDEX_NONE)
			{
				int32 StartIndex = VersionIndex + VersionPrefix.Len();
				
				int32 EndIndex = ResponseBody.Find(TEXT("<"), ESearchCase::IgnoreCase, ESearchDir::FromStart, StartIndex);
				
				if (EndIndex != INDEX_NONE && EndIndex > StartIndex)
				{
					FString VersionString = ResponseBody.Mid(StartIndex, EndIndex - StartIndex).TrimStartAndEnd();
					
					UE_LOG(LogSourcify, Log, TEXT("Found version: %s"), *VersionString);
					
					// Update timestamp file with both timestamp and latest version
					IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
					FString TimestampDir = FPaths::GetPath(TimestampFilePath);
					if (!PlatformFile.DirectoryExists(*TimestampDir))
					{
						PlatformFile.CreateDirectoryTree(*TimestampDir);
					}
					
					// Store timestamp on first line, version on second line
					FString FileContent = FDateTime::UtcNow().ToString() + LINE_TERMINATOR + VersionString;
					FFileHelper::SaveStringToFile(FileContent, *TimestampFilePath);
					
					if (!CurrentPluginVersion.IsEmpty() && VersionString > CurrentPluginVersion)
					{
						NewVersionAvailable = true;
						UE_LOG(LogSourcify, Warning, TEXT("New version available: %s (current: %s)"), *VersionString, *CurrentPluginVersion);
					}
				}
				else
				{
					UE_LOG(LogSourcify, Warning, TEXT("Failed to parse version string from response"));
				}
			}
			else
			{
				UE_LOG(LogSourcify, Warning, TEXT("Version string not found in response"));
			}
		}
		else
		{
			UE_LOG(LogSourcify, Warning, TEXT("Failed to check for plugin updates"));
		}
	});
	
	HttpRequest->ProcessRequest();
}

IMPLEMENT_MODULE(FSourcifyModule, Sourcify)