// Fill out your copyright notice in the Description page of Project Settings.
#include "MasterTestProject.h"
#include <time.h>

#include "ProceduralEntity.h"
#include "ImageDownloader.h"
#include "GLTFDownloader.h"

#include <assimp/DefaultLogger.hpp>
#include <assimp/Logger.hpp>



// Sets default values
AProceduralEntity::AProceduralEntity()
	: _requiresFullRecreation(true), _meshCurrentlyProcessed(0)
{
	this->SetActorEnableCollision(true);
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	//_rootComp = CreateDefaultSubobject<USceneComponent>("RootComp");
	//RootComponent = _rootComp;

	_mesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("ProcMesh"));
	if (_mesh) {
		_mesh->CastShadow = true;
		//_mesh->SetCollisionObjectType(ECC_WorldDynamic);
		//_mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		//_mesh->SetCollisionResponseToAllChannels(ECR_Block);
	//	_mesh->UpdateCollisionProfile();
		RootComponent = _mesh;
}	

	/*if (!IsTemplate(RF_Transient)) {
		std::string filename(TCHAR_TO_UTF8(*_filePath));
		loadModel(filename);UGLTFDownloader
	}*/
}

// Called when the game starts or when spawned
void AProceduralEntity::BeginPlay()
{
	Super::BeginPlay();

	//UGLTFDownloader *downloader = NewObject<UGLTFDownloader>(UGLTFDownloader::StaticClass());
	//FString base = FString(TEXT(""));
	//downloader->GetGLTF(this, base, _filePath);
}

void AProceduralEntity::PostActorCreated() {
	Super::PostActorCreated();

	if (!IsTemplate(RF_Transient)) {
	//	UE_LOG(GLTF, Warning, TEXT("File Path: %s"), *_filePath);
	//	std::string filename(TCHAR_TO_UTF8(*_filePath));
	//	loadModel(filename);
	}
}

// Called every frame
void AProceduralEntity::Tick(float DeltaTime)
{
	Super::Tick( DeltaTime );
	
	//struct _stat buf;
	//int result;
	//std::string sfilename(TCHAR_TO_UTF8(*_filePath));
	//// quick C++11 trick to get a char * from a std::string
	//char* filename = &sfilename[0];


	//result = _stat(filename, &buf);
	//if (result != 0) {
	//	UE_LOG(GLTF, Warning, TEXT("Problem getting info"));
	//	switch (errno) {
	//	case ENOENT:
	//		UE_LOG(GLTF, Warning, TEXT("File not found."));
	//		break;
	//	case EINVAL:
	//		UE_LOG(GLTF, Warning, TEXT("Invalid parameter to _stat."));
	//		break;
	//	default:
	//		UE_LOG(GLTF, Warning, TEXT("Unexpected error."));
	//	}
	//}
	//else {
	//	if (_lastModifiedTime == 0 || (int)buf.st_mtime > _lastModifiedTime) {
	//		_lastModifiedTime = (int)buf.st_mtime;
	//		
	//		UE_LOG(GLTF, Warning, TEXT("Reloading model. %s"), *_filePath);
	//		//loadModel(filename);
	//	}
	//}
}

void AProceduralEntity::loadModelFromBlueprint() {
	std::string filename(TCHAR_TO_UTF8(*_filePath));
	//FIXME
	//loadModel(filename);
}

