/* 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.Text;
using UnityEngine;

namespace AvatarSDKMove
{
	public static class GlbAnimationParser
	{
		private const uint GlbMagic = 0x46546C67;
		private const uint JsonChunkType = 0x4E4F534A;
		private const uint BinaryChunkType = 0x004E4942;

		private const int ComponentTypeFloat = 5126;

		private const string TranslationPath = "translation";
		private const string RotationPath = "rotation";

		private const string StepInterpolation = "STEP";
		private const string CubicSplineInterpolation = "CUBICSPLINE";

		private static readonly string[] PositionProperties = { "localPosition.x", "localPosition.y", "localPosition.z" };
		private static readonly string[] RotationProperties = { "localRotation.x", "localRotation.y", "localRotation.z", "localRotation.w" };

		public static AnimationClip Parse(byte[] data)
		{
			byte[] binaryChunk;
			GlbRoot root = ReadContainer(data, out binaryChunk);

			if (root.animations == null || root.animations.Length == 0)
				throw new Exception("The glb file doesn't contain any animation.");

			if (root.nodes == null || root.nodes.Length == 0)
				throw new Exception("The glb file doesn't contain any node.");

			int[] parentIndices = BuildParentIndices(root.nodes);
			GlbAnimation animation = root.animations[0];

			var clip = new AnimationClip
			{
				name = string.IsNullOrEmpty(animation.name) ? "Animation" : animation.name,
				legacy = true,
				wrapMode = WrapMode.Loop
			};

			if (animation.channels != null)
			{
				foreach (GlbAnimationChannel channel in animation.channels)
					AddChannel(clip, root, binaryChunk, parentIndices, animation, channel);
			}

			clip.EnsureQuaternionContinuity();
			return clip;
		}

		private static void AddChannel(AnimationClip clip, GlbRoot root, byte[] binaryChunk, int[] parentIndices,
			GlbAnimation animation, GlbAnimationChannel channel)
		{
			if (channel.target == null)
				return;

			string targetPath = channel.target.path;
			if (targetPath != TranslationPath && targetPath != RotationPath)
				return;

			if (animation.samplers == null || channel.sampler < 0 || channel.sampler >= animation.samplers.Length)
				throw new Exception(string.Format("Animation channel refers to a missing sampler: {0}", channel.sampler));

			GlbAnimationSampler sampler = animation.samplers[channel.sampler];
			if (sampler.interpolation == CubicSplineInterpolation)
				throw new Exception("CUBICSPLINE interpolation is not supported.");

			bool isStep = sampler.interpolation == StepInterpolation;

			float[] times = ReadAccessor(root, binaryChunk, sampler.input, 1);
			float[] values = ReadAccessor(root, binaryChunk, sampler.output, targetPath == RotationPath ? 4 : 3);

			int componentCount = targetPath == RotationPath ? 4 : 3;
			if (values.Length != times.Length * componentCount)
				throw new Exception("Animation sampler input and output sizes don't match.");

			string animationPath = BuildAnimationPath(root.nodes, parentIndices, channel.target.node);
			string[] properties = targetPath == RotationPath ? RotationProperties : PositionProperties;

			var curves = new AnimationCurve[componentCount];
			for (int i = 0; i < componentCount; i++)
				curves[i] = new AnimationCurve();

			float previousTime = float.NegativeInfinity;
			for (int keyIndex = 0; keyIndex < times.Length; keyIndex++)
			{
				float time = times[keyIndex];
				if (time <= previousTime)
					continue;

				previousTime = time;

				int offset = keyIndex * componentCount;
				for (int i = 0; i < componentCount; i++)
				{
					float value = ConvertComponent(targetPath, i, values[offset + i]);
					curves[i].AddKey(isStep
						? new Keyframe(time, value, float.PositiveInfinity, 0.0f)
						: new Keyframe(time, value, 0.0f, 0.0f));
				}
			}

			for (int i = 0; i < componentCount; i++)
				clip.SetCurve(animationPath, typeof(Transform), properties[i], curves[i]);
		}

		private static float ConvertComponent(string targetPath, int componentIndex, float value)
		{
			if (targetPath == RotationPath)
				return componentIndex == 1 || componentIndex == 2 ? -value : value;

			return componentIndex == 0 ? -value : value;
		}

		private static GlbRoot ReadContainer(byte[] data, out byte[] binaryChunk)
		{
			if (data == null || data.Length < 12)
				throw new Exception("The glb file is too short to be valid.");

			if (BitConverter.ToUInt32(data, 0) != GlbMagic)
				throw new Exception("The file is not a glb container.");

			uint version = BitConverter.ToUInt32(data, 4);
			if (version != 2)
				throw new Exception(string.Format("Unsupported glb version: {0}", version));

			string json = null;
			binaryChunk = null;

			int offset = 12;
			while (offset + 8 <= data.Length)
			{
				int chunkLength = (int)BitConverter.ToUInt32(data, offset);
				uint chunkType = BitConverter.ToUInt32(data, offset + 4);
				offset += 8;

				if (offset + chunkLength > data.Length)
					throw new Exception("The glb file contains a truncated chunk.");

				if (chunkType == JsonChunkType && json == null)
				{
					json = Encoding.UTF8.GetString(data, offset, chunkLength);
				}
				else if (chunkType == BinaryChunkType && binaryChunk == null)
				{
					binaryChunk = new byte[chunkLength];
					Buffer.BlockCopy(data, offset, binaryChunk, 0, chunkLength);
				}

				offset += chunkLength;
			}

			if (string.IsNullOrEmpty(json))
				throw new Exception("The glb file doesn't contain a json chunk.");

			GlbRoot root = JsonUtility.FromJson<GlbRoot>(json);
			if (root == null)
				throw new Exception("The glb json chunk couldn't be parsed.");

			return root;
		}

		private static int[] BuildParentIndices(GlbNode[] nodes)
		{
			var parentIndices = new int[nodes.Length];
			for (int i = 0; i < parentIndices.Length; i++)
				parentIndices[i] = -1;

			for (int i = 0; i < nodes.Length; i++)
			{
				int[] children = nodes[i].children;
				if (children == null)
					continue;

				foreach (int child in children)
				{
					if (child < 0 || child >= nodes.Length)
						throw new Exception(string.Format("Node {0} refers to a missing child {1}.", i, child));

					parentIndices[child] = i;
				}
			}

			return parentIndices;
		}

		private static string BuildAnimationPath(GlbNode[] nodes, int[] parentIndices, int nodeIndex)
		{
			if (nodeIndex < 0 || nodeIndex >= nodes.Length)
				throw new Exception(string.Format("Animation channel targets a missing node: {0}.", nodeIndex));

			var names = new List<string>();
			int currentIndex = nodeIndex;
			while (currentIndex >= 0)
			{
				string name = nodes[currentIndex].name;
				if (string.IsNullOrEmpty(name))
					throw new Exception(string.Format("Node {0} has no name, so an animation path can't be built.", currentIndex));

				names.Add(name);
				currentIndex = parentIndices[currentIndex];
			}

			var path = new StringBuilder();
			for (int i = names.Count - 1; i >= 0; i--)
			{
				if (path.Length > 0)
					path.Append('/');

				path.Append(names[i]);
			}

			return path.ToString();
		}

		private static float[] ReadAccessor(GlbRoot root, byte[] binaryChunk, int accessorIndex, int expectedComponentCount)
		{
			if (root.accessors == null || accessorIndex < 0 || accessorIndex >= root.accessors.Length)
				throw new Exception(string.Format("Missing accessor: {0}.", accessorIndex));

			GlbAccessor accessor = root.accessors[accessorIndex];
			if (accessor.componentType != ComponentTypeFloat)
				throw new Exception(string.Format("Unsupported accessor component type: {0}. Only float is supported.", accessor.componentType));

			if (accessor.normalized)
				throw new Exception("Normalized accessors are not supported.");

			if (GetComponentCount(accessor.type) != expectedComponentCount)
				throw new Exception(string.Format("Unexpected accessor type {0} for an animation channel.", accessor.type));

			if (root.bufferViews == null || accessor.bufferView < 0 || accessor.bufferView >= root.bufferViews.Length)
				throw new Exception(string.Format("Accessor {0} doesn't refer to a buffer view.", accessorIndex));

			GlbBufferView bufferView = root.bufferViews[accessor.bufferView];
			if (bufferView.buffer != 0)
				throw new Exception("Only the glb binary chunk is supported as a buffer.");

			if (binaryChunk == null)
				throw new Exception("The glb file doesn't contain a binary chunk.");

			int elementSize = expectedComponentCount * sizeof(float);
			int stride = bufferView.byteStride > 0 ? bufferView.byteStride : elementSize;
			int start = bufferView.byteOffset + accessor.byteOffset;

			if (start + (accessor.count - 1) * stride + elementSize > binaryChunk.Length)
				throw new Exception(string.Format("Accessor {0} reads outside of the binary chunk.", accessorIndex));

			var result = new float[accessor.count * expectedComponentCount];
			for (int i = 0; i < accessor.count; i++)
			{
				int elementOffset = start + i * stride;
				for (int component = 0; component < expectedComponentCount; component++)
					result[i * expectedComponentCount + component] = BitConverter.ToSingle(binaryChunk, elementOffset + component * sizeof(float));
			}

			return result;
		}

		private static int GetComponentCount(string accessorType)
		{
			switch (accessorType)
			{
				case "SCALAR": return 1;
				case "VEC2": return 2;
				case "VEC3": return 3;
				case "VEC4": return 4;
				default: return -1;
			}
		}

#pragma warning disable 0649

		[Serializable]
		private class GlbRoot
		{
			public GlbNode[] nodes;
			public GlbAnimation[] animations;
			public GlbAccessor[] accessors;
			public GlbBufferView[] bufferViews;
		}

		[Serializable]
		private class GlbNode
		{
			public string name;
			public int[] children;
		}

		[Serializable]
		private class GlbAnimation
		{
			public string name;
			public GlbAnimationChannel[] channels;
			public GlbAnimationSampler[] samplers;
		}

		[Serializable]
		private class GlbAnimationChannel
		{
			public int sampler = -1;
			public GlbAnimationChannelTarget target;
		}

		[Serializable]
		private class GlbAnimationChannelTarget
		{
			public int node = -1;
			public string path;
		}

		[Serializable]
		private class GlbAnimationSampler
		{
			public int input = -1;
			public int output = -1;
			public string interpolation;
		}

		[Serializable]
		private class GlbAccessor
		{
			public int bufferView = -1;
			public int byteOffset;
			public int componentType;
			public int count;
			public string type;
			public bool normalized;
		}

		[Serializable]
		private class GlbBufferView
		{
			public int buffer;
			public int byteOffset;
			public int byteLength;
			public int byteStride;
		}

#pragma warning restore 0649
	}
}
