#include "HyperTwistReplay/HyperTwistReplayLibrary.h"

#include "JsonObjectConverter.h"

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

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

FHyperTwistReplayPacket UHyperTwistReplayLibrary::AppendReplayEvent(const FHyperTwistReplayPacket& Packet, const FHyperTwistReplayEvent& Event)
{
	FHyperTwistReplayPacket UpdatedPacket = Packet;
	FHyperTwistReplayEvent UpdatedEvent = Event;

	const int32 LastSequence = UpdatedPacket.Events.Num() > 0 ? UpdatedPacket.Events.Last().Sequence : 0;
	const int32 LastTimeMs = UpdatedPacket.Events.Num() > 0 ? UpdatedPacket.Events.Last().TimeMs : 0;

	if (UpdatedEvent.Sequence <= LastSequence)
	{
		UpdatedEvent.Sequence = LastSequence + 1;
	}

	if (UpdatedEvent.TimeMs < LastTimeMs)
	{
		UpdatedEvent.TimeMs = LastTimeMs;
	}

	if (UpdatedEvent.EventId.IsEmpty())
	{
		UpdatedEvent.EventId = FString::Printf(TEXT("evt_%03d"), UpdatedEvent.Sequence);
	}

	UpdatedPacket.Events.Add(UpdatedEvent);
	UpdatedPacket.DerivedSummary = DeriveReplaySummary(UpdatedPacket);
	return UpdatedPacket;
}

FHyperTwistDerivedReplaySummary UHyperTwistReplayLibrary::DeriveReplaySummary(const FHyperTwistReplayPacket& Packet)
{
	FHyperTwistDerivedReplaySummary Summary;
	if (Packet.Events.Num() == 0)
	{
		return Summary;
	}

	int32 TimerStartMs = INDEX_NONE;
	float ConfidenceSum = 0.0f;
	int32 ConfidenceCount = 0;

	TMap<FString, FHyperTwistPhaseTiming> OpenPhaseTimings;

	for (const FHyperTwistReplayEvent& Event : Packet.Events)
	{
		Summary.TotalTimeMs = FMath::Max(Summary.TotalTimeMs, Event.TimeMs);

		switch (Event.EventType)
		{
		case EHyperTwistReplayEventType::TimerStart:
			if (TimerStartMs == INDEX_NONE)
			{
				TimerStartMs = Event.TimeMs;
				Summary.InspectionTimeMs = TimerStartMs;
			}
			break;

		case EHyperTwistReplayEventType::Move:
			++Summary.MoveCount;
			break;

		case EHyperTwistReplayEventType::RecognitionCommit:
			++Summary.RecognizedSegments;
			// intentional fallthrough
		case EHyperTwistReplayEventType::RecognitionPreview:
		case EHyperTwistReplayEventType::RecognitionScan:
		case EHyperTwistReplayEventType::RecognitionCorrection:
		{
			FHyperTwistReplayRecognitionPayload RecognitionPayload;
			if (HyperTwistReplayLibraryInternal::DeserializePayload(Event.PayloadJson, RecognitionPayload))
			{
				ConfidenceSum += RecognitionPayload.Confidence;
				++ConfidenceCount;
			}
			break;
		}

		case EHyperTwistReplayEventType::PhaseMark:
		{
			FHyperTwistReplayPhaseMarkPayload PhasePayload;
			if (HyperTwistReplayLibraryInternal::DeserializePayload(Event.PayloadJson, PhasePayload)
				&& !PhasePayload.PhaseId.IsEmpty())
			{
				const FString Action = PhasePayload.PhaseAction.ToLower();
				if (Action == TEXT("start"))
				{
					FHyperTwistPhaseTiming& PhaseTiming = OpenPhaseTimings.FindOrAdd(PhasePayload.PhaseId);
					PhaseTiming.PhaseKind = PhasePayload.PhaseKind;
					PhaseTiming.StartedAtMs = Event.TimeMs;
				}
				else if ((Action == TEXT("end") || Action == TEXT("complete")) && OpenPhaseTimings.Contains(PhasePayload.PhaseId))
				{
					FHyperTwistPhaseTiming PhaseTiming = OpenPhaseTimings[PhasePayload.PhaseId];
					PhaseTiming.EndedAtMs = Event.TimeMs;
					PhaseTiming.DurationMs = FMath::Max(0, PhaseTiming.EndedAtMs - PhaseTiming.StartedAtMs);
					Summary.PhaseTimings.Add(PhaseTiming);
					OpenPhaseTimings.Remove(PhasePayload.PhaseId);
				}
			}
			break;
		}

		case EHyperTwistReplayEventType::SolveEnd:
			Summary.Result = TEXT("completed");
			break;

		default:
			break;
		}
	}

	for (TPair<FString, FHyperTwistPhaseTiming>& Pair : OpenPhaseTimings)
	{
		Pair.Value.EndedAtMs = Summary.TotalTimeMs;
		Pair.Value.DurationMs = FMath::Max(0, Pair.Value.EndedAtMs - Pair.Value.StartedAtMs);
		Summary.PhaseTimings.Add(Pair.Value);
	}

	if (Summary.Result.IsEmpty())
	{
		Summary.Result = TEXT("in-progress");
	}

	if (ConfidenceCount > 0)
	{
		Summary.ConfidenceRollup = ConfidenceSum / static_cast<float>(ConfidenceCount);
	}

	if (Summary.TotalTimeMs > 0)
	{
		Summary.TPS = static_cast<float>(Summary.MoveCount) / (static_cast<float>(Summary.TotalTimeMs) / 1000.0f);
	}

	return Summary;
}
