/* 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>, August 2026
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

namespace AvatarSDKMove
{
	public class RuntimeAnimationGenerationSample : MonoBehaviour
	{
		private const string PendingAnimationsKey = "AvatarSDKMove.RuntimePendingAnimations";
		private const float StatusPollIntervalSeconds = 3.0f;
		private const int CompletedPageSize = 10;

		private static readonly Color InProgressColor = new Color(1.0f, 0.93f, 0.78f);
		private static readonly Color FailedColor = new Color(1.0f, 0.82f, 0.82f);
		private static readonly Color PlayingColor = new Color(0.8f, 0.88f, 1.0f);
		private static readonly Color DefaultColor = Color.white;

		public AnimationGenerator animationGenerator;
		public GameObject targetModel;
		public AccountPanel accountPanel;

		[Header("Generation")]
		public GameObject generationPanel;
		public InputField promptInputField;
		public Button generateButton;
		public Slider durationSlider;
		public Text durationText;
		public Text priceText;
		public Image priceCoinIcon;

		[Header("History")]
		public RectTransform historyContent;
		public Text historyHeaderTemplate;
		public RuntimeHistoryItem historyItemTemplate;
		public Button showMoreButton;

		public Text statusText;

		private readonly List<TextToAnimationData> history = new List<TextToAnimationData>();
		private readonly List<GameObject> historyObjects = new List<GameObject>();
		private readonly HashSet<string> unsupportedCodes = new HashSet<string>();
		private string nextPageUrl;
		private int completedVisibleCount = CompletedPageSize;
		private string playingCode;
		private string loadingCode;
		private Text generateButtonText;
		private bool isLoadingHistory = false;
		private Coroutine pollingRoutine;

		private void Awake()
		{
			generateButtonText = generateButton.GetComponentInChildren<Text>();
			historyHeaderTemplate.gameObject.SetActive(false);
			historyItemTemplate.gameObject.SetActive(false);

			generateButton.onClick.AddListener(OnGenerateClicked);
			showMoreButton.onClick.AddListener(OnShowMoreClicked);
			durationSlider.onValueChanged.AddListener(OnDurationChanged);
			animationGenerator.onProgress.AddListener(SetStatus);
			accountPanel.SignedInChanged += OnSignedInChanged;
			accountPanel.UserInfoChanged += OnUserInfoChanged;

			durationSlider.wholeNumbers = true;
			durationSlider.minValue = AvatarSDKMoveSettings.MinAnimationDuration;
			durationSlider.maxValue = AvatarSDKMoveSettings.MaxAnimationDuration;
		}

		private void Start()
		{
			OnSignedInChanged(accountPanel.IsSignedIn);
			UpdateDurationTexts();
		}

		private void OnDestroy()
		{
			generateButton.onClick.RemoveListener(OnGenerateClicked);
			showMoreButton.onClick.RemoveListener(OnShowMoreClicked);
			durationSlider.onValueChanged.RemoveListener(OnDurationChanged);
			animationGenerator.onProgress.RemoveListener(SetStatus);
			accountPanel.SignedInChanged -= OnSignedInChanged;
			accountPanel.UserInfoChanged -= OnUserInfoChanged;
		}

		private void OnSignedInChanged(bool isSignedIn)
		{
			generationPanel.SetActive(isSignedIn);
			SetStatus(string.Empty);

			if (pollingRoutine != null)
			{
				StopCoroutine(pollingRoutine);
				pollingRoutine = null;
			}

			history.Clear();
			nextPageUrl = null;
			completedVisibleCount = CompletedPageSize;
			RebuildHistory();

			if (isSignedIn)
			{
				LoadHistory();
				pollingRoutine = StartCoroutine(PollInProgressAnimations());
			}
		}

		private void OnUserInfoChanged(UserInfoData userInfo)
		{
			UpdateDurationTexts();
		}

		private bool IsTopUpRequired()
		{
			UserInfoData userInfo = accountPanel.UserInfo;
			return userInfo != null && !GenerationPricing.CanAfford(userInfo, GetDuration());
		}

		private int GetDuration()
		{
			return Mathf.RoundToInt(durationSlider.value);
		}

		private void OnDurationChanged(float value)
		{
			UpdateDurationTexts();
		}

		private void UpdateDurationTexts()
		{
			int duration = GetDuration();
			durationText.text = string.Format("Duration: {0} s", duration);
			int price = GenerationPricing.GetDisplayPrice(accountPanel.UserInfo, duration);
			priceText.text = price == 0 ? "Free" : price.ToString();
			if (priceCoinIcon != null)
				priceCoinIcon.gameObject.SetActive(price > 0);
			if (generateButtonText != null)
				generateButtonText.text = IsTopUpRequired() ? "Top Up Coins" : "Generate";
		}

		private string GetPrompt()
		{
			if (!string.IsNullOrWhiteSpace(promptInputField.text))
				return promptInputField.text;

			return (promptInputField.placeholder as Text)?.text;
		}

		private async void OnGenerateClicked()
		{
			if (IsTopUpRequired())
			{
				SetStatus("Not enough coins. Top up your balance in MetaPerson Creator and come back.");
				MetaPersonCreatorLink.OpenShop();
				return;
			}

			string prompt = GetPrompt();
			if (string.IsNullOrWhiteSpace(prompt))
			{
				SetStatus("Describe the animation you want to create.");
				return;
			}

			generateButton.interactable = false;
			try
			{
				TextToAnimationData animation = await animationGenerator.StartGeneration(prompt, GetDuration());
				AddPendingAnimation(animation);
				MergeIntoHistory(animation);
				RebuildHistory();
				SetStatus("The animation is being generated. It will play automatically when it is ready.");
			}
			catch (NotEnoughCoinsException ex)
			{
				SetStatus(ex.Message + " Top up your balance in MetaPerson Creator.");
			}
			catch (Exception ex)
			{
				Debug.LogException(ex);
				SetStatus("Could not start the generation: " + ex.Message);
			}
			finally
			{
				generateButton.interactable = true;
			}

			accountPanel.RefreshUserInfo();
		}

		private async void LoadHistory()
		{
			await LoadHistoryPage(null);

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

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

			RebuildHistory();
		}

		private async Task LoadHistoryPage(string pageUrl)
		{
			isLoadingHistory = true;
			UpdateShowMoreButton();
			try
			{
				AnimationsPage page = await animationGenerator.Connection.GetAnimations(pageUrl);
				foreach (TextToAnimationData animation in page.animations)
					MergeIntoHistory(animation);
				nextPageUrl = page.nextPageUrl;
			}
			catch (Exception ex)
			{
				Debug.LogWarningFormat("Could not load previous animations: {0}", ex.Message);
				SetStatus("Could not load previous animations: " + ex.Message);
			}
			finally
			{
				isLoadingHistory = false;
				RebuildHistory();
			}
		}

		private async void OnShowMoreClicked()
		{
			completedVisibleCount += CompletedPageSize;
			while (history.Count(a => !a.IsInProgress) < completedVisibleCount && !string.IsNullOrEmpty(nextPageUrl))
			{
				string pageUrl = nextPageUrl;
				await LoadHistoryPage(pageUrl);
				if (nextPageUrl == pageUrl)
					break;
			}
			RebuildHistory();
		}

		private IEnumerator PollInProgressAnimations()
		{
			var wait = new WaitForSecondsRealtime(StatusPollIntervalSeconds);
			while (true)
			{
				yield return wait;

				List<TextToAnimationData> inProgress = history.Where(a => a.IsInProgress).ToList();
				if (inProgress.Count == 0)
					continue;

				Task polling = UpdateAnimations(inProgress);
				while (!polling.IsCompleted)
					yield return null;

				RebuildHistory();
			}
		}

		private async Task UpdateAnimations(List<TextToAnimationData> animations)
		{
			foreach (TextToAnimationData animation in animations)
			{
				try
				{
					MergeIntoHistory(await animationGenerator.Connection.GetAnimation(animation.url));
				}
				catch (Exception ex)
				{
					Debug.LogWarningFormat("Could not update the status of animation {0}: {1}", animation.code, ex.Message);
				}
			}
		}

		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 void OnPendingAnimationFinished(TextToAnimationData animation)
		{
			if (animation.IsFailed)
				SetStatus("Generation failed. " + AnimationGenerationFailedException.GetSupportMessage(animation.code));
			else
				PlayAnimation(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 bool IsPlayable(TextToAnimationData animation)
		{
			if (!animation.IsCompleted || unsupportedCodes.Contains(animation.code))
				return false;

			string format = animation.Format;
			return format == null || string.Equals(format, "glb", StringComparison.OrdinalIgnoreCase);
		}

		private async void PlayAnimation(TextToAnimationData animation)
		{
			if (!IsPlayable(animation) || !string.IsNullOrEmpty(loadingCode))
				return;

			loadingCode = animation.code;
			RebuildHistory();
			try
			{
				await animationGenerator.Play(targetModel, animation);
				playingCode = animation.code;
			}
			catch (NotSupportedException)
			{
				unsupportedCodes.Add(animation.code);
				SetStatus("This animation was generated in a format the sample can't play. Only GLB animations can be played at runtime.");
			}
			catch (Exception ex)
			{
				Debug.LogException(ex);
				SetStatus("Could not play the animation: " + ex.Message);
			}
			finally
			{
				loadingCode = null;
				RebuildHistory();
			}
		}

		private void RebuildHistory()
		{
			foreach (GameObject historyObject in historyObjects)
				Destroy(historyObject);
			historyObjects.Clear();

			List<TextToAnimationData> inProgress = history.Where(a => a.IsInProgress).ToList();
			List<TextToAnimationData> finished = history.Where(a => !a.IsInProgress).ToList();

			if (inProgress.Count > 0)
			{
				AddHeader("In Progress");
				foreach (TextToAnimationData animation in inProgress)
					AddItem(animation);
			}

			AddHeader(finished.Count == 0 && !isLoadingHistory ? "No animations yet" : "Completed");
			foreach (TextToAnimationData animation in finished.Take(completedVisibleCount))
				AddItem(animation);

			UpdateShowMoreButton();
		}

		private void UpdateShowMoreButton()
		{
			bool hasMore = history.Count(a => !a.IsInProgress) > completedVisibleCount || !string.IsNullOrEmpty(nextPageUrl);
			showMoreButton.gameObject.SetActive(hasMore || isLoadingHistory);
			showMoreButton.interactable = !isLoadingHistory;
			showMoreButton.transform.SetAsLastSibling();
		}

		private void AddHeader(string title)
		{
			Text header = Instantiate(historyHeaderTemplate, historyContent);
			header.text = title;
			header.gameObject.SetActive(true);
			historyObjects.Add(header.gameObject);
		}

		private void AddItem(TextToAnimationData animation)
		{
			RuntimeHistoryItem item = Instantiate(historyItemTemplate, historyContent);
			item.promptText.text = string.IsNullOrEmpty(animation.Prompt) ? animation.code : animation.Prompt;
			item.infoText.text = GetItemInfo(animation);
			item.background.color = GetItemColor(animation);
			item.button.interactable = IsPlayable(animation) && string.IsNullOrEmpty(loadingCode);
			item.button.onClick.AddListener(() => PlayAnimation(animation));
			item.gameObject.SetActive(true);
			historyObjects.Add(item.gameObject);
		}

		private string GetItemInfo(TextToAnimationData animation)
		{
			if (animation.IsInProgress)
				return new GenerationTimeEstimate(animation.created_on, animation.Duration).FormatStatus(animation.status);
			if (animation.IsFailed)
				return "Failed";
			string format = animation.Format;
			if (format == null)
				return "Unknown format";
			return string.Equals(format, "glb", StringComparison.OrdinalIgnoreCase)
				? "GLB"
				: format.ToUpperInvariant() + " is not supported in this sample";
		}

		private Color GetItemColor(TextToAnimationData animation)
		{
			if (animation.IsInProgress)
				return InProgressColor;
			if (animation.IsFailed)
				return FailedColor;
			if (animation.code == playingCode || animation.code == loadingCode)
				return PlayingColor;
			return DefaultColor;
		}

		private static string GetPendingAnimationsKey()
		{
			return PendingAnimationsKey + AvatarSDKMoveServer.SessionKeySuffix;
		}

		private static Dictionary<string, string> LoadPendingAnimations()
		{
			var pending = new Dictionary<string, string>();
			string stored = PlayerPrefs.GetString(GetPendingAnimationsKey(), 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)
				PlayerPrefs.DeleteKey(GetPendingAnimationsKey());
			else
				PlayerPrefs.SetString(GetPendingAnimationsKey(), string.Join("\n", pending.Select(p => p.Key + " " + p.Value)));
			PlayerPrefs.Save();
		}

		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 void SetStatus(string message)
		{
			if (statusText != null)
				statusText.text = message;
		}
	}
}
