#include "ZipFileFunctionLibrary.h"
#include "ZipUtilityPrivatePCH.h"

#include "ZipFileFunctionInternalCallback.h"
#include "ListCallback.h"
#include "ProgressCallback.h"
#include "Runtime/Projects/Public/Interfaces/IPluginManager.h"
#include "WFULambdaRunnable.h"
#include "ZULambdaDelegate.h"
#include "SevenZipCallbackHandler.h"
#include "WindowsFileUtilityFunctionLibrary.h"

#include "7zpp.h"

using namespace SevenZip;

//Private Namespace
namespace{

	//Threaded Lambda convenience wrappers - Task graph is only suitable for short duration lambdas, but doesn't incur thread overhead
	FGraphEventRef RunLambdaOnAnyThread(TFunction< void()> InFunction)
	{
		return FFunctionGraphTask::CreateAndDispatchWhenReady(InFunction, TStatId(), nullptr, ENamedThreads::AnyThread);
	}

	//Uses proper threading, for any task that may run longer than about 2 seconds.
	void RunLongLambdaOnAnyThread(TFunction< void()> InFunction)
	{
		WFULambdaRunnable::RunLambdaOnBackGroundThread(InFunction);
	}

	// Run the lambda on the queued threadpool
	IQueuedWork* RunLambdaOnThreadPool(TFunction< void()> InFunction)
	{
		return WFULambdaRunnable::AddLambdaToQueue(InFunction);
	}

	//Private static vars
	SevenZipLibrary SZLib;

	//Utility functions
	FString PluginRootFolder()
	{
		return IPluginManager::Get().FindPlugin("ZipUtility")->GetBaseDir();
		//return FPaths::ConvertRelativePathToFull(FPaths::GameDir());
	}

	FString DLLPath()
	{

#if defined(_WIN64)

		FString PlatformString = FString(TEXT("Win64"));
#else
		FString PlatformString = FString(TEXT("Win32"));
#endif
		//Swap these to change which license you wish to fall under for zip-utility

		FString DLLString = FString("7z.dll");		//Using 7z.dll: GNU LGPL + unRAR restriction
		//FString dllString = FString("7za.dll");	//Using 7za.dll: GNU LGPL license, crucially doesn't support .zip out of the box

		return FPaths::ConvertRelativePathToFull(FPaths::Combine(*PluginRootFolder(), TEXT("ThirdParty/7zpp/dll"), *PlatformString, *DLLString));
	}

	FString ReversePathSlashes(FString forwardPath)
	{
		return forwardPath.Replace(TEXT("/"), TEXT("\\"));
	}

