#include "HyperTwistBrowser/HyperTwistBrowserWidget.h"

#include "HyperTwistBrowser/HyperTwistBrowserBridgeObject.h"
#include "Misc/Paths.h"
#include "SWebBrowser.h"

namespace HyperTwistBrowserWidgetInternal
{
	FString MakeFileUrl(const FString& AbsolutePath)
	{
		FString Normalized = FPaths::ConvertRelativePathToFull(AbsolutePath);
		FPaths::NormalizeFilename(Normalized);
		Normalized.ReplaceInline(TEXT(" "), TEXT("%20"));
		return FString::Printf(TEXT("file:///%s"), *Normalized);
	}

	FString ResolveBundledBrowserShellAbsolutePath()
	{
		return FPaths::Combine(FPaths::ProjectDir(), TEXT(".."), TEXT("Content"), TEXT("Browser"), TEXT("index.html"));
	}

	FString GetShellAuthorityFallback(const UHyperTwistBrowserWidget& BrowserWidget)
	{
		if (BrowserWidget.bUseBundledBrowserShell)
		{
			return TEXT("Content/Browser/index.html");
		}

		return BrowserWidget.InitialUrl.IsEmpty()
			? TEXT("pending-shell-selection")
			: BrowserWidget.InitialUrl;
	}

	FString MakeShellAuthorityLine(const FString& ShellAuthority)
	{
		return FString::Printf(TEXT("shell %s"), *ShellAuthority);
	}

	FString MakeQueueStatusLine(const int32 CommandCount, const int32 ShellStateCount)
	{
		return FString::Printf(
			TEXT("queue commands %d | shell states %d"),
			CommandCount,
			ShellStateCount
		);
	}

	FString MakeReadySummaryLine(const FString& RuntimeMode, const int32 AdapterCount)
	{
		return FString::Printf(TEXT("%s | adapters %d"), *RuntimeMode, AdapterCount);
	}

	FString MakeLiveSummaryLine(
		const FString& RuntimeMode,
		const FString& BridgeTrafficStatus,
		const int32 AdapterCount
	)
	{
		return FString::Printf(
			TEXT("%s | %s | adapters %d"),
			*RuntimeMode,
			*BridgeTrafficStatus,
			AdapterCount
		);
	}

	FString MakeReadyBridgeStatusLine()
	{
		return TEXT("bridge waiting for live runtime-status envelope");
	}

	FString MakeWaitingBridgeStatusLine()
	{
		return TEXT("bridge waiting for runtime-ready");
	}

	FString MakeLiveBridgeStatusLine(
		const int32 CommandReceiveCount,
		const int32 ShellStateReceiveCount
	)
	{
		return FString::Printf(
			TEXT("bridge commands %d | shell states %d"),
			CommandReceiveCount,
			ShellStateReceiveCount
		);
	}

}

void UHyperTwistBrowserWidget::LoadBundledBrowserShell()
{
	bUseBundledBrowserShell = true;
	InitialUrl = ResolveBundledBrowserShellUrl();
	ResetBrowserRuntimeHandshake();
	if (BrowserWidget.IsValid())
	{
		BrowserWidget->LoadURL(InitialUrl);
	}
}

void UHyperTwistBrowserWidget::LoadBrowserUrl(const FString& NewUrl)
{
	bUseBundledBrowserShell = false;
	InitialUrl = NewUrl;
	if (!NewUrl.IsEmpty())
	{
		ResetBrowserRuntimeHandshake();
	}

	if (BrowserWidget.IsValid() && !NewUrl.IsEmpty())
	{
		BrowserWidget->LoadURL(NewUrl);
	}
}

void UHyperTwistBrowserWidget::DispatchCommandJson(const FString& CommandJson)
{
	if (CommandJson.IsEmpty())
	{
		return;
	}

	if (!IsBrowserRuntimeDispatchReady())
	{
		QueueCommandJson(CommandJson);
		return;
	}

	ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson));
}

void UHyperTwistBrowserWidget::PushShellStateJson(const FString& StateJson)
{
	if (StateJson.IsEmpty())
	{
		return;
	}

	if (!IsBrowserRuntimeDispatchReady())
	{
		QueueShellStateJson(StateJson);
		return;
	}

	ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(StateJson));
}

bool UHyperTwistBrowserWidget::PushTilingStateEnvelope(
	const FHyperTwistTrainingTilingStateEnvelope& StateEnvelope
)
{
	FString StateJson;
	if (!UHyperTwistTrainingTilingLibrary::TrySerializeStateEnvelopeToJson(
			StateEnvelope,
			StateJson))
	{
		return false;
	}

	PushShellStateJson(StateJson);
	return true;
}

