#include "HyperTwistReplay/HyperTwistReplayReviewLibrary.h"

#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
#include "JsonObjectConverter.h"

namespace HyperTwistReplayReviewLibraryInternal
{
	template <typename TStruct>
	bool DeserializePayload(const FString& Json, TStruct& OutValue)
	{
		if (Json.IsEmpty())
		{
			return false;
		}

		return FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0);
	}

	FHyperTwistReplayReviewAnchor MakeAnchor(
		const FHyperTwistReplayEvent& Event,
		const EHyperTwistReplayReviewAnchorKind AnchorKind,
		const FString& Label)
	{
		FHyperTwistReplayReviewAnchor Anchor;
		Anchor.AnchorKind = AnchorKind;
		Anchor.EventId = Event.EventId;
		Anchor.Sequence = Event.Sequence;
		Anchor.TimeMs = Event.TimeMs;
		Anchor.Label = Label;
		return Anchor;
	}

	void AddPhaseAnchor(
		TArray<FHyperTwistReplayReviewAnchor>& Anchors,
		const FHyperTwistReplayEvent& Event,
		const FHyperTwistReplayPhaseMarkPayload& PhasePayload)
	{
		const FString Action = PhasePayload.PhaseAction.ToLower();
		const bool bMarksCompletion = (Action == TEXT("end") || Action == TEXT("complete"));
		const FString EffectiveAction = bMarksCompletion ? TEXT("complete") : TEXT("start");
		const FString EffectiveLabel = PhasePayload.PhaseKind.IsEmpty()
			? FString::Printf(TEXT("phase %s"), *EffectiveAction)
			: FString::Printf(TEXT("%s %s"), *PhasePayload.PhaseKind, *EffectiveAction);

		FHyperTwistReplayReviewAnchor Anchor = MakeAnchor(
			Event,
			EHyperTwistReplayReviewAnchorKind::PhaseBoundary,
			EffectiveLabel);
		Anchor.PhaseId = PhasePayload.PhaseId;
		Anchor.PhaseKind = PhasePayload.PhaseKind;
		Anchor.bMarksCompletion = bMarksCompletion;
		Anchors.Add(Anchor);
	}
}

TArray<FHyperTwistReplayReviewAnchor> UHyperTwistReplayReviewLibrary::CollectReplayReviewAnchors(const FHyperTwistReplayPacket& Packet)
{
	TArray<FHyperTwistReplayReviewAnchor> Anchors;
	bool bFirstMoveAnchorAdded = false;

	for (const FHyperTwistReplayEvent& Event : Packet.Events)
	{
		switch (Event.EventType)
		{
		case EHyperTwistReplayEventType::InspectionStart:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::InspectionStart,
				TEXT("inspection start")));
			break;

		case EHyperTwistReplayEventType::InspectionEnd:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::InspectionEnd,
				TEXT("inspection end")));
			break;

		case EHyperTwistReplayEventType::TimerStart:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::TimerStart,
				TEXT("timer start")));
			break;

		case EHyperTwistReplayEventType::Move:
			if (!bFirstMoveAnchorAdded)
			{
				Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
					Event,
					EHyperTwistReplayReviewAnchorKind::FirstMove,
					TEXT("first move")));
				bFirstMoveAnchorAdded = true;
			}
			break;

		case EHyperTwistReplayEventType::PhaseMark:
		{
			FHyperTwistReplayPhaseMarkPayload PhasePayload;
			if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, PhasePayload)
				&& !PhasePayload.PhaseId.IsEmpty())
			{
				HyperTwistReplayReviewLibraryInternal::AddPhaseAnchor(Anchors, Event, PhasePayload);
			}
			break;
		}

		case EHyperTwistReplayEventType::RecognitionCommit:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::RecognitionCommit,
				TEXT("recognition commit")));
			break;

		case EHyperTwistReplayEventType::RecognitionCorrection:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::RecognitionCorrection,
				TEXT("recognition correction")));
			break;

		case EHyperTwistReplayEventType::Pause:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::Pause,
				TEXT("pause")));
			break;

		case EHyperTwistReplayEventType::Resume:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::Resume,
				TEXT("resume")));
			break;

		case EHyperTwistReplayEventType::StateSnapshot:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::StateSnapshot,
				TEXT("state snapshot")));
			break;

		case EHyperTwistReplayEventType::DeviceSync:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::DeviceSync,
				TEXT("device sync")));
			break;

		case EHyperTwistReplayEventType::Annotation:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::Annotation,
				TEXT("annotation")));
			break;

		case EHyperTwistReplayEventType::SolveEnd:
			Anchors.Add(HyperTwistReplayReviewLibraryInternal::MakeAnchor(
				Event,
				EHyperTwistReplayReviewAnchorKind::SolveEnd,
				TEXT("solve end")));
			break;

		default:
			break;
		}
	}

	return Anchors;
}

