#include "WingProperty.h"
#include "WingComponent.h"
#include "WingUtils.h"
#include "WingBasics.h"
#include "WingTypes.h"
#include "Engine/Blueprint.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"


bool FWingProperty::SetObject(UObject *Obj, WingOut Errors) const
{
	if (!CheckEditable(Errors)) return false;
	FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
	if (!OProp)
	{
		PrintExpectsReceived(TEXT("object"), Errors);
		return false;
	}
	if (Obj)
	{
		if (!Obj->IsA(OProp->PropertyClass))
		{
			Errors.Printf(TEXT("ERROR: Property '%s' expects %s, but received %s\n"),
				*WingUtils::FormatName(Prop), *OProp->PropertyClass->GetName(), *Obj->GetClass()->GetName());
			return false;
		}
		if (FClassProperty *CProp = CastField<FClassProperty>(Prop))
		{
			UClass *Class = CastChecked<UClass>(Obj);
			if (!Class->IsChildOf(CProp->MetaClass))
			{
				Errors.Printf(TEXT("ERROR: Property '%s' expects a subclass of %s, but received %s\n"),
					*WingUtils::FormatName(Prop), *CProp->MetaClass->GetName(), *Class->GetName());
				return false;
			}
		}
	}
	OProp->SetObjectPropertyValue_InContainer(Container, Obj);
	return true;
}

bool FWingProperty::SetDoubleInternal(
	FNumericProperty *NProp, void *Container, double D, WingOut Errors)
{
	uint8 buffer[16];
	NProp->SetFloatingPointPropertyValue(buffer, D);
	if (!FMath::IsFinite(NProp->GetFloatingPointPropertyValue(buffer)))
	{
		Errors.Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lf\n"),
			*WingUtils::FormatName(NProp), *NProp->GetCPPType(), D);
		return false;
	}
	NProp->SetValue_InContainer(Container, buffer);
	return true;
}

bool FWingProperty::SetInt64Internal(
	FNumericProperty *NProp, void *Container, int64 I, WingOut Errors)
{
	uint8 buffer[16];
	if ((I < 0) && IsUnsigned(NProp))
	{
		Errors.Printf(TEXT(
			"ERROR: Cannot store signed %lld in unsigned property %s\n"),
			I, *WingUtils::FormatName(NProp));
		return false;
	}
	NProp->SetIntPropertyValue(buffer, I);
	if (NProp->GetSignedIntPropertyValue(buffer) != I)
	{
		Errors.Printf(TEXT("ERROR: Property '%s' of type %s cannot hold %lld\n"),
			*WingUtils::FormatName(NProp), *NProp->GetCPPType(), I);
		return false;
	}
	NProp->SetValue_InContainer(Container, buffer);
	return true;
}

bool FWingProperty::SetDouble(double D, WingOut Errors) const
{
	if (!CheckEditable(Errors)) return false;
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (NProp == nullptr)
	{
		PrintExpectsReceived(TEXT("double"), Errors);
		return false;
	}
	else if (NProp->IsFloatingPoint())
	{
		return SetDoubleInternal(NProp, Container, D, Errors);
	}
	else
	{
		int64 I;
		if (!LosslessDoubleToInt64(D, I, Errors)) return false;
		return SetInt64Internal(NProp, Container, I, Errors);
	}
}

bool FWingProperty::SetInt64(int64 I, WingOut Errors) const
{
	if (!CheckEditable(Errors)) return false;
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (NProp == nullptr)
	{
		PrintExpectsReceived(TEXT("int"), Errors);
		return false;
	}
	else if (NProp->IsFloatingPoint())
	{
		double D;
		if (!LosslessInt64ToDouble(I, D, Errors)) return false;
		return SetDoubleInternal(NProp, Container, D, Errors);
	}
	else
	{
		return SetInt64Internal(NProp, Container, I, Errors);
	}
}

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

bool FWingProperty::SetText(FString Value, WingOut Errors) const
{
	if (!CheckEditable(Errors)) return false;

	// 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, Errors)) return false;
		Prop->SetValue_InContainer(Container, &PinType);
		return true;
	}

	// Class properties get parsed by UWingTypes.
	if (FClassProperty *CProp = CastField<FClassProperty>(Prop))
	{
		UWingTypes::Requirements Req;
		Req.IsChildOf = CProp->MetaClass;
		if (CProp->MetaClass == nullptr) Req.IsChildOf = UObject::StaticClass();
		Req.AllowNone = true;
		Req.AllowContainer = false;
		FEdGraphPinType PinType;
		if (!UWingTypes::TextToType(Value, PinType, Req, Errors)) return false;
		CProp->SetObjectPropertyValue_InContainer(Container,
			Cast<UClass>(PinType.PinSubCategoryObject.Get()));
		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, Errors)) return false;
		Value = Enum->GetNameStringByValue(EnumValue);
	}

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

TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
{
	FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
	if (!OProp)
	{
		PrintExpectsReceived(TEXT("object"), Errors);
		return {};
	}
	uint8 *VP = Prop->ContainerPtrToValuePtr<uint8>(Container);
	return OProp->GetObjectPropertyValue(VP);
}

TOptional<double> FWingProperty::GetDouble(WingOut Errors) const
{
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (!NProp)
	{
		PrintExpectsReceived(TEXT("double"), Errors);
		return {};
	}
	uint8 *VP = Prop->ContainerPtrToValuePtr<uint8>(Container);
	if (NProp->IsFloatingPoint())
	{
		return NProp->GetFloatingPointPropertyValue(VP);
	}
	else
	{
		int64 I = NProp->GetSignedIntPropertyValue(VP);
		double D = (double)I;
		if ((int64)D != I)
		{
			Errors.Printf(TEXT("ERROR: Property '%s' value %lld cannot be represented losslessly as double\n"),
				*WingUtils::FormatName(Prop), I);
			return {};
		}
		return D;
	}
}

TOptional<int64> FWingProperty::GetInt64(WingOut Errors) const
{
	FNumericProperty *NProp = CastField<FNumericProperty>(Prop);
	if (!NProp)
	{
		PrintExpectsReceived(TEXT("int"), Errors);
		return {};
	}
	uint8 *VP = Prop->ContainerPtrToValuePtr<uint8>(Container);
	if (NProp->IsFloatingPoint())
	{
		double D = NProp->GetFloatingPointPropertyValue(VP);
		if (FMath::Floor(D) != D)
		{
			Errors.Printf(TEXT("ERROR: Property '%s' value %lf is not an integer\n"),
				*WingUtils::FormatName(Prop), D);
			return {};
		}
		if (FMath::Abs(D) > (double)((1LL)<<53))
		{
			Errors.Printf(TEXT("ERROR: Property '%s' value %lf is too large to convert to int64 losslessly\n"),
				*WingUtils::FormatName(Prop), D);
			return {};
		}
		return (int64)D;
	}
	else
	{
		return NProp->GetSignedIntPropertyValue(VP);
	}
}

TOptional<bool> FWingProperty::GetBool(WingOut Errors) const
{
	if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
	{
		uint8 *VP = Prop->ContainerPtrToValuePtr<uint8>(Container);
		return BoolProp->GetPropertyValue(VP);
	}
	PrintExpectsReceived(TEXT("boolean"), Errors);
	return {};
}

