﻿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;

namespace ItSeez3D.AvatarSdk.Cloud.GLTF
{
	public interface IOutfitsProvider 
	{
		List<string> GetOutfits();
		string GetCurrentOutfitName();
		void ShowOutfit(string outfitName);
		void ShowOutfit(int outfitIdx);
	}


	/// <summary>
	/// Class to load an avatar model in GLTF format and to add it to the unity scene.
	/// </summary>
	public class GLTFAvatarLoader : IOutfitsProvider
	{
		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 = -1;
				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++)
				{
					data[Names[i]].gameObject.SetActive(false);
					if (haircutNamesMatch(Names[i]))
					{
						currentIdx = i;
						data[Names[i]].gameObject.SetActive(true);
					}
				}
			}
			public void ShowByIndex(int idx)
			{
				if (idx >= 0 && idx < Names.Count)
					ShowByName(Names[idx]);
				else
					ShowByName(string.Empty);
			}
			public void ShowNext()
			{
				int idx = currentIdx - 1;
				if (idx < -1)
					idx = Names.Count - 1;
				ShowByIndex(idx);
			}
			public void ShowPrev()
			{
				int idx = currentIdx + 1;
				if (idx >= Names.Count)
					idx = -1;
				ShowByIndex(idx);
			}
		}

		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.KeepCPUCopyOfMesh = false;

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

				avatarGameObject.SetActive(false);
				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);
						materialAdjuster.ConfigureHairMaterial(haircutObject.renderer, haircutName);
						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);
						yield return materialAdjuster.ConfigureOutfitMaterial(outfitObject.renderer, loadedAvatarCode, outfitName);
						outfits.Add(outfitName, outfitObject);
					}
					else if (renderer.gameObject.name == "mesh")
					{
						body = new AvatarMeshObject(renderer.gameObject, renderer);
						materialAdjuster.ConfigureBodyMaterial(body.renderer, loadedAvatarCode, "");
						RenameBlendshapes(body.renderer.sharedMesh);
					}
				}
				avatarGameObject.SetActive(true);

				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;
		}

		#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 void ShowHaircut(string haircutName)
		{
			haircuts.ShowByName(haircutName);
		}

		/// <summary>
		/// Shows haircut by index
		/// </summary>
		public void ShowHaircut(int haircutIdx)
		{
			haircuts.ShowByIndex(haircutIdx);
		}

		/// <summary>
		/// Shows next haircut
		/// </summary>
		public void ShowNextHaircut()
		{
			haircuts.ShowNext();
		}

		/// <summary>
		/// Shows previous haircut
		/// </summary>
		public void ShowPrevHaircut()
		{
			haircuts.ShowPrev();
		}

		#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;
		}

		/// <summary>
		/// Shows outfit by name
		/// </summary>
		public void ShowOutfit(string outfitName)
		{
			outfits.ShowByName(outfitName);
			materialAdjuster.ConfigureBodyMaterial(body.renderer, loadedAvatarCode, outfits.CurrentName);
		}

		/// <summary>
		/// Shows outfit by index
		/// </summary>
		public void ShowOutfit(int outfitIdx)
		{
			outfits.ShowByIndex(outfitIdx);
			materialAdjuster.ConfigureBodyMaterial(body.renderer, loadedAvatarCode, outfits.CurrentName);
		}
		#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);
			}
		}
	}
}
