/* Copyright (C) Itseez3D, Inc. - All Rights Reserved
* You may not use this file except in compliance with an authorized license
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
* UNLESS REQUIRED BY APPLICABLE LAW OR AGREED BY ITSEEZ3D, INC. IN WRITING, SOFTWARE DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED
* See the License for the specific language governing permissions and limitations under the License.
* Written by Itseez3D, Inc. <support@avatarsdk.com>, June 2026
*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;

namespace AvatarSDKMove.Editor
{
	public class AnimationGeneratorWindow : EditorWindow
	{
		private const string PendingAnimationsKey = "AvatarSDKMove.PendingAnimations";
		private const string NonImportableAnimationsKey = "AvatarSDKMove.NonImportableAnimations";
		private static readonly string[] ImportableExtensions = { ".fbx", ".glb" };
		private const double StatusPollIntervalSeconds = 3.0;
		private const int CompletedPageSize = 10;
		private const float SelectionStripeWidth = 3.0f;
		private const double FocusRefreshIntervalSeconds = 30.0;
		private const float AccountBarHeight = 24.0f;

		private GameObject previewModel;

		private string prompt = "a person is jumping";
		private int duration = AvatarSDKMoveSettings.MinAnimationDuration;

		private Connection connection = new Connection();
		private UserInfoData userInfo;
		private string sessionKey;
		private bool isBusy = false;
		private string statusMessage = string.Empty;
		private string controllerStatusMessage = string.Empty;

		[SerializeField] private AnimationClip[] previewClips = new AnimationClip[0];
		[SerializeField] private int previewClipIndex = 0;
		[SerializeField] private string previewAssetPath = string.Empty;

		private AnimationClipPreview preview = new AnimationClipPreview();
		private Vector2 scrollPosition = Vector2.zero;

		private readonly List<TextToAnimationData> history = new List<TextToAnimationData>();
		private string historyNextPageUrl;
		private bool isHistoryLoading = false;
		private string historyError = string.Empty;
		private double lastFocusRefreshTime = 0;
		private bool showInProgress = true;
		private bool showCompleted = true;
		private int completedVisibleCount = CompletedPageSize;
		private bool isPolling = false;
		private double nextPollTime = 0;
		private readonly HashSet<string> downloadingCodes = new HashSet<string>();
		private GUIStyle coinsStyle;
		private GUIStyle topUpButtonStyle;
		private GUIStyle middleLabelStyle;
		private readonly Dictionary<string, string> importedAnimationPaths = new Dictionary<string, string>();

		[MenuItem("Avatar SDK Move/Animation Generator", priority = 1)]
		public static void ShowWindow()
		{
			AnimationGeneratorWindow window = GetWindow<AnimationGeneratorWindow>("Animation Generator");
			window.minSize = new Vector2(340, 400);
			window.Show();
		}

		private void OnEnable()
		{
			UpdateWindowSize();
			sessionKey = GetSessionKey();
			AuthUtils.SessionChanged += OnSessionChanged;
			EditorApplication.update += OnEditorUpdate;
			RefreshUserInfo();
			LoadHistory();
		}

		private void OnDisable()
		{
			AuthUtils.SessionChanged -= OnSessionChanged;
			EditorApplication.update -= OnEditorUpdate;
			preview.Dispose();
		}

		private void OnSessionChanged()
		{
			string key = GetSessionKey();
			if (key == sessionKey)
				return;

			sessionKey = key;
			userInfo = null;
			ClearPendingAnimations();
			RefreshUserInfo();
			LoadHistory();
			Repaint();
		}

		private static string GetSessionKey()
		{
			return AvatarSDKMoveServer.Name + "|" + GetSessionEmail();
		}

		private static string GetSessionEmail()
		{
			UserSession session = AuthUtils.LoadSession();
			return session == null ? null : session.email;
		}

		private async void RefreshUserInfo()
		{
			if (!AuthUtils.HasSession())
				return;

			try
			{
				userInfo = await connection.GetUserInfo();
			}
			catch (SignInRequiredException)
			{
				userInfo = null;
			}
			catch (Exception ex)
			{
				Debug.LogWarningFormat("Could not update the Avatar SDK Move account info: {0}", ex.Message);
			}
			Repaint();
		}

		private void OnGUI()
		{
			if (!AuthUtils.HasSession())
			{
				DrawAuthenticationGUI();
				return;
			}

			DrawAccountBar();
			GUILayout.Space(5);
			DrawGenerationGUI();

			if (!string.IsNullOrEmpty(statusMessage))
				EditorGUILayout.HelpBox(statusMessage, MessageType.Info);

			GUILayout.Space(5);
			DrawSeparator();

			scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
			DrawLibraryHeader();
			GUILayout.Space(5);
			DrawHistoryGUI();
			EditorGUILayout.EndScrollView();

			if (GetPreviewClip() != null)
				Repaint();
		}

		private void DrawAccountBar()
		{
			if (coinsStyle == null)
			{
				coinsStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 15, alignment = TextAnchor.MiddleRight };
				topUpButtonStyle = new GUIStyle(GUI.skin.button) { fontStyle = FontStyle.Bold };
				middleLabelStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft };
			}

			using (new EditorGUILayout.HorizontalScope())
			{
				if (AvatarSDKMoveServer.IsOverridden)
				{
					Color contentColor = GUI.contentColor;
					GUI.contentColor = new Color(1.0f, 0.45f, 0.3f);
					GUILayout.Label(AvatarSDKMoveServer.Name.ToUpperInvariant(), EditorStyles.boldLabel, GUILayout.Height(AccountBarHeight));
					GUI.contentColor = contentColor;
				}

				string email = GetSessionEmail() ?? string.Empty;
				GUILayout.Label(email, middleLabelStyle, GUILayout.Height(AccountBarHeight));
				GUILayout.FlexibleSpace();
				if (userInfo == null)
					GUILayout.Label("...", coinsStyle, GUILayout.Height(AccountBarHeight));
				else
					CoinIcon.LayoutAmount(userInfo.balance, coinsStyle, AccountBarHeight);

				Color backgroundColor = GUI.backgroundColor;
				GUI.backgroundColor = TopUpButtonColor;
				if (GUILayout.Button("Top Up", topUpButtonStyle, GUILayout.Width(80), GUILayout.Height(AccountBarHeight)))
					MetaPersonCreatorLink.OpenShop();
				GUI.backgroundColor = backgroundColor;
			}
		}

		private static Color TopUpButtonColor
		{
			get { return EditorGUIUtility.isProSkin ? new Color(0.35f, 0.85f, 0.35f) : new Color(0.45f, 0.9f, 0.45f); }
		}

		private static void DrawSeparator()
		{
			Rect rect = GUILayoutUtility.GetRect(1, 1, GUILayout.ExpandWidth(true));
			EditorGUI.DrawRect(rect, EditorGUIUtility.isProSkin ? new Color(0.1f, 0.1f, 0.1f) : new Color(0.6f, 0.6f, 0.6f));
		}

		private void DrawLibraryHeader()
		{
			using (new EditorGUILayout.HorizontalScope())
			{
				float lineHeight = EditorGUIUtility.singleLineHeight;
				GUILayout.Label("Animations", EditorStyles.boldLabel, GUILayout.Height(lineHeight));
				GUILayout.FlexibleSpace();

				var previewModelLabel = new GUIContent("Preview Model",
					"Optional. A humanoid model to preview animations on and to assign a created Animator Controller to.");
				GUILayout.Label(previewModelLabel, middleLabelStyle, GUILayout.Height(lineHeight));
				previewModel = (GameObject)EditorGUILayout.ObjectField(previewModel, typeof(GameObject), true,
					GUILayout.Width(180), GUILayout.Height(lineHeight));
			}

			if (previewModel != null && !IsPreviewModelValid())
			{
				EditorGUILayout.HelpBox(
					"The Preview Model does not have a valid Humanoid Animator.\n" +
					"Make sure it contains an Animator component with an Avatar set to Humanoid.",
					MessageType.Error
				);
			}
		}

		private void DrawPreviewGUI()
		{
			AnimationClip previewClip = GetPreviewClip();
			if (previewClip == null)
				return;

			GUILayout.Space(5);
			if (previewClips.Length > 1)
			{
				string[] clipNames = previewClips.Select(clip => clip == null ? "<missing>" : clip.name).ToArray();
				previewClipIndex = EditorGUILayout.Popup("Clip", previewClipIndex, clipNames);
				previewClip = GetPreviewClip();
			}

			preview.SetClip(previewClip);
			preview.Draw(260, previewModel);
		}

		private AnimationClip GetPreviewClip()
		{
			if (previewClips == null || previewClipIndex < 0 || previewClipIndex >= previewClips.Length)
				return null;

			return previewClips[previewClipIndex];
		}

		private void SetPreviewClips(AnimationClip[] clips)
		{
			previewClips = clips ?? new AnimationClip[0];
			previewClipIndex = 0;
			preview.SetClip(GetPreviewClip());
			UpdateWindowSize();
		}

		private void UpdateWindowSize()
		{
			minSize = new Vector2(340, GetPreviewClip() != null ? 700 : 400);
		}

		private void DrawAuthenticationGUI()
		{
			GUILayout.Space(10);
			EditorGUILayout.HelpBox("Sign in to your Avatar SDK Move account to generate animations.", MessageType.Warning);

			if (GUILayout.Button("Open Account Window", GUILayout.Height(30)))
				AccountWindow.ShowWindow();
		}

		private void DrawAnimatorControllerGUI()
		{
			GUILayout.Space(5);

			bool isPreviewModelValid = IsPreviewModelValid();
			GUI.enabled = !isBusy && isPreviewModelValid;
			if (GUILayout.Button("Create Animator Controller", GUILayout.Height(30)))
			{
				CreateAnimatorController(previewAssetPath);
			}
			GUI.enabled = true;

			if (!isPreviewModelValid)
				EditorGUILayout.LabelField("Assign a humanoid Preview Model above to create an Animator Controller.", EditorStyles.wordWrappedMiniLabel);

			if (!string.IsNullOrEmpty(controllerStatusMessage))
			{
				GUILayout.Space(5);
				EditorGUILayout.HelpBox(controllerStatusMessage, MessageType.Info);
			}
		}

		private void DrawGenerationGUI()
		{
			using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
			{
				GUILayout.Label("Generate", EditorStyles.boldLabel);

				EditorGUILayout.LabelField("Prompt");
				prompt = EditorGUILayout.TextArea(prompt, GUILayout.Height(50));

				duration = EditorGUILayout.IntSlider("Duration, sec", duration,
					AvatarSDKMoveSettings.MinAnimationDuration, AvatarSDKMoveSettings.MaxAnimationDuration);

				GUILayout.Space(5);

				if (userInfo != null && !GenerationPricing.CanAfford(userInfo, duration))
				{
					EditorGUILayout.LabelField(
						string.Format("Not enough coins: this animation costs {0}. Top up your balance in MetaPerson Creator, then return to this window.",
							GenerationPricing.FormatPrice(userInfo, duration)),
						EditorStyles.wordWrappedMiniLabel);
					if (GUILayout.Button("Top Up Coins", GUILayout.Height(30)))
						MetaPersonCreatorLink.OpenShop();
					return;
				}

				GUI.enabled = !isBusy && !string.IsNullOrEmpty(prompt);
				if (CoinIcon.LayoutPriceButton("Generate", GenerationPricing.GetDisplayPrice(userInfo, duration), GUILayout.Height(30)))
					GenerateAnimation();
				GUI.enabled = true;
			}
		}

		private async void GenerateAnimation()
		{
			isBusy = true;
			SetControllerStatus(string.Empty);
			SetStatus("Starting animation generation...");
			try
			{
				TextToAnimationData animation = await connection.StartAnimationGeneration(prompt, duration, "fbx", SetStatus);

				AddPendingAnimation(animation);
				MergeIntoHistory(animation);
				showInProgress = true;
				SetStatus(string.Empty);
				nextPollTime = EditorApplication.timeSinceStartup + StatusPollIntervalSeconds;
			}
			catch (NotEnoughCoinsException ex)
			{
				SetStatus(ex.Message + " Top up your balance in MetaPerson Creator.");
			}
			catch (Exception ex)
			{
				Debug.LogException(ex);
				SetStatus("Animation generation failed: " + ex.Message);
			}
			finally
			{
				isBusy = false;
				Repaint();
			}

			RefreshUserInfo();
		}

		private static Dictionary<string, string> LoadPendingAnimations()
		{
			var pending = new Dictionary<string, string>();
			string stored = SessionState.GetString(PendingAnimationsKey, string.Empty);
			foreach (string line in stored.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries))
			{
				int separator = line.IndexOf(' ');
				if (separator > 0)
					pending[line.Substring(0, separator)] = line.Substring(separator + 1);
			}
			return pending;
		}

		private static void SavePendingAnimations(Dictionary<string, string> pending)
		{
			if (pending.Count == 0)
				SessionState.EraseString(PendingAnimationsKey);
			else
				SessionState.SetString(PendingAnimationsKey, string.Join("\n", pending.Select(p => p.Key + " " + p.Value)));
		}

		private static void AddPendingAnimation(TextToAnimationData animation)
		{
			Dictionary<string, string> pending = LoadPendingAnimations();
			pending[animation.code] = animation.url;
			SavePendingAnimations(pending);
		}

		private static bool RemovePendingAnimation(string code)
		{
			Dictionary<string, string> pending = LoadPendingAnimations();
			if (!pending.Remove(code))
				return false;

			SavePendingAnimations(pending);
			return true;
		}

		private static void ClearPendingAnimations()
		{
			SessionState.EraseString(PendingAnimationsKey);
		}

		private async void LoadHistory()
		{
			history.Clear();
			importedAnimationPaths.Clear();
			historyNextPageUrl = null;
			completedVisibleCount = CompletedPageSize;
			historyError = string.Empty;
			if (!AuthUtils.HasSession())
				return;

			lastFocusRefreshTime = EditorApplication.timeSinceStartup;
			await LoadHistoryPage(null, true);

			foreach (KeyValuePair<string, string> pending in LoadPendingAnimations())
			{
				if (history.Any(a => a.code == pending.Key))
					continue;

				try
				{
					MergeIntoHistory(await connection.GetAnimation(pending.Value));
				}
				catch (Exception ex)
				{
					Debug.LogWarningFormat("Could not get the status of animation {0}: {1}", pending.Key, ex.Message);
				}
			}
		}

		private void OnFocus()
		{
			if (!AuthUtils.HasSession() || isHistoryLoading)
				return;
			if (EditorApplication.timeSinceStartup - lastFocusRefreshTime < FocusRefreshIntervalSeconds)
				return;

			lastFocusRefreshTime = EditorApplication.timeSinceStartup;
			importedAnimationPaths.Clear();
			RefreshUserInfo();
			RefreshLatestHistory();
		}

		private async void RefreshLatestHistory()
		{
			await LoadHistoryPage(null, string.IsNullOrEmpty(historyNextPageUrl) && history.Count == 0);
		}

		private async Task LoadHistoryPage(string pageUrl, bool updateNextPage)
		{
			isHistoryLoading = true;
			Repaint();
			try
			{
				AnimationsPage page = await connection.GetAnimations(pageUrl);
				foreach (TextToAnimationData animation in page.animations)
					MergeIntoHistory(animation);
				if (updateNextPage)
					historyNextPageUrl = page.nextPageUrl;
				historyError = string.Empty;
			}
			catch (SignInRequiredException ex)
			{
				historyError = ex.Message;
			}
			catch (Exception ex)
			{
				Debug.LogWarningFormat("Could not load previous animations: {0}", ex.Message);
				historyError = "Could not load previous animations: " + ex.Message;
			}
			finally
			{
				isHistoryLoading = false;
				Repaint();
			}
		}

		private void MergeIntoHistory(TextToAnimationData animation)
		{
			int index = history.FindIndex(a => a.code == animation.code);
			if (index >= 0)
				history[index] = animation;
			else
				history.Add(animation);

			history.Sort((a, b) => DateTime.Compare(ParseCreatedOn(b), ParseCreatedOn(a)));

			if (!animation.IsInProgress && RemovePendingAnimation(animation.code))
				OnPendingAnimationFinished(animation);
		}

		private static DateTime ParseCreatedOn(TextToAnimationData animation)
		{
			DateTime createdOn;
			return DateTime.TryParse(animation.created_on, CultureInfo.InvariantCulture,
				DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out createdOn) ? createdOn : DateTime.MinValue;
		}

		private void OnEditorUpdate()
		{
			if (isPolling || EditorApplication.timeSinceStartup < nextPollTime)
				return;

			nextPollTime = EditorApplication.timeSinceStartup + StatusPollIntervalSeconds;
			if (history.Any(a => a.IsInProgress))
				PollInProgressAnimations();
		}

		private async void PollInProgressAnimations()
		{
			isPolling = true;
			try
			{
				foreach (TextToAnimationData animation in history.Where(a => a.IsInProgress).ToList())
				{
					MergeIntoHistory(await connection.GetAnimation(animation.url));
				}
			}
			catch (SignInRequiredException ex)
			{
				SetStatus(ex.Message);
			}
			catch (Exception ex)
			{
				Debug.LogWarningFormat("Could not update the animation status: {0}", ex.Message);
			}
			finally
			{
				isPolling = false;
				Repaint();
			}
		}

		private void OnPendingAnimationFinished(TextToAnimationData animation)
		{
			if (animation.IsCompleted && string.IsNullOrEmpty(FindImportedAnimation(animation.code)))
				ImportFromHistory(animation);
		}

		private void DrawHistoryGUI()
		{
			if (!string.IsNullOrEmpty(historyError))
			{
				EditorGUILayout.HelpBox(historyError, MessageType.Warning);
				GUI.enabled = !isHistoryLoading;
				if (GUILayout.Button("Retry"))
					LoadHistory();
				GUI.enabled = true;
			}

			List<TextToAnimationData> inProgress = history.Where(a => a.IsInProgress).ToList();
			if (inProgress.Count > 0)
			{
				showInProgress = EditorGUILayout.Foldout(showInProgress, string.Format("In Progress Animations ({0})", inProgress.Count), true, EditorStyles.foldoutHeader);
				if (showInProgress)
				{
					foreach (TextToAnimationData animation in inProgress)
						DrawHistoryItem(animation);
				}
				GUILayout.Space(10);
			}

			showCompleted = EditorGUILayout.Foldout(showCompleted, "Completed Animations", true, EditorStyles.foldoutHeader);

			if (!showCompleted)
				return;

			List<TextToAnimationData> completed = history.Where(a => !a.IsInProgress).ToList();
			if (completed.Count == 0)
				EditorGUILayout.LabelField(isHistoryLoading ? "Loading..." : "No animations yet.", EditorStyles.miniLabel);

			foreach (TextToAnimationData animation in completed.Take(completedVisibleCount))
				DrawHistoryItem(animation);

			if (completed.Count > completedVisibleCount || !string.IsNullOrEmpty(historyNextPageUrl))
			{
				GUI.enabled = !isHistoryLoading;
				if (GUILayout.Button(isHistoryLoading ? "Loading..." : "Show More"))
					ShowMoreCompleted();
				GUI.enabled = true;
			}
		}

		private async void ShowMoreCompleted()
		{
			completedVisibleCount += CompletedPageSize;
			while (history.Count(a => !a.IsInProgress) < completedVisibleCount && !string.IsNullOrEmpty(historyNextPageUrl))
			{
				string pageUrl = historyNextPageUrl;
				await LoadHistoryPage(pageUrl, true);
				if (historyNextPageUrl == pageUrl)
					break;
			}
			Repaint();
		}

		private void DrawHistoryItem(TextToAnimationData animation)
		{
			string importedPath = animation.IsCompleted ? GetImportedAnimation(animation.code) : null;

			bool isShown = IsShownInPreview(importedPath);
			using (var itemScope = new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
			{
				if (Event.current.type == EventType.Repaint)
				{
					if (isShown)
						DrawItemHighlight(itemScope.rect, SelectedAccentColor);
					else if (animation.IsFailed)
						DrawItemHighlight(itemScope.rect, FailedAccentColor);
					else if (animation.IsInProgress)
						DrawItemHighlight(itemScope.rect, InProgressAccentColor);
				}

				DrawHistoryItemHeader(animation, importedPath);

				if (isShown)
				{
					DrawPreviewGUI();
					DrawAnimatorControllerGUI();
				}
			}
		}

		private static Color SelectedAccentColor
		{
			get { return EditorGUIUtility.isProSkin ? new Color(0.17f, 0.36f, 0.53f) : new Color(0.23f, 0.45f, 0.69f); }
		}

		private static Color InProgressAccentColor
		{
			get { return EditorGUIUtility.isProSkin ? new Color(0.65f, 0.48f, 0.12f) : new Color(0.85f, 0.6f, 0.1f); }
		}

		private static Color FailedAccentColor
		{
			get { return EditorGUIUtility.isProSkin ? new Color(0.6f, 0.2f, 0.2f) : new Color(0.8f, 0.25f, 0.25f); }
		}

		private static void DrawItemHighlight(Rect rect, Color accent)
		{
			EditorGUI.DrawRect(rect, new Color(accent.r, accent.g, accent.b, 0.25f));
			EditorGUI.DrawRect(new Rect(rect.x, rect.y, SelectionStripeWidth, rect.height), accent);
		}

		private bool IsShownInPreview(string importedPath)
		{
			return !string.IsNullOrEmpty(importedPath) && importedPath == previewAssetPath;
		}

		private void DrawHistoryItemHeader(TextToAnimationData animation, string importedPath)
		{
			using (new EditorGUILayout.HorizontalScope())
			{
				using (new EditorGUILayout.VerticalScope())
				{
					string itemPrompt = string.IsNullOrEmpty(animation.Prompt) ? animation.code : animation.Prompt;
					EditorGUILayout.LabelField(itemPrompt, EditorStyles.wordWrappedLabel);

					var details = new List<string>();
					DateTime createdOn = ParseCreatedOn(animation);
					if (createdOn != DateTime.MinValue)
						details.Add(createdOn.ToLocalTime().ToString("g", CultureInfo.CurrentCulture));
					if (animation.Duration > 0)
						details.Add(string.Format("{0:0} s", animation.Duration));
					EditorGUILayout.LabelField(string.Join(", ", details), EditorStyles.miniLabel);

					if (!string.IsNullOrEmpty(importedPath))
						DrawReadOnlyAssetField(importedPath);

					if (animation.IsFailed)
						EditorGUILayout.LabelField("Generation failed. " + AnimationGenerationFailedException.GetSupportMessage(animation.code),
							EditorStyles.wordWrappedMiniLabel);
				}

				using (new EditorGUILayout.VerticalScope(GUILayout.Width(110)))
				{
					DrawHistoryItemAction(animation, importedPath);
				}
			}
		}

		private static void DrawReadOnlyAssetField(string assetPath)
		{
			UnityEngine.Object asset = AssetDatabase.LoadMainAssetAtPath(assetPath);
			Rect rect = EditorGUILayout.GetControlRect();

			Event current = Event.current;
			if (current.type == EventType.MouseDown && rect.Contains(current.mousePosition))
			{
				Selection.activeObject = asset;
				EditorGUIUtility.PingObject(asset);
				current.Use();
			}
			else if ((current.type == EventType.DragUpdated || current.type == EventType.DragPerform) && rect.Contains(current.mousePosition))
			{
				current.Use();
			}

			EditorGUI.ObjectField(rect, asset, typeof(UnityEngine.Object), false);
		}

		private void DrawHistoryItemAction(TextToAnimationData animation, string importedPath)
		{
			if (animation.IsFailed)
			{
				if (GUILayout.Button("Copy Code"))
				{
					EditorGUIUtility.systemCopyBuffer = animation.code;
					ShowNotification(new GUIContent("Animation code copied"));
				}
				return;
			}

			if (animation.IsInProgress)
			{
				var timeEstimate = new GenerationTimeEstimate(animation.created_on, animation.Duration);
				EditorGUILayout.LabelField(timeEstimate.FormatStatus(animation.status), EditorStyles.wordWrappedMiniLabel);
				return;
			}

			if (!string.IsNullOrEmpty(importedPath))
			{
				if (!IsFbxPath(importedPath))
					EditorGUILayout.LabelField("No preview for GLB", EditorStyles.wordWrappedMiniLabel);
				else if (IsShownInPreview(importedPath))
				{
					if (GUILayout.Button("Hide"))
						HideAnimationPreview();
				}
				else if (GUILayout.Button("Show"))
				{
					ShowAnimationPreview(importedPath);
				}
				return;
			}

			if (!IsSupportedFormat(animation))
			{
				EditorGUILayout.LabelField(string.Format("{0} is not supported by the plugin", animation.Format.ToUpperInvariant()),
					EditorStyles.wordWrappedMiniLabel);
				return;
			}

			if (!IsImportableAnimation(animation))
			{
				EditorGUILayout.LabelField("This format is not supported by the plugin", EditorStyles.wordWrappedMiniLabel);
				return;
			}

			bool isDownloading = downloadingCodes.Contains(animation.code);
			GUI.enabled = !isDownloading;
			if (GUILayout.Button(isDownloading ? "Importing..." : "Import"))
				ImportFromHistory(animation);
			GUI.enabled = true;
		}

		private static bool IsSupportedFormat(TextToAnimationData animation)
		{
			string format = animation.Format;
			return format == null
				|| string.Equals(format, "fbx", StringComparison.OrdinalIgnoreCase)
				|| string.Equals(format, "glb", StringComparison.OrdinalIgnoreCase);
		}

		private static bool IsImportableAnimation(TextToAnimationData animation)
		{
			return !GetNonImportableAnimations().Contains(animation.code);
		}

		private static bool IsFbxPath(string assetPath)
		{
			return assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase);
		}

		private static HashSet<string> GetNonImportableAnimations()
		{
			string stored = EditorPrefs.GetString(NonImportableAnimationsKey, string.Empty);
			return new HashSet<string>(stored.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
		}

		private static void MarkNonImportableAnimation(string code)
		{
			HashSet<string> codes = GetNonImportableAnimations();
			codes.Add(code);
			EditorPrefs.SetString(NonImportableAnimationsKey, string.Join(";", codes));
		}

		private string GetImportedAnimation(string code)
		{
			string path;
			if (!importedAnimationPaths.TryGetValue(code, out path))
			{
				path = FindImportedAnimation(code);
				importedAnimationPaths[code] = path;
			}
			return path;
		}

		private static string FindImportedAnimation(string code)
		{
			string animationDir = Path.Combine(AvatarSDKMoveSettings.TargetFolder, code);
			if (!Directory.Exists(animationDir))
				return null;

			string[] files = Directory.GetFiles(animationDir, "*", SearchOption.AllDirectories);
			string animationFile = ImportableExtensions
				.Select(extension => files.FirstOrDefault(f => f.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
				.FirstOrDefault(f => f != null);
			return animationFile == null ? null : animationFile.Replace("\\", "/");
		}

		private void ShowAnimationPreview(string assetPath)
		{
			previewAssetPath = assetPath;
			SetControllerStatus(string.Empty);
			SetPreviewClips(LoadAnimationClips(assetPath));
		}

		private void HideAnimationPreview()
		{
			previewAssetPath = string.Empty;
			SetControllerStatus(string.Empty);
			SetPreviewClips(null);
		}

		private async void ImportFromHistory(TextToAnimationData animation)
		{
			if (!downloadingCodes.Add(animation.code))
				return;

			try
			{
				byte[] data = await connection.DownloadAnimation(animation);

				string assetPath = SaveAnimationAsset(new GeneratedAnimation { code = animation.code, data = data });
				importedAnimationPaths.Remove(animation.code);
				if (string.IsNullOrEmpty(assetPath))
				{
					MarkNonImportableAnimation(animation.code);
					SetStatus("This animation was generated in a format the plugin doesn't support. Only FBX and GLB animations can be imported.");
					return;
				}

				ImportAnimationAsset(assetPath);
				SetStatus("Animation imported: " + assetPath);
			}
			catch (Exception ex)
			{
				Debug.LogException(ex);
				SetStatus("Could not import the animation: " + ex.Message);
			}
			finally
			{
				downloadingCodes.Remove(animation.code);
				Repaint();
			}
		}

		private static bool IsGlb(byte[] data)
		{
			return data != null && data.Length >= 4 && data[0] == 'g' && data[1] == 'l' && data[2] == 'T' && data[3] == 'F';
		}

		private static string PrepareAnimationDirectory(string code)
		{
			Directory.CreateDirectory(AvatarSDKMoveSettings.TargetFolder);

			string animationDir = Path.Combine(AvatarSDKMoveSettings.TargetFolder, code).Replace("\\", "/");
			if (Directory.Exists(animationDir))
				Directory.Delete(animationDir, true);
			Directory.CreateDirectory(animationDir);
			return animationDir;
		}

		private static string SaveAnimationAsset(GeneratedAnimation animation)
		{
			if (IsGlb(animation.data))
			{
				string glbDir = PrepareAnimationDirectory(animation.code);
				File.WriteAllBytes(Path.Combine(glbDir, "animation.glb"), animation.data);
			}
			else
			{
				using (var stream = new MemoryStream(animation.data))
				using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
				{
					bool hasAnimation = archive.Entries.Any(e => ImportableExtensions.Any(
						extension => e.FullName.EndsWith(extension, StringComparison.OrdinalIgnoreCase)));
					if (!hasAnimation)
						return null;

					archive.ExtractToDirectory(PrepareAnimationDirectory(animation.code));
				}
			}

			AssetDatabase.Refresh();

			return FindImportedAnimation(animation.code);
		}

		private bool IsPreviewModelValid()
		{
			if (previewModel == null)
				return false;

			Animator anim = previewModel.GetComponent<Animator>();
			return anim != null && anim.isHuman;
		}

		private void SetStatus(string message)
		{
			statusMessage = message;
			Repaint();
		}

		private void SetControllerStatus(string message)
		{
			controllerStatusMessage = message;
			Repaint();
		}

		private void ImportAnimationAsset(string assetPath)
		{
			AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

			if (AssetImporter.GetAtPath(assetPath) is ModelImporter)
				ConfigureModelImporter(assetPath);

			AssetDatabase.Refresh();

			Debug.Log($"Animation imported and configured: {assetPath}");
			LogAnimationInfo(assetPath, LoadAnimationClips(assetPath));
		}

		private static void LogAnimationInfo(string assetPath, AnimationClip[] clips)
		{
			foreach (AnimationClip clip in clips)
				Debug.Log($"Clip '{clip.name}': length={clip.length:0.###}s frameRate={clip.frameRate} frames={clip.length * clip.frameRate:0.##}");

			ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
			if (importer == null)
				return;

			foreach (ModelImporterClipAnimation take in importer.defaultClipAnimations)
				Debug.Log($"Take '{take.takeName}': firstFrame={take.firstFrame} lastFrame={take.lastFrame}");
		}

		private static AnimationClip[] LoadAnimationClips(string animationAssetPath)
		{
			return AssetDatabase.LoadAllAssetsAtPath(animationAssetPath)
								.OfType<AnimationClip>()
								.Where(clip => !clip.name.StartsWith("__preview__"))
								.ToArray();
		}

		private void CreateAnimatorController(string animationAssetPath)
		{
			AnimationClip[] clips = LoadAnimationClips(animationAssetPath);

			if (clips.Length == 0)
			{
				Debug.LogWarning("No usable animation clips found in the imported asset.");
				SetControllerStatus("No usable animation clips found in the imported asset.");
				return;
			}

			string controllerFileName = Path.GetFileNameWithoutExtension(animationAssetPath) + "_Animator.controller";
			string controllerPath = Path.Combine(Path.GetDirectoryName(animationAssetPath), controllerFileName).Replace("\\", "/");

			if (File.Exists(controllerPath))
				AssetDatabase.DeleteAsset(controllerPath);

			AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath);
			if (controller == null)
			{
				Debug.LogError("Failed to create Animator Controller.");
				SetControllerStatus("Failed to create Animator Controller.");
				return;
			}

			var stateMachine = controller.layers[0].stateMachine;
			foreach (AnimationClip clip in clips)
			{
				var state = stateMachine.AddState(clip.name);
				state.motion = clip;
				if (clips[0] == clip)
					stateMachine.defaultState = state;
			}

			EditorUtility.SetDirty(controller);
			AssetDatabase.SaveAssets();

			Animator anim = previewModel.GetComponent<Animator>();
			if (anim != null)
			{
				anim.runtimeAnimatorController = controller;
				EditorUtility.SetDirty(anim);
			}

			Selection.activeObject = controller;
			EditorGUIUtility.PingObject(controller);

			Debug.Log($"Animator Controller created and assigned to {previewModel.name}: {controllerPath}");
			SetControllerStatus("Animator Controller assigned: " + controllerPath);
		}

		public static void ConfigureModelImporter(string assetPath)
		{
			ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
			if (importer == null)
			{
				Debug.LogError($"Could not get ModelImporter for {assetPath}");
				return;
			}

			importer.animationType = ModelImporterAnimationType.Human;

			string avatarDefinitionPath = AssetDatabase.GUIDToAssetPath(AvatarSDKMoveSettings.AvatarDefinitionGuid);
			Avatar avatarDefinition = string.IsNullOrEmpty(avatarDefinitionPath)
				? null
				: AssetDatabase.LoadAssetAtPath<Avatar>(avatarDefinitionPath);

			if (avatarDefinition == null)
			{
				Debug.LogError($"Could not load the avatar definition asset (guid {AvatarSDKMoveSettings.AvatarDefinitionGuid})");
				importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel;
			}
			else
			{
				importer.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;
				importer.sourceAvatar = avatarDefinition;
			}

			importer.importAnimation = true;
			importer.motionNodeName = string.Empty;
			importer.resampleCurves = true;
			importer.animationCompression = ModelImporterAnimationCompression.Off;

			importer.SaveAndReimport();
		}
	}
}