FString FWingProperty::GetText() const
{
	if (IsPinTypeProperty(Prop))
	{
		FEdGraphPinType *PinType = Prop->ContainerPtrToValuePtr<FEdGraphPinType>(Container);
		return UWingTypes::TypeToText(*PinType);
	}
	if (FClassProperty *CProp = CastField<FClassProperty>(Prop))
	{
		UObject *Obj = CProp->GetObjectPropertyValue_InContainer(Container);
		if (Obj) return UWingTypes::TypeToText(Obj);
		return TEXT("None");
	}
	FString Result;
	// DefaultValue == PropertyValue makes ExportText_Direct's Data==Delta
	// short-circuit fire for every subfield, so default-valued fields still
	// get emitted (e.g. SizeRule=Automatic on FSlateChildSize).
	const void* Value = Prop->ContainerPtrToValuePtr<void>(Container);
	Prop->ExportTextItem_Direct(Result, Value, /*DefaultValue=*/Value, 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::Print(WingOut Out) const
{
	bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
	if (Editable) bEditable = true;
	Out.Printf(TEXT("    %s %s %s = %s\n"),
		bEditable ? TEXT("editable") : TEXT("readonly"),
		*UWingTypes::TypeToText(Prop),
		*WingUtils::FormatName(Prop),
		*GetTruncatedText(100));
}

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

TArray<FWingProperty> FWingProperty::GetAll(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
{
	TArray<FWingProperty> Result;
	for (TFieldIterator<FProperty> It(Struct); It; ++It)
	{
		bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
		Result.Emplace(Obj, Container, *It, Editable);
	}
	return Result;
}

TArray<FWingProperty> FWingProperty::GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable)
{
	TArray<FWingProperty> Result;
	for (TFieldIterator<FProperty> It(Struct); It; ++It)
	{
		if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
		bool Editable = Mutable && !It->HasAnyPropertyFlags(CPF_EditConst);
		Result.Emplace(Obj, Container, *It, Editable);
	}
	return Result;
}

TArray<FName> FWingProperty::GetVisibleNames(UStruct *US)
{
	TArray<FName> Result;
	for (TFieldIterator<FProperty> It(US); It; ++It)
	{
		if (!It->HasAllPropertyFlags(CPF_Edit)) continue;
		Result.Add(It->GetFName());
	}
	return Result;
}

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

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

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

	// If it's a UWingStructRef, return the properties
	// of the struct instead.  Propagate editability of the host.
	if (UWingStructRef *SP = Cast<UWingStructRef>(Obj))
	{
		return GetVisible(SP->Object, SP->StructBase,
				SP->StructType, Mutable && SP->Editable);
	}

	// 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)
		{
			WingOut::Stdout.Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName());
			return {};
		}
		Obj = BP->GeneratedClass->GetDefaultObject();
	}

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

	TArray<FWingProperty> Result = GetVisible(Obj, Mutable);

	// 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)
		{
			Result.Append(GetVisible(Expr, Mutable));
		}
	}

	// If it's a Widget, hide the slot property, and add the
	// slot properties.  Then, add the 'bIsVariable' property and
	// make it editable.
	if (UWidget* Widget = Cast<UWidget>(Obj))
	{
		FWingProperty::Remove(Result, TEXT("Slot"));
		if (UPanelSlot* Slot = Widget->Slot)
		{
			Result.Append(GetVisible(Slot, Mutable));
		}
		FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable"));
		if (VarProp) Result.Emplace(Widget, Widget, VarProp, true);
	}

	return Result;
}

bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
{
	int32 ArgIndex = 0;
	for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
	{
		FWingProperty& P = Props[PropIndex];
		FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
		const bool bIsRest =
			StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());

		if (bIsRest)
		{
			if (PropIndex + 1 != Props.Num())
			{
				Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
					*WingUtils::FormatName(P.Prop));
				return false;
			}

			FWingRestOfArgv Rest;
			for (int32 I = ArgIndex; I < Argv.Num(); ++I)
			{
				Rest.Argv.Add(Argv[I]);
			}
			P.Prop->SetValue_InContainer(P.Container, &Rest);
			ArgIndex = Argv.Num();
			continue;
		}

		if (ArgIndex >= Argv.Num())
		{
			Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
				*WingUtils::FormatName(P.Prop));
			return false;
		}

		if (!P.SetText(Argv[ArgIndex], Errors)) return false;
		ArgIndex++;
	}

	if (ArgIndex < Argv.Num())
	{
		Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
			*Argv[ArgIndex]);
		return false;
	}

	return true;
}


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



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


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

bool FWingProperty::CheckEditable(WingOut Errors) const
{
	if (!Editable)
	{
		Errors.Printf(TEXT("ERROR: Cannot edit property %s, not marked editable"),
				*WingUtils::FormatName(Prop));
		return false;
	}
	return true;
}

bool FWingProperty::IsUnsigned(FNumericProperty* Prop)
{
	return
		CastField<FByteProperty>(Prop) ||
		CastField<FUInt16Property>(Prop) ||
		CastField<FUInt32Property>(Prop) ||
		CastField<FUInt64Property>(Prop);
}

bool FWingProperty::LosslessDoubleToInt64(double D, int64 &I, WingOut Errors)
{
	if (FMath::Floor(D) != D)
	{
		Errors.Printf(TEXT(
			"ERROR: Converting double %.4lf to integer would lose precision\n"), D);
		return false;
	}
	if (FMath::Abs(D) > (double)((1LL)<<53))
	{
		Errors.Printf(TEXT(
			"ERROR: Converting huge double %lf to integer would lose data.\n"), D);
		return false;
	}
	I = (int64)D;
	return true;
}

bool FWingProperty::LosslessInt64ToDouble(int64 I, double &D, WingOut Errors)
{
	D = (double)I;
	if ((int64)D != I)
	{
		Errors.Printf(TEXT(
			"ERROR: Converting huge integer %lld to floating point would lose data.\n"), I);
		return false;
	}
	return true;
}
