#include "WingProperty.h"
#include "WingActorComponent.h"
#include "WingUtils.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "Engine/Blueprint.h"
#include "Engine/SCS_Node.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Components/Widget.h"
#include "Components/PanelSlot.h"
#include "EdGraph/EdGraphPin.h"
#include "UObject/EnumProperty.h"
#include "Dom/JsonValue.h"


static bool IsPinTypeProperty(FProperty* Prop)
{
	FStructProperty* StructProp = CastField<FStructProperty>(Prop);
	return StructProp && StructProp->Struct == FEdGraphPinType::StaticStruct();
}

FWingProperty::FWingProperty(FProperty* InProp, void* InContainer)
	: Prop(InProp), Container(InContainer) {}

FWingProperty::FWingProperty(FProperty* InProp, UObject* InContainer)
	: Prop(InProp), Container(static_cast<void*>(InContainer)) {}

FString FWingProperty::GetCategory()
{
	FString Result = Prop->GetMetaData(TEXT("Category"));
	if (Result.IsEmpty()) Result = "Unclassified";
	return Result;
}

FString FWingProperty::GetText() const
{
	if (IsPinTypeProperty(Prop))
	{
		FEdGraphPinType PinType;
		Prop->GetValue_InContainer(Container, &PinType);
		return UWingTypes::TypeToText(PinType);
	}
	FString Result;
	Prop->ExportTextItem_InContainer(Result, Container, nullptr, nullptr, PPF_None);
	return Result;
}

FString FWingProperty::GetTruncatedText(int32 MaxLen) const
{
	FString Result = GetText();
	for (int i = 0; i < Result.Len(); i++)
		if (Result[i] == '\n') Result[i] = ' ';
	if (Result.Len() > MaxLen)
		Result = Result.Left(MaxLen) + TEXT("...");
	return Result;
}

void FWingProperty::PrintExpectsReceived(const TCHAR *Type)
{
	UWingServer::Printf(TEXT("ERROR: '%s' received a %s, but expects %s\n"),
		*WingUtils::FormatName(Prop), Type, *Prop->GetCPPType());
}


bool FWingProperty::CheckImportTextResult(const FString &Value)
{
	uint8 *VP = Prop->ContainerPtrToValuePtr<uint8>(Container);
	if (FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop))
	{
		UObject *Obj = OProp->GetObjectPropertyValue(VP);
		if (Obj == nullptr && Value.TrimStartAndEnd().Compare(TEXT("None"), ESearchCase::IgnoreCase) != 0)
		{
			UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s'\n"),
				*Value, *WingUtils::FormatName(Prop));
			return false;
		}
	}
	return true;
}

bool FWingProperty::SetText(FString Value)
{
	// Pin types get parsed by UWingTypes.
	if (IsPinTypeProperty(Prop))
	{
		FEdGraphPinType PinType;
		UWingTypes::Requirements Req;
		Req.BlueprintType = true;
		Req.Blueprintable = false;
		Req.AllowContainer = true;
		if (!UWingTypes::TextToType(Value, PinType, Req)) return false;
		Prop->SetValue_InContainer(Container, &PinType);
		return true;
	}

	// If it's an enum type, use our parsing routine which is smarter about
	// prefixes than ImportText.  We canonicalize the string, and then send
	// it onward to ImportText.
	UEnum *Enum = nullptr;
	if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
		Enum = ByteProp->Enum;
	if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
		Enum = EnumProp->GetEnum();
	if (Enum != nullptr)
	{
		int64 EnumValue;
		if (!WingUtils::StringToEnum(Enum, Value, EnumValue)) return false;
		Value = Enum->GetNameStringByValue(EnumValue);
	}

	// Now Use ImportText
	const TCHAR* Result = Prop->ImportText_InContainer(*Value, Container, nullptr, PPF_None);
	if ((!Result) || (*Result != 0))
	{
		UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
			*Value, *WingUtils::FormatName(Prop), *Prop->GetCPPType());
		return false;
	}
	if (!CheckImportTextResult(Value)) return false;
	return true;
}

bool FWingProperty::SetDouble(double D)
{
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (!NProp)
	{
		PrintExpectsReceived(TEXT("double"));
		return false;
	}
	if (NProp->IsFloatingPoint())
	{
		uint8 buffer[16];
		NProp->SetFloatingPointPropertyValue(buffer, D);
		if (!FMath::IsFinite(NProp->GetFloatingPointPropertyValue(buffer)))
		{
			UWingServer::Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lf\n"),
				*WingUtils::FormatName(Prop), *Prop->GetCPPType(), D);
			return false;
		}
		Prop->SetValue_InContainer(Container, buffer);
		return true;
	}
	else
	{
		uint8 buffer[16];
		if (FMath::Floor(D) != D)
		{
			PrintExpectsReceived(TEXT("double"));
			return false;
		}
		if (FMath::Abs(D) > (double)((1LL)<<53))
		{
			UWingServer::Printf(TEXT("ERROR: To store very large numbers in '%s', do not pass them as double. Use string or int.\n"),
				*WingUtils::FormatName(Prop));
			return false;
		}
		int64 I = (int64)D;
		NProp->SetIntPropertyValue(buffer, I);
		if (NProp->GetSignedIntPropertyValue(buffer) != I)
		{
			UWingServer::Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lld\n"),
				*WingUtils::FormatName(Prop), *Prop->GetCPPType(), I);
			return false;
		}
		NProp->SetValue_InContainer(Container, buffer);
		return true;
	}
}

