﻿using GLTF;
using ItSeez3D.AvatarSdk.Core;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Assertions;
using UnityGLTF;
using static GLTF.CoroutineGLTFSceneImporter;

namespace ItSeez3D.AvatarSdk.Cloud.GLTF
{
	/// <summary>
	/// Class to load an avatar model in GLTF format and to add it to the unity scene.
	/// </summary>
	public class GLTFAvatarLoader
	{
		private class AvatarMeshObject
		{
			public GameObject gameObject;
			public SkinnedMeshRenderer renderer;

			public AvatarMeshObject(GameObject gameObject, SkinnedMeshRenderer renderer)
			{
				this.gameObject = gameObject;
				this.renderer = renderer;
			}
		}

		private class AvatarFeaturesList
		{
			private int currentIdx = -1;
			private Dictionary<string, AvatarMeshObject> data = new Dictionary<string, AvatarMeshObject>();
			public List<string> Names { get; private set; } = new List<string>();
			public string CurrentName { get { return currentIdx >= 0 ? Names[currentIdx] : string.Empty; } }

			public void Add(string name, AvatarMeshObject obj)
			{
				Names.Add(name);
				data.Add(name, obj);
			}

			public void ShowByName(string name)
			{
				currentIdx = GetIdxByName(name);
				for (int i = 0; i < Names.Count; i++)
					data[Names[i]].gameObject.SetActive(i == currentIdx);
			}
			public void ShowByIndex(int idx)
			{
				if (idx >= 0 && idx < Names.Count)
					ShowByName(Names[idx]);
				else
					ShowByName(string.Empty);
			}
			public void ShowNext()
			{
				ShowByIndex(GetNextIdx());
			}
			public void ShowPrev()
			{
				ShowByIndex(GetPrevIdx());
			}

			public int CurrentIdx
			{
				get { return currentIdx; }
			}

			public int GetObjectsCount
			{
				get { return Names.Count; }
			}

			public AvatarMeshObject GetCurrentMeshObject()
			{
				if (currentIdx < 0)
					return null;
				return data[Names[currentIdx]];
			}

			public AvatarMeshObject GetMeshObjectByIdx(int idx)
			{
				if (idx < 0)
					return null;
				return data[Names[idx]];
			}

			public int GetNextIdx()
			{
				int idx = currentIdx + 1;
				if (idx >= Names.Count)
					idx = -1;
				return idx;
			}

			public int GetPrevIdx()
			{
				int idx = currentIdx - 1;
				if (idx < -1)
					idx = Names.Count - 1;
				return idx;
			}

			public int GetIdxByName(string name)
			{
				Func<string, bool> haircutNamesMatch = new Func<string, bool>((string id) => {
					return id.Equals(name) || id.Equals(name.Replace('/', '_'));
				});
				for (int i = 0; i < Names.Count; i++)
				{
					if (haircutNamesMatch(Names[i]))
						return i;
				}
				return -1;
			}
		}

		/// <summary>
		/// Render body with PBR textures (metallness/rougness maps, normal map)
		/// </summary>
		public bool UseBodyPBRTextures = true;

		/// <summary>
		/// Render outfits with PBR textures (metallness/rougness maps, normal map)
		/// </summary>
		public bool UseOutfitsPBRTextures = true;

		/// <summary>
		/// This option enforce normals recalculation for the body mesh.
		/// It allows to get rid of seams on the neck and shoulders but it increases the loading time.
		/// </summary>
		public bool ImproveBodyNormals = true;

		/// <summary>
		/// Haircuts and outfits textures are loaded only for the meshes that are in the active state.
		/// It reduces graphics memory consumption.
		/// </summary>
		public bool LazyTexturesLoading = true;

		private string loadedAvatarCode = string.Empty;

		private AvatarMeshObject body;

		private AvatarFeaturesList haircuts = new AvatarFeaturesList();
		private AvatarFeaturesList outfits = new AvatarFeaturesList();

		private bool playAnimationOnStart = false;

		private FullbodyMaterialAdjuster materialAdjuster = new FullbodyMaterialAdjuster();