FString UHyperTwistBrowserWidget::GetLastEnvelopeJson() const
{
	return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastEnvelopeJson : FString();
}

FString UHyperTwistBrowserWidget::GetLastRuntimeReadyJson() const
{
	return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastRuntimeReadyJson : FString();
}

FString UHyperTwistBrowserWidget::GetLastStateJson() const
{
	return BrowserBridgeObject != nullptr ? BrowserBridgeObject->LastStateJson : FString();
}

int32 UHyperTwistBrowserWidget::GetPendingCommandCount() const
{
	return PendingCommandJsonQueue.Num();
}

bool UHyperTwistBrowserWidget::HasPendingShellStateJson() const
{
	return bHasPendingShellStateJson;
}

bool UHyperTwistBrowserWidget::IsBrowserRuntimeReady() const
{
	return bBrowserRuntimeReady;
}

FHyperTwistBrowserOperatorStatusSurface UHyperTwistBrowserWidget::GetBrowserOperatorStatusSurface() const
{
	FHyperTwistBrowserOperatorStatusSurface StatusSurface;
	StatusSurface.bBrowserRuntimeReady = bBrowserRuntimeReady;
	StatusSurface.PendingCommandCount = PendingCommandJsonQueue.Num();
	StatusSurface.PendingShellStateCount = bHasPendingShellStateJson ? 1 : 0;

	FHyperTwistBrowserRuntimeReadyPayload RuntimeReadyPayload;
	const bool bHasRuntimeReadyPayload =
		BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastRuntimeReadyPayload(RuntimeReadyPayload);
	StatusSurface.bHasRuntimeReadyPayload = bHasRuntimeReadyPayload;

	FHyperTwistBrowserRuntimeStatusEnvelope RuntimeStatusEnvelope;
	const bool bHasRuntimeStatusEnvelope =
		BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastBrowserRuntimeStatusEnvelope(RuntimeStatusEnvelope);
	StatusSurface.bHasRuntimeStatusEnvelope = bHasRuntimeStatusEnvelope;

	if (bHasRuntimeStatusEnvelope)
	{
		const FHyperTwistBrowserRuntimeStatusSnapshot& RuntimeStatus = RuntimeStatusEnvelope.Status;
		StatusSurface.RuntimeStageId = RuntimeStatus.RuntimeStatusId;
		StatusSurface.RuntimeMode = RuntimeStatus.RuntimeMode;
		StatusSurface.BridgeTrafficStatus = RuntimeStatus.BridgeTrafficStatus;
		StatusSurface.BootstrapStartedAtUtc = RuntimeStatus.BootstrapStartedAtUtc;
		StatusSurface.RuntimeReadyAtUtc = RuntimeStatus.RuntimeReadyAtUtc;
		StatusSurface.LastCommandReceivedAtUtc = RuntimeStatus.LastCommandReceivedAtUtc;
		StatusSurface.LastShellStateReceivedAtUtc = RuntimeStatus.LastShellStateReceivedAtUtc;
		StatusSurface.FallbackReason = RuntimeStatus.FallbackReason;
		StatusSurface.AdapterCount = RuntimeStatus.AdapterCount;
		StatusSurface.Headline = TEXT("Browser runtime live");
		StatusSurface.SummaryLine = HyperTwistBrowserWidgetInternal::MakeLiveSummaryLine(
			RuntimeStatus.RuntimeMode,
			RuntimeStatus.BridgeTrafficStatus,
			RuntimeStatus.AdapterCount
		);
		StatusSurface.QueueStatusLine = HyperTwistBrowserWidgetInternal::MakeQueueStatusLine(
			RuntimeStatus.QueuedCommandCountAtRuntimeReady,
			RuntimeStatus.QueuedShellStateCountAtRuntimeReady
		);
		StatusSurface.BridgeStatusLine = HyperTwistBrowserWidgetInternal::MakeLiveBridgeStatusLine(
			RuntimeStatus.CommandReceiveCount,
			RuntimeStatus.ShellStateReceiveCount
		);
		StatusSurface.ShellAuthorityLine = HyperTwistBrowserWidgetInternal::MakeShellAuthorityLine(
			RuntimeStatus.ShellAuthority.IsEmpty()
				? HyperTwistBrowserWidgetInternal::GetShellAuthorityFallback(*this)
				: RuntimeStatus.ShellAuthority
		);
		return StatusSurface;
	}

	if (bHasRuntimeReadyPayload || bBrowserRuntimeReady)
	{
		const FString RuntimeMode =
			(bHasRuntimeReadyPayload && !RuntimeReadyPayload.Mode.IsEmpty())
				? RuntimeReadyPayload.Mode
				: TEXT("ready");
		const int32 AdapterCount = bHasRuntimeReadyPayload ? RuntimeReadyPayload.AdapterCount : 0;
		StatusSurface.RuntimeStageId = bHasRuntimeReadyPayload
			? RuntimeReadyPayload.Status
			: TEXT("ready");
		StatusSurface.RuntimeMode = RuntimeMode;
		StatusSurface.BootstrapStartedAtUtc = TEXT("awaiting-live-runtime-status-envelope");
		StatusSurface.RuntimeReadyAtUtc = TEXT("awaiting-live-runtime-status-envelope");
		StatusSurface.LastCommandReceivedAtUtc = TEXT("awaiting-live-runtime-status-envelope");
		StatusSurface.LastShellStateReceivedAtUtc = TEXT("awaiting-live-runtime-status-envelope");
		StatusSurface.AdapterCount = AdapterCount;
		StatusSurface.Headline = TEXT("Browser runtime ready");
		StatusSurface.SummaryLine = HyperTwistBrowserWidgetInternal::MakeReadySummaryLine(
			RuntimeMode,
			AdapterCount
		);
		StatusSurface.QueueStatusLine = HyperTwistBrowserWidgetInternal::MakeQueueStatusLine(
			bHasRuntimeReadyPayload ? RuntimeReadyPayload.QueuedCommandCountAtRuntimeReady : StatusSurface.PendingCommandCount,
			bHasRuntimeReadyPayload ? RuntimeReadyPayload.QueuedShellStateCountAtRuntimeReady : StatusSurface.PendingShellStateCount
		);
		StatusSurface.BridgeStatusLine = HyperTwistBrowserWidgetInternal::MakeReadyBridgeStatusLine();
		StatusSurface.ShellAuthorityLine = HyperTwistBrowserWidgetInternal::MakeShellAuthorityLine(
			(bHasRuntimeReadyPayload && !RuntimeReadyPayload.ShellAuthority.IsEmpty())
				? RuntimeReadyPayload.ShellAuthority
				: HyperTwistBrowserWidgetInternal::GetShellAuthorityFallback(*this)
		);
		return StatusSurface;
	}

	StatusSurface.RuntimeStageId = TEXT("waiting");
	StatusSurface.RuntimeMode = TEXT("bootstrapping");
	StatusSurface.BootstrapStartedAtUtc = TEXT("pending-runtime-ready");
	StatusSurface.RuntimeReadyAtUtc = TEXT("pending-runtime-ready");
	StatusSurface.LastCommandReceivedAtUtc = TEXT("pending-runtime-ready");
	StatusSurface.LastShellStateReceivedAtUtc = TEXT("pending-runtime-ready");
	StatusSurface.Headline = TEXT("Browser runtime waiting");
	StatusSurface.SummaryLine = TEXT("awaiting runtime-ready handshake");
	StatusSurface.QueueStatusLine = HyperTwistBrowserWidgetInternal::MakeQueueStatusLine(
		StatusSurface.PendingCommandCount,
		StatusSurface.PendingShellStateCount
	);
	StatusSurface.BridgeStatusLine = HyperTwistBrowserWidgetInternal::MakeWaitingBridgeStatusLine();
	StatusSurface.ShellAuthorityLine = HyperTwistBrowserWidgetInternal::MakeShellAuthorityLine(
		HyperTwistBrowserWidgetInternal::GetShellAuthorityFallback(*this)
	);
	return StatusSurface;
}