bool FWingProperty::SetInt64(int64 I)
{
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (!NProp)
	{
		PrintExpectsReceived(TEXT("int"));
		return false;
	}
	if (NProp->IsFloatingPoint())
	{
		uint8 buffer[16];
		double D = I;
		NProp->SetFloatingPointPropertyValue(buffer, D);
		int64 RT = (int64)NProp->GetFloatingPointPropertyValue(buffer);
		if (RT != I)
		{
			UWingServer::Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lld\n"),
				*WingUtils::FormatName(Prop), *Prop->GetCPPType(), I);
			return false;
		}
		Prop->SetValue_InContainer(Container, buffer);
		return true;
	}
	else
	{
		uint8 buffer[16];
		NProp->SetIntPropertyValue(buffer, I);
		if (NProp->GetSignedIntPropertyValue(buffer) != I)
		{
			UWingServer::Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lld\n"),
				*WingUtils::FormatName(Prop), *Prop->GetCPPType(), I);
			return false;
		}
		NProp->SetValue_InContainer(Container, buffer);
		return true;
	}
}

bool FWingProperty::SetBool(bool B)
{
	if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
	{
		Prop->SetValue_InContainer(Container, &B);
		return true;
	}
	PrintExpectsReceived(TEXT("boolean"));
	return false;
}


bool FWingProperty::SetJson(const TSharedPtr<FJsonValue> &JsonValue)
{
	if (JsonValue->Type == EJson::String)
	{
		return SetText(JsonValue->AsString());
	}

	if (JsonValue->Type == EJson::Number)
	{
		return SetDouble(JsonValue->AsNumber());
	}

	if (JsonValue->Type == EJson::Boolean)
	{
		return SetBool(JsonValue->AsBool());
	}

	if (JsonValue->Type == EJson::Object)
	{
		void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
		FStructProperty* StructProp = CastField<FStructProperty>(Prop);
		if (StructProp && (StructProp->Struct == FWingJsonObject::StaticStruct()))
		{
			static_cast<FWingJsonObject*>(ValuePtr)->Json = JsonValue->AsObject();
			return true;
		}
		PrintExpectsReceived(TEXT("json object"));
		return false;
	}

	if (JsonValue->Type == EJson::Array)
	{
		void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
		FStructProperty* StructProp = CastField<FStructProperty>(Prop);
		if (StructProp && (StructProp->Struct == FWingJsonArray::StaticStruct()))
		{
			static_cast<FWingJsonArray*>(ValuePtr)->Array = JsonValue->AsArray();
			return true;
		}
		PrintExpectsReceived(TEXT("json array"));
		return false;
	}

	PrintExpectsReceived(TEXT("Unrecognized Json Data"));
	return false;
}

void FWingProperty::Collect(UStruct* StructType, void* Container, TArray<FWingProperty> &Props, EPropertyFlags Flags)
{
	TMap<FString, TArray<FWingProperty>> Grouped;

	for (TFieldIterator<FProperty> It(StructType); It; ++It)
	{
		if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
		FString SortCat = *It->GetMetaData(TEXT("Category"));
		Grouped.FindOrAdd(SortCat).Add(FWingProperty(*It, Container));
	}
	TArray<FString> Categories;

	Grouped.GetKeys(Categories);
	Categories.Sort([](const FString& A, const FString& B) {
		if (A.IsEmpty()) return false;
		if (B.IsEmpty()) return true;
		return A < B;
	});

	for (const FString& Category : Categories)
	{
		Props.Append(Grouped[Category]);
	}
}

TArray<FWingProperty> FWingProperty::GetAll(UObject* Object, EPropertyFlags Flags)
{
	return GetAll(Object->GetClass(), Object, Flags);
}

TArray<FWingProperty> FWingProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags)
{
	TArray<FWingProperty> Result;
	Collect(StructType, Container, Result, Flags);
	return Result;
}

TArray<FWingProperty> FWingProperty::GetNamed(UStruct* StructType, void* Container, const TArray<FName> &Names)
{
	TArray<FWingProperty> Result;
	for (FName Name : Names)
	{
		FProperty *Prop = StructType->FindPropertyByName(Name);
		if (Prop != nullptr) Result.Emplace(Prop, Container);
	}
	return Result;
}