		/// <summary>
		/// Load the model on the scene
		/// </summary>
		/// <param name="avatarCode">Full body avatar code</param>
		/// <param name="avatarGameObject">Parent GameObject for the avatar</param>
		public IEnumerator LoadModelOnSceneAsync(string avatarCode, GameObject avatarGameObject)
		{
			loadedAvatarCode = avatarCode;
			string meshFilename = AvatarSdkMgr.Storage().GetAvatarFilename(avatarCode, AvatarFile.MESH_GLTF);
			yield return LoadAsync(meshFilename, avatarGameObject);
		}

		/// <summary>
		/// Load the model on the scene
		/// </summary>
		/// <param name="filePath">Path to the GLTF file</param>
		/// <param name="avatarGameObject">Parent GameObject for the avatar</param>
		public IEnumerator LoadAsync(string filePath, GameObject avatarGameObject)
		{
			if (string.IsNullOrWhiteSpace(filePath))
				yield break;

			var importOptions = new ImportOptions();

			CoroutineGLTFSceneImporter sceneImporter = null;
			try
			{
				sceneImporter = new CoroutineGLTFSceneImporter(Path.GetFileName(filePath), importOptions);
				sceneImporter.SkipTexturesLoading = LazyTexturesLoading;

				string directoryPath = URIHelper.GetDirectoryName(filePath);
				importOptions.DataLoader = new UnityGLTF.Loader.FileLoader(directoryPath);

				sceneImporter.SceneParent = avatarGameObject;

				yield return sceneImporter.LoadSceneAsync();

				SkinnedMeshRenderer[] renderers = avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>();
				foreach (SkinnedMeshRenderer renderer in renderers)
				{
					if (renderer.gameObject.name.StartsWith("haircut"))
					{
						string haircutName = ExtractHaircutName(renderer.gameObject.name);
						AvatarMeshObject haircutObject = new AvatarMeshObject(renderer.gameObject, renderer);
						haircutObject.gameObject.SetActive(false);
						if (!LazyTexturesLoading)
						{
							CoroutineResult<Material> coroutineResult = new CoroutineResult<Material>();
							yield return materialAdjuster.PrepareHairMaterial(coroutineResult, loadedAvatarCode, haircutName);
							haircutObject.renderer.sharedMaterial = coroutineResult.result;
						}
						haircuts.Add(haircutName, haircutObject);
					}
					else if (renderer.gameObject.name.StartsWith("outfit"))
					{
						string outfitName = renderer.gameObject.name;
						AvatarMeshObject outfitObject = new AvatarMeshObject(renderer.gameObject, renderer);
						outfitObject.gameObject.SetActive(false);
						if (!LazyTexturesLoading)
						{
							CoroutineResult<Material> coroutineResult = new CoroutineResult<Material>();
							yield return materialAdjuster.PrepareOutfitMaterial(coroutineResult, loadedAvatarCode, outfitName, UseOutfitsPBRTextures);
							outfitObject.renderer.sharedMaterial = coroutineResult.result;
						}
						outfits.Add(outfitName, outfitObject);
					}
					else if (renderer.gameObject.name == "mesh")
					{
						if (ImproveBodyNormals)
							MeshUtils.ImproveNormals(renderer.sharedMesh);
						body = new AvatarMeshObject(renderer.gameObject, renderer);
						CoroutineResult<Material> coroutineResult = new CoroutineResult<Material>();
						yield return materialAdjuster.PrepareBodyMaterial(coroutineResult, loadedAvatarCode, "", UseBodyPBRTextures);
						body.renderer.sharedMaterial = coroutineResult.result;
						RenameBlendshapes(body.renderer.sharedMesh);
					}
				}

				var animations = sceneImporter.SceneParent.GetComponents<Animation>();
				if (playAnimationOnStart && animations.Any())
				{
					animations.FirstOrDefault().Play();
				}
			}
			finally
			{
				if (importOptions.DataLoader != null)
				{
					sceneImporter?.Dispose();
					sceneImporter = null;
					importOptions.DataLoader = null;
				}
			}
		}

