/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated July 28, 2023. Replaces all prior versions.
 *
 * Copyright (c) 2013-2023, Esoteric Software LLC
 *
 * Integration of the Spine Runtimes into software or otherwise creating
 * derivative works of the Spine Runtimes is permitted under the terms and
 * conditions of Section 2 of the Spine Editor License Agreement:
 * http://esotericsoftware.com/spine-editor-license
 *
 * Otherwise, it is permitted to integrate the Spine Runtimes into software or
 * otherwise create derivative works of the Spine Runtimes (collectively,
 * "Products"), provided that each user of the Products must obtain their own
 * Spine Editor license and redistribution of the Products in any form must
 * include this license and copyright notice.
 *
 * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
 * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
 * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#endif

#if UNITY_2019_3_OR_NEWER
#define SET_VERTICES_HAS_LENGTH_PARAMETER
#endif

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Spine.Unity.Examples {

#if NEW_PREFAB_SYSTEM
	[ExecuteAlways]
#else
	[ExecuteInEditMode]
#endif
	public class RenderCombinedMesh : MonoBehaviour {
		public SkeletonRenderer skeletonRenderer;
		public SkeletonRenderSeparator renderSeparator;
		public MeshRenderer[] referenceRenderers;

		bool updateViaSkeletonCallback = false;
		MeshFilter[] referenceMeshFilters;
		MeshRenderer ownRenderer;
		MeshFilter ownMeshFilter;

		protected DoubleBuffered<Mesh> doubleBufferedMesh;
		protected ExposedList<Vector3> positionBuffer;
		protected ExposedList<Color32> colorBuffer;
		protected ExposedList<Vector2> uvBuffer;
		protected ExposedList<int> indexBuffer;

#if UNITY_EDITOR
		private void Reset () {
			if (skeletonRenderer == null)
				skeletonRenderer = this.GetComponentInParent<SkeletonRenderer>();
			GatherRenderers();

			Awake();
			if (referenceRenderers.Length > 0)
				ownRenderer.sharedMaterial = referenceRenderers[0].sharedMaterial;

			LateUpdate();
		}
#endif
		protected void GatherRenderers () {
			referenceRenderers = this.GetComponentsInChildren<MeshRenderer>();
			if (referenceRenderers.Length == 0 ||
				(referenceRenderers.Length == 1 && referenceRenderers[0].gameObject == this.gameObject)) {
				Transform parent = this.transform.parent;
				if (parent)
					referenceRenderers = parent.GetComponentsInChildren<MeshRenderer>();
			}
			referenceRenderers = referenceRenderers.Where(
				(val, idx) => val.gameObject != this.gameObject && val.enabled).ToArray();
		}

		void Awake () {
			if (skeletonRenderer == null)
				skeletonRenderer = this.GetComponentInParent<SkeletonRenderer>();
			if (referenceRenderers == null || referenceRenderers.Length == 0) {
				GatherRenderers();
			}

			if (renderSeparator == null) {
				if (skeletonRenderer)
					renderSeparator = skeletonRenderer.GetComponent<SkeletonRenderSeparator>();
				else
					renderSeparator = this.GetComponentInParent<SkeletonRenderSeparator>();
			}

			int count = referenceRenderers.Length;
			referenceMeshFilters = new MeshFilter[count];
			for (int i = 0; i < count; ++i) {
				referenceMeshFilters[i] = referenceRenderers[i].GetComponent<MeshFilter>();
			}

			ownRenderer = this.GetComponent<MeshRenderer>();
			if (ownRenderer == null)
				ownRenderer = this.gameObject.AddComponent<MeshRenderer>();
			ownMeshFilter = this.GetComponent<MeshFilter>();
			if (ownMeshFilter == null)
				ownMeshFilter = this.gameObject.AddComponent<MeshFilter>();
		}

		void OnEnable () {
#if UNITY_EDITOR
			if (Application.isPlaying)
				Awake();
#endif
			if (skeletonRenderer) {
				skeletonRenderer.OnMeshAndMaterialsUpdated -= UpdateOnCallback;
				skeletonRenderer.OnMeshAndMaterialsUpdated += UpdateOnCallback;
				updateViaSkeletonCallback = true;
			}
			if (renderSeparator) {
				renderSeparator.OnMeshAndMaterialsUpdated -= UpdateOnCallback;
				renderSeparator.OnMeshAndMaterialsUpdated += UpdateOnCallback;
				updateViaSkeletonCallback = true;
			}
		}

		void OnDisable () {
			if (skeletonRenderer)
				skeletonRenderer.OnMeshAndMaterialsUpdated -= UpdateOnCallback;
			if (renderSeparator)
				renderSeparator.OnMeshAndMaterialsUpdated -= UpdateOnCallback;
		}

		void OnDestroy () {
			for (int i = 0; i < 2; ++i) {
				Mesh mesh = doubleBufferedMesh.GetNext();
#if UNITY_EDITOR
				if (Application.isEditor && !Application.isPlaying)
					UnityEngine.Object.DestroyImmediate(mesh);
				else
					UnityEngine.Object.Destroy(mesh);
#else
				UnityEngine.Object.Destroy(mesh);
#endif
			}
		}

		void LateUpdate () {
#if UNITY_EDITOR
			if (!Application.isPlaying) {
				UpdateMesh();
				return;
			}
#endif

			if (updateViaSkeletonCallback)
				return;
			UpdateMesh();
		}

		void UpdateOnCallback (SkeletonRenderer r) {
			UpdateMesh();
		}

		protected void EnsureBufferSizes (int combinedVertexCount, int combinedIndexCount) {
			if (positionBuffer == null) {
				positionBuffer = new ExposedList<Vector3>(combinedVertexCount);
				uvBuffer = new ExposedList<Vector2>(combinedVertexCount);
				colorBuffer = new ExposedList<Color32>(combinedVertexCount);
				indexBuffer = new ExposedList<int>(combinedIndexCount);
			}

			if (positionBuffer.Count != combinedVertexCount) {
				positionBuffer.Resize(combinedVertexCount);
				uvBuffer.Resize(combinedVertexCount);
				colorBuffer.Resize(combinedVertexCount);
			}
			if (indexBuffer.Count != combinedIndexCount) {
				indexBuffer.Resize(combinedIndexCount);
			}
		}

		void InitMesh () {
			if (doubleBufferedMesh == null) {
				doubleBufferedMesh = new DoubleBuffered<Mesh>();
				for (int i = 0; i < 2; ++i) {
					Mesh combinedMesh = doubleBufferedMesh.GetNext();
					combinedMesh.MarkDynamic();
					combinedMesh.name = "RenderCombinedMesh" + i;
					combinedMesh.subMeshCount = 1;
				}
			}
		}

		void UpdateMesh () {
			InitMesh();
			int combinedVertexCount = 0;
			int combinedIndexCount = 0;
			GetCombinedMeshInfo(ref combinedVertexCount, ref combinedIndexCount);

			EnsureBufferSizes(combinedVertexCount, combinedIndexCount);

			int combinedV = 0;
			int combinedI = 0;
			for (int r = 0, rendererCount = referenceMeshFilters.Length; r < rendererCount; ++r) {
				MeshFilter meshFilter = referenceMeshFilters[r];
				Mesh mesh = meshFilter.sharedMesh;
				if (mesh == null) continue;

				int vertexCount = mesh.vertexCount;
				Vector3[] positions = mesh.vertices;
				Vector2[] uvs = mesh.uv;
				Color32[] colors = mesh.colors32;

				System.Array.Copy(positions, 0, this.positionBuffer.Items, combinedV, vertexCount);
				System.Array.Copy(uvs, 0, this.uvBuffer.Items, combinedV, vertexCount);
				System.Array.Copy(colors, 0, this.colorBuffer.Items, combinedV, vertexCount);

				for (int s = 0, submeshCount = mesh.subMeshCount; s < submeshCount; ++s) {
					int submeshIndexCount = (int)mesh.GetIndexCount(s);
					int[] submeshIndices = mesh.GetIndices(s);
					int[] dstIndices = this.indexBuffer.Items;
					for (int i = 0; i < submeshIndexCount; ++i)
						dstIndices[i + combinedI] = submeshIndices[i] + combinedV;
					combinedI += submeshIndexCount;
				}
				combinedV += vertexCount;
			}

			Mesh combinedMesh = doubleBufferedMesh.GetNext();
			combinedMesh.Clear();
#if SET_VERTICES_HAS_LENGTH_PARAMETER
			combinedMesh.SetVertices(this.positionBuffer.Items, 0, this.positionBuffer.Count);
			combinedMesh.SetUVs(0, this.uvBuffer.Items, 0, this.uvBuffer.Count);
			combinedMesh.SetColors(this.colorBuffer.Items, 0, this.colorBuffer.Count);
			combinedMesh.SetTriangles(this.indexBuffer.Items, 0, this.indexBuffer.Count, 0);
#else
			// Note: excess already contains zero positions and indices after ExposedList.Resize().
			combinedMesh.vertices = this.positionBuffer.Items;
			combinedMesh.uv = this.uvBuffer.Items;
			combinedMesh.colors32 = this.colorBuffer.Items;
			combinedMesh.triangles = this.indexBuffer.Items;
#endif
			ownMeshFilter.sharedMesh = combinedMesh;
		}

		void GetCombinedMeshInfo (ref int vertexCount, ref int indexCount) {
			for (int r = 0, rendererCount = referenceMeshFilters.Length; r < rendererCount; ++r) {
				MeshFilter meshFilter = referenceMeshFilters[r];
				Mesh mesh = meshFilter.sharedMesh;
				if (mesh == null) continue;

				vertexCount += mesh.vertexCount;
				for (int s = 0, submeshCount = mesh.subMeshCount; s < submeshCount; ++s) {
					indexCount += (int)mesh.GetIndexCount(s);
				}
			}
		}
	}
}