	bool IsValidDirectory(FString& Directory, FString& FileName, const FString& Path)
	{
		bool Found = Path.Split(TEXT("/"), &Directory, &FileName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
		//try a back split
		if (!Found)
		{
			Found = Path.Split(TEXT("\\"), &Directory, &FileName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
		}

		//No valid Directory found
		if (!Found)
			return false;
		else
			return true;
	}

	SevenZip::CompressionLevelEnum libZipLevelFromUELevel(ZipUtilityCompressionLevel ueLevel) {
		switch (ueLevel)
		{
		case COMPRESSION_LEVEL_NONE:
			return SevenZip::CompressionLevel::None;
		case COMPRESSION_LEVEL_FAST:
			return SevenZip::CompressionLevel::Fast;
		case COMPRESSION_LEVEL_NORMAL:
			return SevenZip::CompressionLevel::Normal;
		default:
			return SevenZip::CompressionLevel::None;
		}
	}

	SevenZip::CompressionFormatEnum libZipFormatFromUEFormat(EZipUtilityCompressionFormat UeFormat) {
		switch (UeFormat)
		{
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN:
			return CompressionFormat::Unknown;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_SEVEN_ZIP:
			return CompressionFormat::SevenZip;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_ZIP:
			return CompressionFormat::Zip;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_GZIP:
			return CompressionFormat::GZip;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_BZIP2:
			return CompressionFormat::BZip2;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_RAR:
			return CompressionFormat::Rar;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_TAR:
			return CompressionFormat::Tar;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_ISO:
			return CompressionFormat::Iso;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_CAB:
			return CompressionFormat::Cab;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_LZMA:
			return CompressionFormat::Lzma;
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_LZMA86:
			return CompressionFormat::Lzma86;
		default:
			return CompressionFormat::Unknown;
		}
	}

	FString defaultExtensionFromUEFormat(EZipUtilityCompressionFormat ueFormat) 
	{
		switch (ueFormat)
		{
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN:
			return FString(TEXT(".dat"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_SEVEN_ZIP:
			return FString(TEXT(".7z"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_ZIP:
			return FString(TEXT(".zip"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_GZIP:
			return FString(TEXT(".gz"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_BZIP2:
			return FString(TEXT(".bz2"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_RAR:
			return FString(TEXT(".rar"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_TAR:
			return FString(TEXT(".tar"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_ISO:
			return FString(TEXT(".iso"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_CAB:
			return FString(TEXT(".cab"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_LZMA:
			return FString(TEXT(".lzma"));
		case EZipUtilityCompressionFormat::COMPRESSION_FORMAT_LZMA86:
			return FString(TEXT(".lzma86"));
		default:
			return FString(TEXT(".dat"));
		}
	}

	using namespace std;

	

	//Background Thread convenience functions
	UZipOperation* UnzipFilesOnBGThreadWithFormat(const TArray<int32> FileIndices, const FString& ArchivePath, const FString& DestinationDirectory, const UObject* ProgressDelegate, EZipUtilityCompressionFormat Format)
	{
		UZipOperation* ZipOperation = NewObject<UZipOperation>();

		IQueuedWork* Work = RunLambdaOnThreadPool([ProgressDelegate, FileIndices, ArchivePath, DestinationDirectory, Format, ZipOperation] 
		{
			SevenZipCallbackHandler PrivateCallback;
			PrivateCallback.ProgressDelegate = (UObject*)ProgressDelegate;
			ZipOperation->SetCallbackHandler(&PrivateCallback);

			//UE_LOG(LogClass, Log, TEXT("path is: %s"), *path);
			SevenZipExtractor Extractor(SZLib, *ArchivePath);


			if (Format == EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN)
			{
				if (!Extractor.DetectCompressionFormat())
				{
					UE_LOG(LogTemp, Log, TEXT("auto-compression detection did not succeed, passing in unknown format to 7zip library."));
				}
			}
			else
			{
				Extractor.SetCompressionFormat(libZipFormatFromUEFormat(Format));
			}

			// Extract indices
			const int32 NumberFiles = FileIndices.Num(); 
			unsigned int* Indices = new unsigned int[NumberFiles]; 

			for (int32 idx = 0; idx < NumberFiles; idx++)
			{
				Indices[idx] = FileIndices[idx]; 
			}
			
			// Perform the extraction
			Extractor.ExtractFilesFromArchive(Indices, NumberFiles, *DestinationDirectory, &PrivateCallback);

			// Clean up the indices
			delete Indices;

			// Null out the callback handler now that we're exiting
			ZipOperation->SetCallbackHandler(nullptr);
		});

		ZipOperation->SetThreadPoolWorker(Work);
		return ZipOperation;
	}

	//Background Thread convenience functions
	UZipOperation* UnzipOnBGThreadWithFormat(const FString& ArchivePath, const FString& DestinationDirectory, const UObject* ProgressDelegate, EZipUtilityCompressionFormat Format)
	{
		UZipOperation* ZipOperation = NewObject<UZipOperation>();

		IQueuedWork* Work = RunLambdaOnThreadPool([ProgressDelegate, ArchivePath, DestinationDirectory, Format, ZipOperation] 
		{
			SevenZipCallbackHandler PrivateCallback;
			PrivateCallback.ProgressDelegate = (UObject*)ProgressDelegate;
			ZipOperation->SetCallbackHandler(&PrivateCallback);

			//UE_LOG(LogClass, Log, TEXT("path is: %s"), *path);
			SevenZipExtractor Extractor(SZLib, *ArchivePath);

			if (Format == EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN)
			{
				if (!Extractor.DetectCompressionFormat())
				{
					UE_LOG(LogTemp, Log, TEXT("auto-compression detection did not succeed, passing in unknown format to 7zip library."));
				}
			}
			else
			{
				Extractor.SetCompressionFormat(libZipFormatFromUEFormat(Format));
			}

			Extractor.ExtractArchive(*DestinationDirectory, &PrivateCallback);

			// Null out the callback handler now that we're exiting
			ZipOperation->SetCallbackHandler(nullptr);
		});

		ZipOperation->SetThreadPoolWorker(Work);
		return ZipOperation;
	}

	void ListOnBGThread(const FString& Path, const FString& Directory, const UObject* ListDelegate, EZipUtilityCompressionFormat Format)
	{
		//RunLongLambdaOnAnyThread - this shouldn't take long, but if it lags, swap the lambda methods
		RunLambdaOnAnyThread([ListDelegate, Path, Format, Directory] {
			SevenZipCallbackHandler PrivateCallback;
			PrivateCallback.ProgressDelegate = (UObject*)ListDelegate;
			SevenZipLister Lister(SZLib, *Path);

			if (Format == EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN)
			{
				if (!Lister.DetectCompressionFormat())
				{
					UE_LOG(LogTemp, Log, TEXT("auto-compression detection did not succeed, passing in unknown format to 7zip library."));
				}
			}
			else
			{
				Lister.SetCompressionFormat(libZipFormatFromUEFormat(Format));
			}

			if (!Lister.ListArchive(&PrivateCallback))
			{
				// If ListArchive returned false, it was most likely because the compression format was unsupported
				// Call OnDone with a failure message, make sure to call this on the game thread.
				if (IZipUtilityInterface* ZipInterface = Cast<IZipUtilityInterface>((UObject*)ListDelegate))
				{
					UE_LOG(LogClass, Warning, TEXT("ZipUtility: Unknown failure for list operation on %s"), *Path);
					UZipFileFunctionLibrary::RunLambdaOnGameThread([ZipInterface, ListDelegate, Path] 
					{
						ZipInterface->Execute_OnDone((UObject*)ListDelegate, *Path, EZipUtilityCompletionState::FAILURE_UNKNOWN);
					});
				}
			}
		});
	}

	UZipOperation* ZipOnBGThread(const FString& Path, const FString& FileName, const FString& Directory, const UObject* ProgressDelegate, EZipUtilityCompressionFormat UeCompressionformat, ZipUtilityCompressionLevel UeCompressionlevel)
	{
		UZipOperation* ZipOperation = NewObject<UZipOperation>();

		IQueuedWork* Work = RunLambdaOnThreadPool([ProgressDelegate, FileName, Path, UeCompressionformat, UeCompressionlevel, Directory, ZipOperation] 
		{
			SevenZipCallbackHandler PrivateCallback;
			PrivateCallback.ProgressDelegate = (UObject*)ProgressDelegate;
			ZipOperation->SetCallbackHandler(&PrivateCallback);

			//Set the zip format
			EZipUtilityCompressionFormat UeFormat = UeCompressionformat;

			if (UeFormat == EZipUtilityCompressionFormat::COMPRESSION_FORMAT_UNKNOWN)
			{
				UeFormat = EZipUtilityCompressionFormat::COMPRESSION_FORMAT_ZIP;
			}
			//Disallow creating .rar archives as per unrar restriction, this won't work anyway so redirect to 7z
			else if (UeFormat == EZipUtilityCompressionFormat::COMPRESSION_FORMAT_RAR)
			{
				UE_LOG(LogClass, Warning, TEXT("ZipUtility: Rar compression not supported for creating archives, re-targeting as 7z."));
				UeFormat = EZipUtilityCompressionFormat::COMPRESSION_FORMAT_SEVEN_ZIP;
			}
			
			//concatenate the output filename
			FString OutputFileName = FString::Printf(TEXT("%s/%s%s"), *Directory, *FileName, *defaultExtensionFromUEFormat(UeFormat));
			//UE_LOG(LogClass, Log, TEXT("\noutputfile is: <%s>\n path is: <%s>"), *outputFileName, *path);
			
			SevenZipCompressor compressor(SZLib, *ReversePathSlashes(OutputFileName));
			compressor.SetCompressionFormat(libZipFormatFromUEFormat(UeFormat));
			compressor.SetCompressionLevel(libZipLevelFromUELevel(UeCompressionlevel));

			if (PathIsDirectory(*Path))
			{
				//UE_LOG(LogClass, Log, TEXT("Compressing Folder"));
				compressor.CompressDirectory(*ReversePathSlashes(Path), &PrivateCallback);
			}
			else
			{
				//UE_LOG(LogClass, Log, TEXT("Compressing File"));
				compressor.CompressFile(*ReversePathSlashes(Path), &PrivateCallback);
			}

			// Null out the callback handler
			ZipOperation->SetCallbackHandler(nullptr);
			//Todo: expand to support zipping up contents of current folder
			//compressor.CompressFiles(*ReversePathSlashes(path), TEXT("*"),  &PrivateCallback);
		});
		ZipOperation->SetThreadPoolWorker(Work);
		return ZipOperation;
	}

}//End private namespace

UZipFileFunctionLibrary::UZipFileFunctionLibrary(const class FObjectInitializer& PCIP)
	: Super(PCIP)
{
	UE_LOG(LogTemp, Log, TEXT("DLLPath is: %s"), *DLLPath());
	SZLib.Load(*DLLPath());
}

UZipFileFunctionLibrary::~UZipFileFunctionLibrary()
{
	SZLib.Free();
}

bool UZipFileFunctionLibrary::UnzipFileNamed(const FString& archivePath, const FString& Name, UObject* ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat format /*= COMPRESSION_FORMAT_UNKNOWN*/)
{	
	UZipFileFunctionInternalCallback* InternalCallback = NewObject<UZipFileFunctionInternalCallback>();
	InternalCallback->SetFlags(RF_MarkAsRootSet);
	InternalCallback->SetCallback(Name, ZipUtilityInterfaceDelegate, format);

	ListFilesInArchive(archivePath, InternalCallback, format);

	return true;
}

bool UZipFileFunctionLibrary::UnzipFileNamedTo(const FString& archivePath, const FString& Name, const FString& destinationPath, UObject* ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat format /*= COMPRESSION_FORMAT_UNKNOWN*/)
{
	UZipFileFunctionInternalCallback* InternalCallback = NewObject<UZipFileFunctionInternalCallback>();
	InternalCallback->SetFlags(RF_MarkAsRootSet);
	InternalCallback->SetCallback(Name, destinationPath, ZipUtilityInterfaceDelegate, format);

	ListFilesInArchive(archivePath, InternalCallback, format);

	return true;
}

UZipOperation* UZipFileFunctionLibrary::UnzipFilesTo(const TArray<int32> fileIndices, const FString & archivePath, const FString & destinationPath, UObject * ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat format)
{
	bool bObjectIsValid = ZipUtilityInterfaceDelegate->GetClass()->ImplementsInterface(UZipUtilityInterface::StaticClass());

	if (!bObjectIsValid)
	{
		UE_LOG(LogTemp, Warning, TEXT("Object passed as Delegate does not respond to IZipUtilityInterface"));
		return nullptr;
	}

	return UnzipFilesOnBGThreadWithFormat(fileIndices, archivePath, destinationPath, ZipUtilityInterfaceDelegate, format);
}

UZipOperation* UZipFileFunctionLibrary::UnzipFiles(const TArray<int32> fileIndices, const FString & ArchivePath, UObject * ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat format)
{
	FString Directory;
	FString FileName;

	//Check Directory validity
	if (!IsValidDirectory(Directory, FileName, ArchivePath))
	{
		return nullptr;
	}
		

	if (fileIndices.Num() == 0)
	{
		return nullptr;
	}

	return UnzipFilesTo(fileIndices, ArchivePath, Directory, ZipUtilityInterfaceDelegate, format);
}

UZipOperation* UZipFileFunctionLibrary::Unzip(const FString& ArchivePath, UObject* ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat Format /*= COMPRESSION_FORMAT_UNKNOWN*/)
{
	FString Directory;
	FString FileName;

	//Check Directory validity
	if (!IsValidDirectory(Directory, FileName, ArchivePath) || !UWindowsFileUtilityFunctionLibrary::DoesFileExist(ArchivePath))
	{
		bool bObjectIsValid = ZipUtilityInterfaceDelegate->GetClass()->ImplementsInterface(UZipUtilityInterface::StaticClass());

		if (!bObjectIsValid)
		{
			UE_LOG(LogTemp, Warning, TEXT("Object passed as Delegate does not respond to IZipUtilityInterface"));
			return nullptr;
		}

		((IZipUtilityInterface*)ZipUtilityInterfaceDelegate)->Execute_OnDone((UObject*)ZipUtilityInterfaceDelegate, ArchivePath, EZipUtilityCompletionState::FAILURE_NOT_FOUND);
		return nullptr;
	}

	return UnzipTo(ArchivePath, Directory, ZipUtilityInterfaceDelegate, Format);
}

UZipOperation* UZipFileFunctionLibrary::UnzipWithLambda(const FString& ArchivePath, TFunction<void()> OnDoneCallback, TFunction<void(float)> OnProgressCallback, EZipUtilityCompressionFormat Format)
{
	UZULambdaDelegate* LambdaDelegate = NewObject<UZULambdaDelegate>();
	LambdaDelegate->AddToRoot();
	LambdaDelegate->SetOnDoneCallback([LambdaDelegate, OnDoneCallback]() 
	{
		OnDoneCallback();
		LambdaDelegate->RemoveFromRoot();
	});
	LambdaDelegate->SetOnProgessCallback(OnProgressCallback);

	return Unzip(ArchivePath, LambdaDelegate, Format);
}


UZipOperation* UZipFileFunctionLibrary::UnzipTo(const FString& ArchivePath, const FString& DestinationPath, UObject* ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat Format)
{
	return UnzipOnBGThreadWithFormat(ArchivePath, DestinationPath, ZipUtilityInterfaceDelegate, Format);
}

UZipOperation* UZipFileFunctionLibrary::Zip(const FString& ArchivePath, UObject* ZipUtilityInterfaceDelegate, EZipUtilityCompressionFormat Format, TEnumAsByte<ZipUtilityCompressionLevel> Level)
{
	FString Directory;
	FString FileName;

	bool bObjectIsValid = ZipUtilityInterfaceDelegate->GetClass()->ImplementsInterface(UZipUtilityInterface::StaticClass());

	if (!bObjectIsValid)
	{
		UE_LOG(LogTemp, Warning, TEXT("Object passed as Delegate does not respond to IZipUtilityInterface"));
		return nullptr;
	}

	//Check Directory and File validity
	if (!IsValidDirectory(Directory, FileName, ArchivePath) || !UWindowsFileUtilityFunctionLibrary::DoesFileExist(ArchivePath))
	{
		((IZipUtilityInterface*)ZipUtilityInterfaceDelegate)->Execute_OnDone((UObject*)ZipUtilityInterfaceDelegate, ArchivePath, EZipUtilityCompletionState::FAILURE_NOT_FOUND);
		return nullptr;
	}

	return ZipOnBGThread(ArchivePath, FileName, Directory, ZipUtilityInterfaceDelegate, Format, Level);
}

UZipOperation* UZipFileFunctionLibrary::ZipWithLambda(const FString& ArchivePath, TFunction<void()> OnDoneCallback, TFunction<void(float)> OnProgressCallback /*= nullptr*/, EZipUtilityCompressionFormat Format /*= COMPRESSION_FORMAT_UNKNOWN*/, TEnumAsByte<ZipUtilityCompressionLevel> Level /*=COMPRESSION_LEVEL_NORMAL*/)
{
	UZULambdaDelegate* LambdaDelegate = NewObject<UZULambdaDelegate>();
	LambdaDelegate->AddToRoot();
	LambdaDelegate->SetOnDoneCallback([OnDoneCallback, LambdaDelegate]() 
	{
		OnDoneCallback();
		LambdaDelegate->RemoveFromRoot();
	});
	LambdaDelegate->SetOnProgessCallback(OnProgressCallback);

	return Zip(ArchivePath, LambdaDelegate, Format);
}

bool UZipFileFunctionLibrary::ListFilesInArchive(const FString& path, UObject* ListDelegate, EZipUtilityCompressionFormat format)
{
	FString Directory;
	FString FileName;

	//Check Directory validity
	if (!IsValidDirectory(Directory, FileName, path))
	{
		return false;
	}

	ListOnBGThread(path, Directory, ListDelegate, format);
	return true;
}

FGraphEventRef UZipFileFunctionLibrary::RunLambdaOnGameThread(TFunction< void()> InFunction)
{
	return FFunctionGraphTask::CreateAndDispatchWhenReady(InFunction, TStatId(), nullptr, ENamedThreads::GameThread);
}