		/// <summary>
		/// Returns GameObject on the body mesh
		/// </summary>
		public GameObject GetBodyObject()
		{
			return body.gameObject;
		}

		/// <summary>
		/// Updates material of the body mesh. 
		/// The method is used in case the UseBodyPBRTextures changing will take effect for the current avatar.
		/// </summary>
		/// <returns></returns>
		public IEnumerator UpdateBodyRenderer()
		{
			CoroutineResult<Material> coroutineResult = new CoroutineResult<Material>();
			yield return materialAdjuster.PrepareBodyMaterial(coroutineResult, loadedAvatarCode, outfits.CurrentName, UseBodyPBRTextures);
			body.renderer.sharedMaterial = coroutineResult.result;
			Resources.UnloadUnusedAssets();
		}

		#region HaircutsAccessMethods
		/// <summary>
		/// Returns list of available haircuts
		/// </summary>
		public List<string> GetHaircuts()
		{
			return haircuts.Names.ToList();
		}

		/// <summary>
		/// Returns name of the current displayed haircut 
		/// </summary>
		public string GetCurrentHaircutName()
		{
			return haircuts.CurrentName;
		}

		/// <summary>
		/// Shows haircut by name
		/// </summary>
		public IEnumerator ShowHaircut(string haircutName)
		{
			yield return ChangeHaircutAsync(() => haircuts.ShowByName(haircutName), haircuts.CurrentIdx, haircuts.GetIdxByName(haircutName));
		}

		/// <summary>
		/// Shows haircut by index
		/// </summary>
		public IEnumerator ShowHaircut(int haircutIdx)
		{
			yield return ChangeHaircutAsync(() => haircuts.ShowByIndex(haircutIdx), haircuts.CurrentIdx, haircutIdx);
		}

		/// <summary>
		/// Shows next haircut
		/// </summary>
		public IEnumerator ShowNextHaircut()
		{
			yield return ChangeHaircutAsync(() => haircuts.ShowNext(), haircuts.CurrentIdx, haircuts.GetNextIdx());
		}

		/// <summary>
		/// Shows previous haircut
		/// </summary>
		public IEnumerator ShowPrevHaircut()
		{
			yield return ChangeHaircutAsync(() => haircuts.ShowPrev(), haircuts.CurrentIdx, haircuts.GetPrevIdx());
		}

		private IEnumerator ChangeHaircutAsync(Action changeHaircutFunc, int currentHaircutIdx, int newHaircutIdx)
		{
			if (currentHaircutIdx == newHaircutIdx)
				yield break;

			if (LazyTexturesLoading)
			{
				if (newHaircutIdx >= 0)
				{
					CoroutineResult<Material> coroutineResult = new CoroutineResult<Material>();
					yield return materialAdjuster.PrepareHairMaterial(coroutineResult, loadedAvatarCode, haircuts.Names[newHaircutIdx]);
					haircuts.GetMeshObjectByIdx(newHaircutIdx).renderer.sharedMaterial = coroutineResult.result;
				}

				if (currentHaircutIdx >= 0)
					haircuts.GetMeshObjectByIdx(currentHaircutIdx).renderer.materials = new Material[] { };

				Resources.UnloadUnusedAssets();
			}

			changeHaircutFunc();
		}

		#endregion

		#region OutfitsAccessMethods
		/// <summary>
		/// Returns list of available outfits
		/// </summary>
		public List<string> GetOutfits()
		{
			return outfits.Names.ToList();
		}

		/// <summary>
		/// Returns name of the current displayed outfit 
		/// </summary>
		public string GetCurrentOutfitName()
		{
			return outfits.CurrentName;
		}

		public int GetCurrentOutfitIdx()
		{
			return outfits.CurrentIdx;
		}

		/// <summary>
		/// Shows outfit by name
		/// </summary>
		public IEnumerator ShowOutfit(string outfitName)
		{
			int outfitIdx = outfits.Names.IndexOf(outfitName);
			yield return ShowOutfit(outfitIdx);
		}

