/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated January 1, 2020. Replaces all prior versions.
 *
 * Copyright (c) 2013-2020, 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

using System.Collections.Generic;
using UnityEngine;

namespace Spine.Unity {

#if NEW_PREFAB_SYSTEM
	[ExecuteAlways]
#else
	[ExecuteInEditMode]
#endif
	[HelpURL("http://esotericsoftware.com/spine-unity#BoundingBoxFollower")]
	public class BoundingBoxFollower : MonoBehaviour {
		internal static bool DebugMessages = true;

		#region Inspector
		public SkeletonRenderer skeletonRenderer;
		[SpineSlot(dataField: "skeletonRenderer", containsBoundingBoxes: true)]
		public string slotName;
		public bool isTrigger, usedByEffector, usedByComposite;
		public bool clearStateOnDisable = true;
		#endregion

		Slot slot;
		BoundingBoxAttachment currentAttachment;
		string currentAttachmentName;
		PolygonCollider2D currentCollider;

		public readonly Dictionary<BoundingBoxAttachment, PolygonCollider2D> colliderTable = new Dictionary<BoundingBoxAttachment, PolygonCollider2D>();
		public readonly Dictionary<BoundingBoxAttachment, string> nameTable = new Dictionary<BoundingBoxAttachment, string>();

		public Slot Slot { get { return slot; } }
		public BoundingBoxAttachment CurrentAttachment { get { return currentAttachment; } }
		public string CurrentAttachmentName { get { return currentAttachmentName; } }
		public PolygonCollider2D CurrentCollider { get { return currentCollider; } }
		public bool IsTrigger { get { return isTrigger; } }

		void Start () {
			Initialize();
		}

		void OnEnable () {
			if (skeletonRenderer != null) {
				skeletonRenderer.OnRebuild -= HandleRebuild;
				skeletonRenderer.OnRebuild += HandleRebuild;
			}

			Initialize();
		}

		void HandleRebuild (SkeletonRenderer sr) {
			//if (BoundingBoxFollower.DebugMessages) Debug.Log("Skeleton was rebuilt. Repopulating BoundingBoxFollower.");
			Initialize();
		}

		/// <summary>
		/// Initialize and instantiate the BoundingBoxFollower colliders. This is method checks if the BoundingBoxFollower has already been initialized for the skeleton instance and slotName and prevents overwriting unless it detects a new setup.</summary>
		public void Initialize (bool overwrite = false) {
			if (skeletonRenderer == null)
				return;

			skeletonRenderer.Initialize(false);

			if (string.IsNullOrEmpty(slotName))
				return;

			// Don't reinitialize if the setup did not change.
			if (!overwrite &&
				colliderTable.Count > 0 && slot != null &&    // Slot is set and colliders already populated.
				skeletonRenderer.skeleton == slot.Skeleton && // Skeleton object did not change.
				slotName == slot.Data.Name                    // Slot object did not change.
			)
				return;

			slot = null;
			currentAttachment = null;
			currentAttachmentName = null;
			currentCollider = null;
			colliderTable.Clear();
			nameTable.Clear();

			var skeleton = skeletonRenderer.skeleton;
			if (skeleton == null)
				return;
			slot = skeleton.FindSlot(slotName);
			if (slot == null) {
				if (BoundingBoxFollower.DebugMessages)
					Debug.LogWarning(string.Format("Slot '{0}' not found for BoundingBoxFollower on '{1}'. (Previous colliders were disposed.)", slotName, this.gameObject.name));
				return;
			}
			int slotIndex = slot.Data.Index;

			int requiredCollidersCount = 0;
			var colliders = GetComponents<PolygonCollider2D>();
			if (this.gameObject.activeInHierarchy) {
				foreach (var skin in skeleton.Data.Skins)
					AddCollidersForSkin(skin, slotIndex, colliders, ref requiredCollidersCount);

				if (skeleton.Skin != null)
					AddCollidersForSkin(skeleton.Skin, slotIndex, colliders, ref requiredCollidersCount);
			}
			DisposeExcessCollidersAfter(requiredCollidersCount);

			if (BoundingBoxFollower.DebugMessages) {
				bool valid = colliderTable.Count != 0;
				if (!valid) {
					if (this.gameObject.activeInHierarchy)
						Debug.LogWarning("Bounding Box Follower not valid! Slot [" + slotName + "] does not contain any Bounding Box Attachments!");
					else
						Debug.LogWarning("Bounding Box Follower tried to rebuild as a prefab.");
				}
			}
		}