/* ASSIMP IMPORT */
void AProceduralEntity::processMesh(aiMesh* mesh, const aiScene* scene, FMatrix modelTransform) {
	//UE_LOG(GLTF, Warning, TEXT("Processing mesh"));

	// the very first time this method runs, we'll need to create the empty arrays
	// we can't really do that in the class constructor because we don't know how many meshes we'll read, and this data can change between imports
	if (_vertices.Num() <= _meshCurrentlyProcessed) {
		_vertices.AddZeroed();
		_normals.AddZeroed();
		_uvs.AddZeroed();
		_tangents.AddZeroed();
		_vertexColors.AddZeroed();
		_indices.AddZeroed();
	}

	// we check whether the current data to read has a different amount of vertices compared to the last time we generated the mesh
	// if so, it means we'll need to recreate the mesh and resupply new indices.
	if (mesh->mNumVertices != _vertices[_meshCurrentlyProcessed].Num())
		_requiresFullRecreation = true;

	// we reinitialize the arrays for the new data we're reading
	_vertices[_meshCurrentlyProcessed].Empty();
	_normals[_meshCurrentlyProcessed].Empty();
	_uvs[_meshCurrentlyProcessed].Empty();
	// this if actually seems useless, seeing what it does without it
	//if (_requiresFullRecreation) {
		_tangents[_meshCurrentlyProcessed].Empty();
		_vertexColors[_meshCurrentlyProcessed].Empty();
		_indices[_meshCurrentlyProcessed].Empty();
	//}


	FMatrix normalMatrix = modelTransform.Inverse().GetTransposed();

	bool isIdentity = modelTransform.Equals(FMatrix::Identity);

	for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
		FVector4 vertex;
		FVector	normal;
		// process vertex positions, normals and UVs
		vertex.X = mesh->mVertices[i].x;
		vertex.Y = mesh->mVertices[i].y;
		vertex.Z = mesh->mVertices[i].z;
		vertex.W = 1.0;

		normal.X = mesh->mNormals[i].x;
		normal.Y = mesh->mNormals[i].y;
		normal.Z = mesh->mNormals[i].z;

		// if the mesh contains tex coords
		if (mesh->mTextureCoords[0]) {
			FVector2D uvs;
			uvs.X = mesh->mTextureCoords[0][i].x;
			uvs.Y = mesh->mTextureCoords[0][i].y;
			_uvs[_meshCurrentlyProcessed].Add(uvs);
		}
		else {
			_uvs[_meshCurrentlyProcessed].Add(FVector2D(0.f, 0.f));
		}

		if (isIdentity) {
			_vertices[_meshCurrentlyProcessed].Add(vertex);
			_normals[_meshCurrentlyProcessed].Add(normal);
		}
		else {


			FVector4 pos4 = modelTransform.TransformFVector4(vertex);
			FVector dehomogenisert = FVector(pos4 / pos4.W);
			_vertices[_meshCurrentlyProcessed].Add(dehomogenisert);

			//noremalen brauchen keine translation
			_normals[_meshCurrentlyProcessed].Add(normalMatrix.TransformVector(normal));
		}


	}

	if (_requiresFullRecreation) {
		// process indices
		for (uint32 i = 0; i < mesh->mNumFaces; i++) {
			aiFace face = mesh->mFaces[i];
			_indices[_meshCurrentlyProcessed].Add(face.mIndices[2]);
			_indices[_meshCurrentlyProcessed].Add(face.mIndices[1]);
			_indices[_meshCurrentlyProcessed].Add(face.mIndices[0]);
		}
	}

	// we can finally either update or create the mesh
	if (_requiresFullRecreation) {
		//UE_LOG(GLTF, Warning, TEXT("Full recreation"));
		_mesh->CreateMeshSection(_meshCurrentlyProcessed, _vertices[_meshCurrentlyProcessed], _indices[_meshCurrentlyProcessed], _normals[_meshCurrentlyProcessed], _uvs[_meshCurrentlyProcessed], _vertexColors[_meshCurrentlyProcessed], _tangents[_meshCurrentlyProcessed], true);
	}
	else {
	//	UE_LOG(GLTF, Warning, TEXT("Update"));
		_mesh->UpdateMeshSection(_meshCurrentlyProcessed, _vertices[_meshCurrentlyProcessed], _normals[_meshCurrentlyProcessed], _uvs[_meshCurrentlyProcessed], _vertexColors[_meshCurrentlyProcessed], _tangents[_meshCurrentlyProcessed]);
	}

	//load material
	
	UMaterialInterface *material = GetMaterialForIndex(mesh->mMaterialIndex, scene);

	_mesh->SetMaterial(_meshCurrentlyProcessed, material);

	//UE_LOG(GLTF, Warning, TEXT("Done creating the mesh"));

	_requiresFullRecreation = false;
}