		/// <summary>
		/// Shows outfit by index
		/// </summary>
		public IEnumerator ShowOutfit(int outfitIdx)
		{
			if (LazyTexturesLoading)
			{
				AvatarMeshObject newOutfitObject = outfits.GetMeshObjectByIdx(outfitIdx);
				if (newOutfitObject != null)
				{
					CoroutineResult<Material> outfitMaterialResult = new CoroutineResult<Material>();
					yield return materialAdjuster.PrepareOutfitMaterial(outfitMaterialResult, loadedAvatarCode, outfits.Names[outfitIdx], UseOutfitsPBRTextures);
					newOutfitObject.renderer.sharedMaterial = outfitMaterialResult.result;
				}
				yield return materialAdjuster.UpdateBodyMainTexture(body.renderer.sharedMaterial, loadedAvatarCode, outfitIdx >= 0 ? outfits.Names[outfitIdx] : null, UseBodyPBRTextures);

				if (outfitIdx != outfits.CurrentIdx)
				{
					AvatarMeshObject currentOutfitObject = outfits.GetCurrentMeshObject();
					if (currentOutfitObject != null)
						currentOutfitObject.renderer.materials = new Material[] { };
				}

				Resources.UnloadUnusedAssets();
			}

			outfits.ShowByIndex(outfitIdx);
		}

		#endregion

		/// <summary>
		/// Returns a list of available blendshapes
		/// </summary>
		public List<string> GetBlendshapes()
		{
			List<string> blendshapes = new List<string>();
			for (int i = 0; i < body.renderer.sharedMesh.blendShapeCount; i++)
				blendshapes.Add(body.renderer.sharedMesh.GetBlendShapeName(i));
			return blendshapes;
		}

		/// <summary>
		/// Sets all blendshapes weights to zero
		/// </summary>
		public void ClearBlendshapesWeights()
		{
			for (int i = 0; i < body.renderer.sharedMesh.blendShapeCount; i++)
				body.renderer.SetBlendShapeWeight(i, 0.0f);
		}

		/// <summary>
		/// Sets the weight for the blendshapes with the provided index
		/// </summary>
		public void SetBlendshapeWeight(int blendshapeIdx, float weight)
		{
			body.renderer.SetBlendShapeWeight(blendshapeIdx, weight);
		}

		private string ExtractHaircutName(string haircutObjectName)
		{
			string pattern = "haircut_";
			int pos = haircutObjectName.IndexOf(pattern);
			return haircutObjectName.Substring(pos + pattern.Length);
		}

		private void RenameBlendshapes(Mesh avatarMesh)
		{
			if (avatarMesh.blendShapeCount == 0)
				return;

			List<Vector3[]> blendshapesDeltaVertices = new List<Vector3[]>();
			for (int i = 0; i < avatarMesh.blendShapeCount; i++)
			{
				Vector3[] deltaVertices = new Vector3[avatarMesh.vertexCount];
				avatarMesh.GetBlendShapeFrameVertices(i, 0, deltaVertices, null, null);
				blendshapesDeltaVertices.Add(deltaVertices);
			}

			avatarMesh.ClearBlendShapes();

			int blendshapeIdx = 0;
			if (blendshapesDeltaVertices.Count == 51 || blendshapesDeltaVertices.Count == 66)
			{
				TextAsset mobileBlendshapesListAsset = Resources.Load<TextAsset>("mobile_51_list");
				var mobileList = mobileBlendshapesListAsset.text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
				for (int i = 0; i < 51; i++, blendshapeIdx++)
					avatarMesh.AddBlendShapeFrame(mobileList[i], 100, blendshapesDeltaVertices[blendshapeIdx], null, null);
			}
			if (blendshapesDeltaVertices.Count == 15 || blendshapesDeltaVertices.Count == 66)
			{
				TextAsset visemesBlendshapesListAsset = Resources.Load<TextAsset>("visemes_15_list");
				var visemesList = visemesBlendshapesListAsset.text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
				for (int i = 0; i < 15; i++, blendshapeIdx++)
					avatarMesh.AddBlendShapeFrame(visemesList[i], 100, blendshapesDeltaVertices[blendshapeIdx], null, null);
			}
		}
	}
}