FHyperTwistReplayReviewAnalytics UHyperTwistReplayReviewLibrary::DeriveReplayReviewAnalytics(const FHyperTwistReplayPacket& Packet)
{
	FHyperTwistReplayReviewAnalytics Analytics;
	Analytics.EventCount = Packet.Events.Num();
	Analytics.bHasImportedSource = !Packet.SourceImportFormat.IsEmpty();
	Analytics.ReviewAnchors = CollectReplayReviewAnchors(Packet);

	if (Packet.Events.Num() == 0)
	{
		return Analytics;
	}

	const FHyperTwistDerivedReplaySummary Summary = Packet.DerivedSummary.TotalTimeMs > 0 || Packet.DerivedSummary.MoveCount > 0
		? Packet.DerivedSummary
		: UHyperTwistReplayLibrary::DeriveReplaySummary(Packet);

	int32 LastEventTimeMs = Packet.Events[0].TimeMs;
	int32 PreviousMoveAtMs = INDEX_NONE;
	int32 MoveGapCount = 0;
	int32 MoveGapSumMs = 0;
	int32 PauseStartedAtMs = INDEX_NONE;
	float ConfidenceSum = 0.0f;
	int32 ConfidenceCount = 0;
	TSet<FString> UniquePhaseIds;

	Analytics.TimerStartAtMs = INDEX_NONE;
	Analytics.SolveEndAtMs = INDEX_NONE;
	Analytics.FirstMoveAtMs = INDEX_NONE;
	Analytics.LastMoveAtMs = INDEX_NONE;

	for (const FHyperTwistReplayEvent& Event : Packet.Events)
	{
		Analytics.LongestIdleGapMs = FMath::Max(Analytics.LongestIdleGapMs, Event.TimeMs - LastEventTimeMs);
		LastEventTimeMs = Event.TimeMs;

		switch (Event.EventType)
		{
		case EHyperTwistReplayEventType::TimerStart:
			if (Analytics.TimerStartAtMs == INDEX_NONE)
			{
				Analytics.TimerStartAtMs = Event.TimeMs;
			}
			break;

		case EHyperTwistReplayEventType::Move:
			if (Analytics.FirstMoveAtMs == INDEX_NONE)
			{
				Analytics.FirstMoveAtMs = Event.TimeMs;
			}

			if (PreviousMoveAtMs != INDEX_NONE)
			{
				MoveGapSumMs += Event.TimeMs - PreviousMoveAtMs;
				++MoveGapCount;
			}

			PreviousMoveAtMs = Event.TimeMs;
			Analytics.LastMoveAtMs = Event.TimeMs;
			break;

		case EHyperTwistReplayEventType::RecognitionPreview:
		case EHyperTwistReplayEventType::RecognitionScan:
		case EHyperTwistReplayEventType::RecognitionCommit:
		case EHyperTwistReplayEventType::RecognitionCorrection:
		{
			FHyperTwistReplayRecognitionPayload RecognitionPayload;
			if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, RecognitionPayload))
			{
				Analytics.bHasRecognitionData = true;
				ConfidenceSum += RecognitionPayload.Confidence;
				++ConfidenceCount;

				if (Analytics.LowestRecognitionConfidence < 0.0f)
				{
					Analytics.LowestRecognitionConfidence = RecognitionPayload.Confidence;
					Analytics.HighestRecognitionConfidence = RecognitionPayload.Confidence;
				}
				else
				{
					Analytics.LowestRecognitionConfidence = FMath::Min(Analytics.LowestRecognitionConfidence, RecognitionPayload.Confidence);
					Analytics.HighestRecognitionConfidence = FMath::Max(Analytics.HighestRecognitionConfidence, RecognitionPayload.Confidence);
				}
			}

			if (Event.EventType == EHyperTwistReplayEventType::RecognitionPreview
				|| Event.EventType == EHyperTwistReplayEventType::RecognitionScan)
			{
				++Analytics.RecognitionPreviewCount;
			}
			else if (Event.EventType == EHyperTwistReplayEventType::RecognitionCommit)
			{
				++Analytics.RecognitionCommitCount;
			}
			else
			{
				++Analytics.RecognitionCorrectionCount;
			}
			break;
		}

		case EHyperTwistReplayEventType::PhaseMark:
		{
			FHyperTwistReplayPhaseMarkPayload PhasePayload;
			if (HyperTwistReplayReviewLibraryInternal::DeserializePayload(Event.PayloadJson, PhasePayload)
				&& !PhasePayload.PhaseId.IsEmpty())
			{
				UniquePhaseIds.Add(PhasePayload.PhaseId);
				++Analytics.PhaseBoundaryCount;
			}
			break;
		}

		case EHyperTwistReplayEventType::Pause:
			++Analytics.PauseCount;
			if (PauseStartedAtMs == INDEX_NONE)
			{
				PauseStartedAtMs = Event.TimeMs;
			}
			break;

		case EHyperTwistReplayEventType::Resume:
			if (PauseStartedAtMs != INDEX_NONE)
			{
				Analytics.TotalPausedMs += FMath::Max(0, Event.TimeMs - PauseStartedAtMs);
				PauseStartedAtMs = INDEX_NONE;
			}
			break;

		case EHyperTwistReplayEventType::DeviceSync:
			++Analytics.DeviceSyncCount;
			break;

		case EHyperTwistReplayEventType::StateSnapshot:
			++Analytics.StateSnapshotCount;
			break;

		case EHyperTwistReplayEventType::Annotation:
			++Analytics.AnnotationCount;
			break;

		case EHyperTwistReplayEventType::SolveEnd:
			Analytics.SolveEndAtMs = Event.TimeMs;
			break;

		default:
			break;
		}
	}

	if (PauseStartedAtMs != INDEX_NONE)
	{
		const int32 PauseClosedAtMs = Analytics.SolveEndAtMs != INDEX_NONE ? Analytics.SolveEndAtMs : LastEventTimeMs;
		Analytics.TotalPausedMs += FMath::Max(0, PauseClosedAtMs - PauseStartedAtMs);
	}

	Analytics.UniquePhaseCount = UniquePhaseIds.Num();
	Analytics.DurationAfterTimerStartMs = Analytics.TimerStartAtMs != INDEX_NONE
		? FMath::Max(0, (Analytics.SolveEndAtMs != INDEX_NONE ? Analytics.SolveEndAtMs : LastEventTimeMs) - Analytics.TimerStartAtMs)
		: 0;
	Analytics.AverageMoveGapMs = MoveGapCount > 0 ? MoveGapSumMs / MoveGapCount : 0;
	Analytics.AverageRecognitionConfidence = ConfidenceCount > 0 ? ConfidenceSum / static_cast<float>(ConfidenceCount) : -1.0f;

	for (const FHyperTwistPhaseTiming& PhaseTiming : Summary.PhaseTimings)
	{
		if (PhaseTiming.DurationMs > Analytics.LongestPhaseDurationMs)
		{
			Analytics.LongestPhaseDurationMs = PhaseTiming.DurationMs;
			Analytics.LongestPhaseKind = PhaseTiming.PhaseKind;
		}
	}

	return Analytics;
}
