/* 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.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

namespace AvatarSDKMove
{
	public class AnimationGenerator : MonoBehaviour
	{
		private const string AnimationFormat = "glb";

		public HumanoidRetargeter humanoidRetargeter;

		public UnityEvent<string> onProgress;
		public UnityEvent onCompleted;
		public UnityEvent<string> onFailed;

		private readonly Connection connection = new Connection();
		private readonly Dictionary<string, AnimationClip> clipCache = new Dictionary<string, AnimationClip>();
		private Animation sourceAnimation;
		private AnimationClip currentClip;
		private bool isBusy = false;

		public bool IsBusy { get { return isBusy; } }

		public Connection Connection { get { return connection; } }

		public Task<TextToAnimationData> StartGeneration(string prompt, int duration)
		{
			return connection.StartAnimationGeneration(prompt, duration, AnimationFormat, ReportProgress);
		}

		public async Task Play(GameObject targetModel, TextToAnimationData animation)
		{
			AnimationClip clip;
			if (!clipCache.TryGetValue(animation.code, out clip))
			{
				ReportProgress("Downloading animation...");
				byte[] data = await connection.DownloadAnimation(animation);
				clip = GlbAnimationParser.Parse(ExtractGlb(data));
				clipCache[animation.code] = clip;
			}

			PlayClip(targetModel, clip);
		}

		public async Task Generate(GameObject targetModel, string prompt, int duration)
		{
			if (isBusy)
				return;

			isBusy = true;
			try
			{
				GeneratedAnimation animation = await connection.GenerateAnimation(prompt, duration, AnimationFormat, ReportProgress);

				ReportProgress("Loading animation...");
				AnimationClip clip = GlbAnimationParser.Parse(ExtractGlb(animation.data));
				clipCache[animation.code] = clip;

				PlayClip(targetModel, clip);
			}
			catch (Exception ex)
			{
				Debug.LogException(ex);
				onFailed.Invoke(ex.Message);
			}
			finally
			{
				isBusy = false;
			}
		}

		private void PlayClip(GameObject targetModel, AnimationClip clip)
		{
			humanoidRetargeter.targetModel = targetModel;
			humanoidRetargeter.Initialize();

			PlayOnSourceModel(clip);

			ReportProgress(string.Empty);
			onCompleted.Invoke();
		}

		private static byte[] ExtractGlb(byte[] data)
		{
			if (data == null || data.Length < 2 || data[0] != 'P' || data[1] != 'K')
				return data;

			using (var stream = new MemoryStream(data))
			using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
			{
				ZipArchiveEntry entry = archive.Entries.FirstOrDefault(e => e.FullName.EndsWith(".glb", StringComparison.OrdinalIgnoreCase));
				if (entry == null)
					throw new NotSupportedException("The animation archive doesn't contain a glb file.");

				using (var entryStream = entry.Open())
				using (var extracted = new MemoryStream())
				{
					entryStream.CopyTo(extracted);
					return extracted.ToArray();
				}
			}
		}

		private void PlayOnSourceModel(AnimationClip clip)
		{
			GameObject sourceModel = humanoidRetargeter.sourceModel;

			if (sourceAnimation == null)
			{
				sourceAnimation = sourceModel.GetComponent<Animation>();
				if (sourceAnimation == null)
					sourceAnimation = sourceModel.AddComponent<Animation>();
			}

			sourceAnimation.Stop();
			if (currentClip != null)
				sourceAnimation.RemoveClip(currentClip);

			currentClip = clip;
			sourceAnimation.AddClip(clip, clip.name);
			sourceAnimation.clip = clip;
			sourceAnimation.Play();
		}

		private void ReportProgress(string message)
		{
			onProgress.Invoke(message);
		}
	}
}