bool UHyperTwistBrowserWidget::TryGetLastTilingEnvelope(
	FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope
) const
{
	OutStateEnvelope = FHyperTwistTrainingTilingStateEnvelope();
	return BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastTilingEnvelope(OutStateEnvelope);
}

bool UHyperTwistBrowserWidget::TryGetLastRuntimeReadyPayload(
	FHyperTwistBrowserRuntimeReadyPayload& OutRuntimeReadyPayload
) const
{
	OutRuntimeReadyPayload = FHyperTwistBrowserRuntimeReadyPayload();
	return BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastRuntimeReadyPayload(OutRuntimeReadyPayload);
}

bool UHyperTwistBrowserWidget::TryGetLastTilingStateEnvelope(
	FHyperTwistTrainingTilingStateEnvelope& OutStateEnvelope
) const
{
	OutStateEnvelope = FHyperTwistTrainingTilingStateEnvelope();
	return BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastTilingStateEnvelope(OutStateEnvelope);
}

bool UHyperTwistBrowserWidget::TryGetLastBrowserRuntimeStatusEnvelope(
	FHyperTwistBrowserRuntimeStatusEnvelope& OutStatusEnvelope
) const
{
	OutStatusEnvelope = FHyperTwistBrowserRuntimeStatusEnvelope();
	return BrowserBridgeObject != nullptr
		&& BrowserBridgeObject->TryGetLastBrowserRuntimeStatusEnvelope(OutStatusEnvelope);
}