void AProceduralEntity::processNode(aiNode* node, const aiScene* scene, FMatrix modelTransform) {
	//UE_LOG(GLTF, Warning, TEXT("Processing a node"));

	FMatrix child = FMatrix();

	aiVector3D nodeScale, nodePos;
	aiQuaternion nodeQuat;
	node->mTransformation.Decompose(nodeScale, nodeQuat, nodePos);

	FQuat lightQuat = FQuat(nodeQuat.x, nodeQuat.y, nodeQuat.z, nodeQuat.w);
	FRotator lightRot = FRotator::MakeFromEuler(lightQuat.Euler());
	FVector trans = FVector(nodePos.x, nodePos.y, nodePos.z);
	FVector scale = FVector(nodeScale.x, nodeScale.y, nodeScale.z);
	FTransform LocalTransform = FTransform(lightRot, trans, scale);

	//UE_LOG(GLTF, Warning, TEXT("Transform: Rot: %s Trans: %s  Scale: %s "),*lightRot.ToString(), * trans.ToString(), *scale.ToString() );

	FMatrix absoluteTransform =  LocalTransform.ToMatrixWithScale() * modelTransform;


	// process all the node's meshes
	for (uint32 i = 0; i < node->mNumMeshes; i++) {
		aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
		//UE_LOG(GLTF, Warning, TEXT("New mesh in the node %i"), mesh->mNumVertices);
		_numVertecies += mesh->mNumVertices;
		processMesh(mesh, scene, absoluteTransform);
		++_meshCurrentlyProcessed;
	}

	// do the same for all of its children
	for (uint32 i = 0; i < node->mNumChildren; i++) {
		//UE_LOG(GLTF, Warning, TEXT("New child in the node"));
		processNode(node->mChildren[i], scene, absoluteTransform);
	}
}


void AProceduralEntity::loadModel(const uint8* RawFileData, int32 lenght) {

	if (MasterMaterialRef == nullptr) {
		MasterMaterialRef = LoadObject<UMaterialInterface>(NULL, TEXT("/Game/Materials/TexturedMaterial"), NULL, LOAD_None, NULL);
			//= FindObject<UMaterialInterface>(GetWorld(), TEXT("TexturedMaterial"));
	}
	if (UniformMaterialRef == nullptr) {
		UniformMaterialRef = LoadObject<UMaterialInterface>(NULL, TEXT("/Game/Materials/TexturedMaterial"), NULL, LOAD_None, NULL);
			//= FindObject<UMaterialInterface>(GetWorld(), TEXT("UniformMaterial"));
	}

	Assimp::Importer importer;

	const aiScene* scene = importer.ReadFileFromMemory(RawFileData, lenght, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenNormals, "glb");

	//UE_LOG(GLTF, Warning, TEXT("START LOGGING"));
	//const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenNormals);

	if (!scene) {
		UE_LOG(GLTF, Error, TEXT("Failed, Scene is empty: %s"), *FString(importer.GetErrorString()));

		std::string extensions;
		importer.GetExtensionList(extensions);
		UE_LOG(GLTF, Error, TEXT("Extension list: %s"), *FString(extensions.c_str()));

		//swchar_t buffer[260];
		//GetModuleFileName(NULL, buffer, 260);
		//UE_LOG(GLTF, Error, TEXT("Curretn Path: %s"), *FString(buffer));

		return;
	}
	
	double start = FPlatformTime::Seconds();
	_numVertecies = 0;

	_meshCurrentlyProcessed = 0;
	processNode(scene->mRootNode, scene, FMatrix::Identity);

	double end = FPlatformTime::Seconds();
	UE_LOG(GLTF, Log, TEXT("GLTF loaded: %i Vertecies in %f seconds"), _numVertecies, end - start);
}

AProceduralEntity* AProceduralEntity::clone()
{
	FActorSpawnParameters Parameters;
	Parameters.Template = this;

	class AProceduralEntity * New = GetWorld()->SpawnActor<AProceduralEntity>(GetClass(), Parameters);
	New->SetOwner(GetOwner());
#if WITH_EDITOR
	New->SetFolderPath(this->GetFolderPath());
#endif

	return New;

}

void AProceduralEntity::setActorDisabled(bool disabled)
{
		this->SetActorHiddenInGame(disabled);
		this->SetActorEnableCollision(!disabled);
		this->SetActorTickEnabled(!disabled);
}