		void AddCollidersForSkin (Skin skin, int slotIndex, PolygonCollider2D[] previousColliders, ref int collidersCount) {
			if (skin == null) return;
			var skinEntries = new List<Skin.SkinEntry>();
			skin.GetAttachments(slotIndex, skinEntries);

			foreach (var entry in skinEntries) {
				var attachment = skin.GetAttachment(slotIndex, entry.Name);
				var boundingBoxAttachment = attachment as BoundingBoxAttachment;

				if (BoundingBoxFollower.DebugMessages && attachment != null && boundingBoxAttachment == null)
					Debug.Log("BoundingBoxFollower tried to follow a slot that contains non-boundingbox attachments: " + slotName);

				if (boundingBoxAttachment != null) {
					if (!colliderTable.ContainsKey(boundingBoxAttachment)) {
						var bbCollider = collidersCount < previousColliders.Length ?
							previousColliders[collidersCount] : gameObject.AddComponent<PolygonCollider2D>();
						++collidersCount;
						SkeletonUtility.SetColliderPointsLocal(bbCollider, slot, boundingBoxAttachment);
						bbCollider.isTrigger = isTrigger;
						bbCollider.usedByEffector = usedByEffector;
						bbCollider.usedByComposite = usedByComposite;
						bbCollider.enabled = false;
						bbCollider.hideFlags = HideFlags.NotEditable;
						colliderTable.Add(boundingBoxAttachment, bbCollider);
						nameTable.Add(boundingBoxAttachment, entry.Name);
					}
				}
			}
		}

		void OnDisable () {
			if (clearStateOnDisable)
				ClearState();

			if (skeletonRenderer != null)
				skeletonRenderer.OnRebuild -= HandleRebuild;
		}

		public void ClearState () {
			if (colliderTable != null)
				foreach (var col in colliderTable.Values)
					col.enabled = false;

			currentAttachment = null;
			currentAttachmentName = null;
			currentCollider = null;
		}

		void DisposeExcessCollidersAfter (int requiredCount) {
			var colliders = GetComponents<PolygonCollider2D>();
			if (colliders.Length == 0) return;

			for (int i = requiredCount; i < colliders.Length; ++i) {
				var collider = colliders[i];
				if (collider != null) {
#if UNITY_EDITOR
					if (Application.isEditor && !Application.isPlaying)
						DestroyImmediate(collider);
					else
#endif
					Destroy(collider);
				}
			}
		}

		void LateUpdate () {
			if (slot != null && slot.Attachment != currentAttachment)
				MatchAttachment(slot.Attachment);
		}

		/// <summary>Sets the current collider to match attachment.</summary>
		/// <param name="attachment">If the attachment is not a bounding box, it will be treated as null.</param>
		void MatchAttachment (Attachment attachment) {
			var bbAttachment = attachment as BoundingBoxAttachment;

			if (BoundingBoxFollower.DebugMessages && attachment != null && bbAttachment == null)
				Debug.LogWarning("BoundingBoxFollower tried to match a non-boundingbox attachment. It will treat it as null.");

			if (currentCollider != null)
				currentCollider.enabled = false;

			if (bbAttachment == null) {
				currentCollider = null;
				currentAttachment = null;
				currentAttachmentName = null;
			} else {
				PolygonCollider2D foundCollider;
				colliderTable.TryGetValue(bbAttachment, out foundCollider);
				if (foundCollider != null) {
					currentCollider = foundCollider;
					currentCollider.enabled = true;
					currentAttachment = bbAttachment;
					currentAttachmentName = nameTable[bbAttachment];
				} else {
					currentCollider = null;
					currentAttachment = bbAttachment;
					currentAttachmentName = null;
					if (BoundingBoxFollower.DebugMessages) Debug.LogFormat("Collider for BoundingBoxAttachment named '{0}' was not initialized. It is possibly from a new skin. currentAttachmentName will be null. You may need to call BoundingBoxFollower.Initialize(overwrite: true);", bbAttachment.Name);
				}
			}
		}
	}

}