FString UHyperTwistBrowserWidget::ResolveBundledBrowserShellUrl()
{
	return HyperTwistBrowserWidgetInternal::MakeFileUrl(
		HyperTwistBrowserWidgetInternal::ResolveBundledBrowserShellAbsolutePath()
	);
}

FString UHyperTwistBrowserWidget::ResolveBundledBrowserShellSourcePath()
{
	return HyperTwistBrowserWidgetInternal::ResolveBundledBrowserShellAbsolutePath();
}

TSharedRef<SWidget> UHyperTwistBrowserWidget::RebuildWidget()
{
	if (BrowserBridgeObject != nullptr && RuntimeReadyDelegateHandle.IsValid())
	{
		BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle);
		RuntimeReadyDelegateHandle.Reset();
	}

	EnsureBridgeObject();
	ResetBrowserRuntimeHandshake();

	BrowserWidget =
		SNew(SWebBrowser)
		.InitialURL(ResolveInitialUrl())
		.ShowControls(bShowBrowserControls);

	EnsureBridgeBound();
	return BrowserWidget.ToSharedRef();
}

void UHyperTwistBrowserWidget::ReleaseSlateResources(const bool bReleaseChildren)
{
	Super::ReleaseSlateResources(bReleaseChildren);

	if (BrowserBridgeObject != nullptr && RuntimeReadyDelegateHandle.IsValid())
	{
		BrowserBridgeObject->OnRuntimeReady.Remove(RuntimeReadyDelegateHandle);
		RuntimeReadyDelegateHandle.Reset();
	}

	if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr)
	{
		BrowserWidget->UnbindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
	}

	BrowserWidget.Reset();
}

void UHyperTwistBrowserWidget::SynchronizeProperties()
{
	Super::SynchronizeProperties();

	if (BrowserWidget.IsValid())
	{
		EnsureBridgeBound();
		const FString ResolvedUrl = ResolveInitialUrl();
		if (!ResolvedUrl.IsEmpty())
		{
			ResetBrowserRuntimeHandshake();
			BrowserWidget->LoadURL(ResolvedUrl);
		}
	}
}

bool UHyperTwistBrowserWidget::IsBrowserRuntimeDispatchReady() const
{
#if WITH_AUTOMATION_TESTS
	if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid())
	{
		return bBrowserRuntimeReady;
	}
#endif

	return BrowserWidget.IsValid() && bBrowserRuntimeReady;
}

void UHyperTwistBrowserWidget::ExecuteBrowserRuntimeScript(const FString& Script)
{
#if WITH_AUTOMATION_TESTS
	if (bForceBrowserRuntimeDispatchReadyForTesting && !BrowserWidget.IsValid())
	{
		ExecutedBrowserScriptsForTesting.Add(Script);
		return;
	}
#endif

	if (BrowserWidget.IsValid())
	{
		BrowserWidget->ExecuteJavascript(Script);
	}
}

void UHyperTwistBrowserWidget::HandleBrowserRuntimeReady(const FString& RuntimeReadyJson)
{
	if (RuntimeReadyJson.IsEmpty())
	{
		return;
	}

	bBrowserRuntimeReady = true;
	FlushPendingBrowserMessages();
}

void UHyperTwistBrowserWidget::FlushPendingBrowserMessages()
{
	if (!IsBrowserRuntimeDispatchReady())
	{
		return;
	}

	if (bHasPendingShellStateJson)
	{
		ExecuteBrowserRuntimeScript(BuildShellStateDispatchScript(PendingShellStateJson));
		PendingShellStateJson.Reset();
		bHasPendingShellStateJson = false;
	}

	for (const FString& CommandJson : PendingCommandJsonQueue)
	{
		ExecuteBrowserRuntimeScript(BuildCommandDispatchScript(CommandJson));
	}
	PendingCommandJsonQueue.Reset();
}