UMaterialInterface  *AProceduralEntity::GetMaterialForIndex(unsigned int index, const aiScene* scene){
	if (MaterialMap.Contains(index)) {
		return *MaterialMap.Find(index);
	}
	else {
		//UE_LOG(GLTF, Warning, TEXT("Parsing Material %d"),index);

		UMaterialInstanceDynamic* newMaterial = UMaterialInstanceDynamic::Create(MasterMaterialRef, this);
		aiMaterial* AMat = scene->mMaterials[index];



		if (AMat->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
			aiString Path;
			if (AMat->GetTexture(aiTextureType_DIFFUSE, 0, &Path, NULL, NULL, NULL, NULL, NULL) == AI_SUCCESS) {

				FString fileName = FString(Path.C_Str());

				if (Path.data[0] == '*') { //embedded
					int32 textureIndex = FCString::Atoi(*fileName.RightChop(1));
					//TODO "(note Atoi is unsafe; no way to indicate errors)" from Unreal Wiki

					if (textureIndex >= 0) {
						aiTexture *aiTexture = scene->mTextures[textureIndex];

					//	UE_LOG(GLTF, Warning, TEXT("Importing Texture with Index  \"%s\" %d (%d|%d)"), *fileName, textureIndex, aiTexture->mWidth, aiTexture->mHeight);

						if (aiTexture->mHeight == 0) { // If mHeight = 0 this is a pointer to a memory buffer of size mWidth containing the compressed texture data. Good luck, have fun!
							FString extension = FString(aiTexture->achFormatHint);
							EImageFormat::Type format = UImageDownloader::GetImageTypeByExtension(extension);

							bool isValid;
							int32 width, height;
							UTexture2D*  diffTexture = UImageDownloader::LoadTexture2D((uint8*)aiTexture->pcData, aiTexture->mWidth, format, isValid, width, height);

							if (isValid)
								newMaterial->SetTextureParameterValue(FName("DiffuseTexture"), diffTexture);

						}
						else {
							//TODO not implemented
							UE_LOG(GLTF, Error, TEXT("not Implemented Texture Format"));
						}
					}
					else {
						UE_LOG(GLTF, Error, TEXT("Invalid Texture Index  \"%s\""), *fileName);
					}

				}
				else {
					UImageDownloader *downloader = NewObject<UImageDownloader>(UImageDownloader::StaticClass());
					downloader->GetTextureForMaterial(newMaterial, FName("DiffuseTexture"), _filePath, fileName);
				}
			}
		}
		else {
			newMaterial = UMaterialInstanceDynamic::Create(UniformMaterialRef, this);

			aiColor3D color(0.f, 0.f, 0.f);
			if (AI_SUCCESS == AMat->Get(AI_MATKEY_COLOR_DIFFUSE, color)) {
				//UE_LOG(GLTF, Warning, TEXT("Material Property : %s, %f %f %f"), TEXT("AI_MATKEY_COLOR_DIFFUSE"), color.r, color.g, color.b);

				FLinearColor Fcolor = FLinearColor(color.r, color.g, color.b);
				newMaterial->SetVectorParameterValue(FName("Diffuse"), Fcolor);
			}
		}

		aiColor3D color(0.f, 0.f, 0.f);
		if (AI_SUCCESS == AMat->Get(AI_MATKEY_COLOR_SPECULAR, color)) {
			//UE_LOG(GLTF, Warning, TEXT("Material Property: %s, %f %f %f"), TEXT("AI_MATKEY_COLOR_SPECULAR"), color.r, color.g, color.b);

			FLinearColor Fcolor = FLinearColor(color.r, color.g, color.b);
			newMaterial->SetVectorParameterValue(FName("Specular"), Fcolor);
		}

		//TODO
		//float shininess;
		//if (AI_SUCCESS == AMat->Get(AI_MATKEY_SHININESS, shininess)) {
		//UE_LOG(GLTF, Warning, TEXT("Material Property: %s, %f"), TEXT("AI_MATKEY_SHININESS"), shininess);

		//newMaterial->SetVectorParameterValue(FName("Specular"), Fcolor);
		//}


		MaterialMap.Add(index, newMaterial);
		return newMaterial;
	}
}