TArray<FName> FWingProperty::GetNames(UStruct *StructType, EPropertyFlags Flags)
{
	TArray<FName> Result;
	for (TFieldIterator<FProperty> It(StructType); It; ++It)
	{
		if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
		Result.Add(It->GetFName());
	}
	return Result;
}

void FWingProperty::Remove(TArray<FWingProperty>& Props, const FString& Name)
{
	Props.RemoveAll([&](const FWingProperty& P) { return P.Prop->GetName() == Name; });
}

void FWingProperty::Move(TArray<FWingProperty> &Out, TArray<FWingProperty> &In, const FString &Name)
{
	int Dst = 0;
	for (int i = 0; i < In.Num(); i++)
	{
		if (In[i]->GetName() == Name)
		{
			Out.Add(In[i]);
		}
		else
		{
			In[Dst++] = In[i];
		}
	}
	In.SetNum(Dst);
}

TArray<FWingProperty> FWingProperty::GetDetailsGeneral(UObject* Obj, EPropertyFlags Flags, bool Mutable)
{
	if (!Obj) return {};

	// Blueprints don't have editable properties.  So
	// instead, we fetch properties from the generated CDO.
	if (UBlueprint *BP = ::Cast<UBlueprint>(Obj))
	{
		if (BP->GeneratedClass == nullptr)
		{
			UWingServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName());
			return {};
		}
		Obj = BP->GeneratedClass->GetDefaultObject();
	}

	// Component references: get the proper template.
	if (UWingComponentReference* Ref = ::Cast<UWingComponentReference>(Obj))
	{
		Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate();
		if (!Obj)
		{
			UWingServer::Printf(TEXT("ERROR: Component '%s' has no template\n"), *Ref->VariableName.ToString());
			return {};
		}
	}

	TArray<FWingProperty> Result;
	Collect(Obj->GetClass(), Obj, Result, Flags);

	// If it's a Material Graph node, also collect properties from
	// the associated material expression.
	//
	if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj))
	{
		if (UMaterialExpression* Expr = MatNode->MaterialExpression)
		{
			Collect(Expr->GetClass(), Expr, Result, Flags);
		}
	}

	// If it's a Widget, hide the slot property, and add the
	// slot properties.
	if (UWidget* Widget = Cast<UWidget>(Obj))
	{
		FWingProperty::Remove(Result, TEXT("Slot"));
		if (UPanelSlot* Slot = Widget->Slot)
		{
			Collect(Slot->GetClass(), Slot, Result, Flags);
		}
	}

	return Result;
}


TArray<FWingProperty> FWingProperty::FindAllSubstring(const TArray<FWingProperty>& Props, const FString& Substring)
{
	if (Substring.IsEmpty()) return Props;
	TArray<FWingProperty> Result;
	for (const FWingProperty& P : Props)
	{
		if (WingUtils::FormatName(P.Prop).Contains(Substring, ESearchCase::IgnoreCase))
			Result.Add(P);
	}
	return Result;
}

bool FWingProperty::PopulateFromJson(FWingProperty& P, const FJsonObject* Json, bool AllOptional)
{
	FString JsonKey = WingUtils::FormatName(P.Prop);
	TSharedPtr<FJsonValue> Value = Json->TryGetField(JsonKey);

	if (Value == nullptr)
	{
		bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
		if (Optional) return true;
		UWingServer::Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
		return false;
	}

	return P.SetJson(Value);
}

bool FWingProperty::PopulateFromJson(
	TArray<FWingProperty>& Props, const FJsonObject* Json, bool AllOptional)
{
	bool Ok = true;

	// Build a set of known property names for the unknown-field check.
	TSet<FString> KnownKeys;
	for (const FWingProperty& P : Props)
		KnownKeys.Add(WingUtils::FormatName(P.Prop));
	
	// Check for unknown fields in the JSON
	for (const auto& KV : Json->Values)
	{
		if (!KnownKeys.Contains(KV.Key))
		{
			UWingServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
			Ok = false;
		}
	}

	// Populate each property from JSON
	for (FWingProperty& P : Props)
	{
		if (!PopulateFromJson(P, Json, AllOptional)) Ok = false;
	}
	return Ok;
}

bool FWingProperty::PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Object)
{
	TArray<FWingProperty> Props = FWingProperty::GetAll(StructType, Container, (EPropertyFlags)0);
	return PopulateFromJson(Props, Object);
}

bool FWingProperty::PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& Object)
{
	if (!Object.IsValid() || (Object->Type != EJson::Object))
	{
		UWingServer::Print(TEXT("ERROR: Expected a JSON object\n"));
		return false;
	}
	return PopulateFromJson(StructType, Container, Object->AsObject().Get());
}