void UHyperTwistBrowserWidget::QueueCommandJson(const FString& CommandJson)
{
	if (!CommandJson.IsEmpty())
	{
		PendingCommandJsonQueue.Add(CommandJson);
	}
}

void UHyperTwistBrowserWidget::QueueShellStateJson(const FString& StateJson)
{
	if (!StateJson.IsEmpty())
	{
		PendingShellStateJson = StateJson;
		bHasPendingShellStateJson = true;
	}
}

void UHyperTwistBrowserWidget::ResetBrowserRuntimeHandshake()
{
	bBrowserRuntimeReady = false;

	if (BrowserBridgeObject != nullptr)
	{
		BrowserBridgeObject->ResetReceivedMessages();
	}
}

void UHyperTwistBrowserWidget::EnsureBridgeObject()
{
	if (BrowserBridgeObject == nullptr)
	{
		BrowserBridgeObject = NewObject<UHyperTwistBrowserBridgeObject>(this, TEXT("HyperTwistBrowserBridge"));
	}

	if (BrowserBridgeObject != nullptr && !RuntimeReadyDelegateHandle.IsValid())
	{
		RuntimeReadyDelegateHandle = BrowserBridgeObject->OnRuntimeReady.AddUObject(
			this,
			&UHyperTwistBrowserWidget::HandleBrowserRuntimeReady
		);
	}
}

FString UHyperTwistBrowserWidget::BuildCommandDispatchScript(const FString& CommandJson) const
{
	const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(CommandJson);
	return FString::Printf(
		TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.receiveCommand(JSON.parse('%s'));"),
		*EscapedJson
	);
}

FString UHyperTwistBrowserWidget::BuildShellStateDispatchScript(const FString& StateJson) const
{
	const FString EscapedJson = EscapeForJavaScriptSingleQuotedString(StateJson);
	return FString::Printf(
		TEXT("window.HyperTwistBrowserRuntime && window.HyperTwistBrowserRuntime.setShellState(JSON.parse('%s'));"),
		*EscapedJson
	);
}

FString UHyperTwistBrowserWidget::EscapeForJavaScriptSingleQuotedString(const FString& Input)
{
	FString Escaped = Input;
	Escaped.ReplaceInline(TEXT("\\"), TEXT("\\\\"));
	Escaped.ReplaceInline(TEXT("'"), TEXT("\\'"));
	Escaped.ReplaceInline(TEXT("\r"), TEXT("\\r"));
	Escaped.ReplaceInline(TEXT("\n"), TEXT("\\n"));
	Escaped.ReplaceInline(TEXT("\t"), TEXT("\\t"));
	return Escaped;
}

FString UHyperTwistBrowserWidget::ResolveInitialUrl() const
{
	return bUseBundledBrowserShell
		? ResolveBundledBrowserShellUrl()
		: InitialUrl;
}

void UHyperTwistBrowserWidget::EnsureBridgeBound()
{
	if (BrowserWidget.IsValid() && BrowserBridgeObject != nullptr)
	{
		BrowserWidget->BindUObject(TEXT("hypertwist"), BrowserBridgeObject, true);
	}
}

#if WITH_AUTOMATION_TESTS
void UHyperTwistBrowserWidget::SetBrowserRuntimeDispatchReadyForTesting(const bool bInDispatchReady)
{
	bForceBrowserRuntimeDispatchReadyForTesting = bInDispatchReady;
	ExecutedBrowserScriptsForTesting.Reset();
}

void UHyperTwistBrowserWidget::SimulateBrowserEnvelopeForTesting(const FString& EnvelopeJson)
{
	EnsureBridgeObject();
	if (BrowserBridgeObject != nullptr)
	{
		BrowserBridgeObject->NotifyEnvelope(EnvelopeJson);
	}
}

void UHyperTwistBrowserWidget::SimulateBrowserRuntimeReadyForTesting(const FString& RuntimeReadyJson)
{
	EnsureBridgeObject();
	if (BrowserBridgeObject != nullptr)
	{
		BrowserBridgeObject->NotifyRuntimeReady(RuntimeReadyJson);
	}
	else
	{
		HandleBrowserRuntimeReady(RuntimeReadyJson);
	}
}

const TArray<FString>& UHyperTwistBrowserWidget::GetExecutedBrowserScriptsForTesting() const
{
	return ExecutedBrowserScriptsForTesting;
}
#endif
