/******************************************************************************
 * Spine Runtimes Software License v2.5
 *
 * Copyright (c) 2013-2016, Esoteric Software
 * All rights reserved.
 *
 * You are granted a perpetual, non-exclusive, non-sublicensable, and
 * non-transferable license to use, install, execute, and perform the Spine
 * Runtimes software and derivative works solely for personal or internal
 * use. Without the written permission of Esoteric Software (see Section 2 of
 * the Spine Software License Agreement), you may not (a) modify, translate,
 * adapt, or develop new applications using the Spine Runtimes or otherwise
 * create derivative works or improvements of the Spine Runtimes or (b) remove,
 * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
 * or other intellectual property or proprietary rights notices on or in the
 * Software, including any copy thereof. Redistributions in binary or source
 * form must include this license and terms.
 *
 * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

// Contributed by: Mitch Thompson

#define SPINE_SKELETON_ANIMATOR
//#define SPINE_BAKING

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Spine;

namespace Spine.Unity.Editor {
	using Event = UnityEngine.Event;
	using Icons = SpineEditorUtilities.Icons;

	[CustomEditor(typeof(SkeletonDataAsset)), CanEditMultipleObjects]
	public class SkeletonDataAssetInspector : UnityEditor.Editor {
		static bool showAnimationStateData = true;
		static bool showAnimationList = true;
		static bool showSlotList = false;
		static bool showAttachments = false;

		#if SPINE_BAKING
		static bool isBakingExpanded = false;
		static bool bakeAnimations = true;
		static bool bakeIK = true;
		static SendMessageOptions bakeEventOptions = SendMessageOptions.DontRequireReceiver;
		const string ShowBakingPrefsKey = "SkeletonDataAssetInspector_showUnity";
		#endif

		SerializedProperty atlasAssets, skeletonJSON, scale, fromAnimation, toAnimation, duration, defaultMix;
		#if SPINE_TK2D
		SerializedProperty spriteCollection;
		#endif

		#if SPINE_SKELETON_ANIMATOR
		static bool isMecanimExpanded = false;
		SerializedProperty controller;
		#endif

		bool m_initialized = false;
		SkeletonDataAsset m_skeletonDataAsset;
		SkeletonData m_skeletonData;
		string m_skeletonDataAssetGUID;
		bool needToSerialize;

		readonly List<string> warnings = new List<string>();

		GUIStyle activePlayButtonStyle, idlePlayButtonStyle;
		readonly GUIContent DefaultMixLabel = new GUIContent("Default Mix Duration", "Sets 'SkeletonDataAsset.defaultMix' in the asset and 'AnimationState.data.defaultMix' at runtime load time.");

		void OnEnable () {
			SpineEditorUtilities.ConfirmInitialization();
			m_skeletonDataAsset = (SkeletonDataAsset)target;

			// Clear empty atlas array items.
			{
				bool hasNulls = false;
				foreach (var a in m_skeletonDataAsset.atlasAssets) {
					if (a == null) {
						hasNulls = true;
						break;
					}
				}
				if (hasNulls) {
					var trimmedAtlasAssets = new List<AtlasAsset>();
					foreach (var a in m_skeletonDataAsset.atlasAssets) {
						if (a != null) trimmedAtlasAssets.Add(a);
					}
					m_skeletonDataAsset.atlasAssets = trimmedAtlasAssets.ToArray();
				}
			}

			atlasAssets = serializedObject.FindProperty("atlasAssets");
			skeletonJSON = serializedObject.FindProperty("skeletonJSON");
			scale = serializedObject.FindProperty("scale");
			fromAnimation = serializedObject.FindProperty("fromAnimation");
			toAnimation = serializedObject.FindProperty("toAnimation");
			duration = serializedObject.FindProperty("duration");
			defaultMix = serializedObject.FindProperty("defaultMix");

			#if SPINE_SKELETON_ANIMATOR
			controller = serializedObject.FindProperty("controller");
			#endif

			#if SPINE_TK2D
			atlasAssets.isExpanded = false;
			spriteCollection = serializedObject.FindProperty("spriteCollection");
			#else
			atlasAssets.isExpanded = true;
			#endif

			#if SPINE_BAKING
			isBakingExpanded = EditorPrefs.GetBool(ShowBakingPrefsKey, false);
			#endif

			m_skeletonDataAssetGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m_skeletonDataAsset));
			EditorApplication.update += EditorUpdate;
			m_skeletonData = m_skeletonDataAsset.GetSkeletonData(false);
			RepopulateWarnings();
		}

		void OnDestroy () {
			m_initialized = false;
			EditorApplication.update -= EditorUpdate;
			this.DestroyPreviewInstances();
			if (this.m_previewUtility != null) {
				this.m_previewUtility.Cleanup();
				this.m_previewUtility = null;
			}
		}

		override public void OnInspectorGUI () {
			if (serializedObject.isEditingMultipleObjects) {
				using (new SpineInspectorUtility.BoxScope()) {
					EditorGUILayout.LabelField("SkeletonData", EditorStyles.boldLabel);
					EditorGUILayout.PropertyField(skeletonJSON, new GUIContent(skeletonJSON.displayName, Icons.spine));
					EditorGUILayout.PropertyField(scale);
				}

				using (new SpineInspectorUtility.BoxScope()) {
					EditorGUILayout.LabelField("Atlas", EditorStyles.boldLabel);
					#if !SPINE_TK2D
					EditorGUILayout.PropertyField(atlasAssets, true);
					#else
					using (new EditorGUI.DisabledGroupScope(spriteCollection.objectReferenceValue != null)) {
						EditorGUILayout.PropertyField(atlasAssets, true);
					}
					EditorGUILayout.LabelField("spine-tk2d", EditorStyles.boldLabel);
					EditorGUILayout.PropertyField(spriteCollection, true);
					#endif
				}

				using (new SpineInspectorUtility.BoxScope()) {
					EditorGUILayout.LabelField("Mix Settings", EditorStyles.boldLabel);
					SpineInspectorUtility.PropertyFieldWideLabel(defaultMix, DefaultMixLabel, 160);
					EditorGUILayout.Space();
				}
				return;
			}

			{ 
				// Lazy initialization because accessing EditorStyles values in OnEnable during a recompile causes UnityEditor to throw null exceptions. (Unity 5.3.5)
				idlePlayButtonStyle = idlePlayButtonStyle ?? new GUIStyle(EditorStyles.miniButton);
				if (activePlayButtonStyle == null) {
					activePlayButtonStyle = new GUIStyle(idlePlayButtonStyle);
					activePlayButtonStyle.normal.textColor = Color.red;
				}
			}

			serializedObject.Update();

			EditorGUILayout.LabelField(new GUIContent(target.name + " (SkeletonDataAsset)", Icons.spine), EditorStyles.whiteLargeLabel);
			if (m_skeletonData != null) {
				EditorGUILayout.LabelField("(Drag and Drop to instantiate.)", EditorStyles.miniLabel);
			}

			EditorGUI.BeginChangeCheck();

			// SkeletonData
			using (new SpineInspectorUtility.BoxScope()) {
				using (new EditorGUILayout.HorizontalScope()) {
					EditorGUILayout.LabelField("SkeletonData", EditorStyles.boldLabel);
//					if (m_skeletonData != null) {
//						var sd = m_skeletonData;
//						string m = string.Format("{8} - {0} {1}\nBones: {2}\tConstraints: {5} IK + {6} Path + {7} Transform\nSlots: {3}\t\tSkins: {4}\n",
//							sd.Version, string.IsNullOrEmpty(sd.Version) ? "" : "export", sd.Bones.Count, sd.Slots.Count, sd.Skins.Count, sd.IkConstraints.Count, sd.PathConstraints.Count, sd.TransformConstraints.Count, skeletonJSON.objectReferenceValue.name);						
//						EditorGUILayout.LabelField(new GUIContent("SkeletonData"), new GUIContent("+", m), EditorStyles.boldLabel);
//					}
				}

				EditorGUILayout.PropertyField(skeletonJSON, new GUIContent(skeletonJSON.displayName, Icons.spine));
				EditorGUILayout.PropertyField(scale);
			}

//			if (m_skeletonData != null) {
//				if (SpineInspectorUtility.CenteredButton(new GUIContent("Instantiate", Icons.spine, "Creates a new Spine GameObject in the active scene using this Skeleton Data.\nYou can also instantiate by dragging the SkeletonData asset from Project view into Scene View.")))
//					SpineEditorUtilities.ShowInstantiateContextMenu(this.m_skeletonDataAsset, Vector3.zero);
//			}

			// Atlas
			using (new SpineInspectorUtility.BoxScope()) {
				EditorGUILayout.LabelField("Atlas", EditorStyles.boldLabel);
				#if !SPINE_TK2D
				EditorGUILayout.PropertyField(atlasAssets, true);
				#else
				using (new EditorGUI.DisabledGroupScope(spriteCollection.objectReferenceValue != null)) {
					EditorGUILayout.PropertyField(atlasAssets, true);
				}
				EditorGUILayout.LabelField("spine-tk2d", EditorStyles.boldLabel);
				EditorGUILayout.PropertyField(spriteCollection, true);
				#endif
			}

			if (EditorGUI.EndChangeCheck()) {
				if (serializedObject.ApplyModifiedProperties()) {
					if (m_previewUtility != null) {
						m_previewUtility.Cleanup();
						m_previewUtility = null;
					}
					RepopulateWarnings();
					OnEnable();
					return;
				}
			}

			// Some code depends on the existence of m_skeletonAnimation instance.
			// If m_skeletonAnimation is lazy-instantiated elsewhere, this can cause contents to change between Layout and Repaint events, causing GUILayout control count errors.
			InitPreview();
			if (m_skeletonData != null) {
				GUILayout.Space(20f);

				using (new SpineInspectorUtility.BoxScope()) {
					EditorGUILayout.LabelField("Mix Settings", EditorStyles.boldLabel);
					DrawAnimationStateInfo();
					EditorGUILayout.Space();
				}

				EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
				DrawAnimationList();
				EditorGUILayout.Space();
				DrawSlotList();
				EditorGUILayout.Space();
				DrawUnityTools();
			} else {
				#if !SPINE_TK2D
				// Reimport Button
				using (new EditorGUI.DisabledGroupScope(skeletonJSON.objectReferenceValue == null)) {
					if (GUILayout.Button(new GUIContent("Attempt Reimport", Icons.warning))) {
						DoReimport();
						return;
					}
				}
				#else
				EditorGUILayout.HelpBox("Couldn't load SkeletonData.", MessageType.Error);
				#endif

				// List warnings.
				foreach (var line in warnings)
					EditorGUILayout.LabelField(new GUIContent(line, Icons.warning));
			}

			if (!Application.isPlaying)
				serializedObject.ApplyModifiedProperties();
		}

		void DrawUnityTools () {
			#if SPINE_SKELETON_ANIMATOR
			using (new SpineInspectorUtility.BoxScope()) {
				isMecanimExpanded = EditorGUILayout.Foldout(isMecanimExpanded, new GUIContent("SkeletonAnimator", Icons.unityIcon));
				if (isMecanimExpanded) {
					EditorGUI.indentLevel++;
					EditorGUILayout.PropertyField(controller, new GUIContent("Controller", Icons.controllerIcon));		
					if (controller.objectReferenceValue == null) {

						// Generate Mecanim Controller Button
						using (new GUILayout.HorizontalScope()) {
							GUILayout.Space(EditorGUIUtility.labelWidth);
							if (GUILayout.Button(new GUIContent("Generate Mecanim Controller"), GUILayout.Height(20)))
								SkeletonBaker.GenerateMecanimAnimationClips(m_skeletonDataAsset);						
						}
						EditorGUILayout.HelpBox("SkeletonAnimator is the Mecanim alternative to SkeletonAnimation.\nIt is not required.", MessageType.Info);

					} else {

						// Update AnimationClips button.
						using (new GUILayout.HorizontalScope()) {
							GUILayout.Space(EditorGUIUtility.labelWidth);
							if (GUILayout.Button(new GUIContent("Force Update AnimationClips"), GUILayout.Height(20)))
								SkeletonBaker.GenerateMecanimAnimationClips(m_skeletonDataAsset);				
						}

					}
					EditorGUI.indentLevel--;
				}
			}
			#endif

			#if SPINE_BAKING
			bool pre = isBakingExpanded;
			isBakingExpanded = EditorGUILayout.Foldout(isBakingExpanded, new GUIContent("Baking", Icons.unityIcon));
			if (pre != isBakingExpanded)
				EditorPrefs.SetBool(ShowBakingPrefsKey, isBakingExpanded);
			
			if (isBakingExpanded) {
				EditorGUI.indentLevel++;
				const string BakingWarningMessage =
//					"WARNING!" +
//					"\nBaking is NOT the same as SkeletonAnimator!" +
//					"\n\n" + 
					"The main use of Baking is to export Spine projects to be used without the Spine Runtime (ie: for sale on the Asset Store, or background objects that are animated only with a wind noise generator)" +

					"\n\nBaking does not support the following:" +
					"\n\tDisabled transform inheritance" +
					"\n\tShear" +
					"\n\tColor Keys" +
					"\n\tDraw Order Keys" +
					"\n\tAll Constraint types" +

					"\n\nCurves are sampled at 60fps and are not realtime." +
					"\nPlease read SkeletonBaker.cs comments for full details.";
				EditorGUILayout.HelpBox(BakingWarningMessage, MessageType.Info, true);

				EditorGUI.indentLevel++;
				bakeAnimations = EditorGUILayout.Toggle("Bake Animations", bakeAnimations);
				using (new EditorGUI.DisabledGroupScope(!bakeAnimations)) {
					EditorGUI.indentLevel++;
					bakeIK = EditorGUILayout.Toggle("Bake IK", bakeIK);
					bakeEventOptions = (SendMessageOptions)EditorGUILayout.EnumPopup("Event Options", bakeEventOptions);
					EditorGUI.indentLevel--;
				}

				// Bake Skin buttons.
				using (new GUILayout.HorizontalScope()) {
					if (GUILayout.Button(new GUIContent("Bake All Skins", Icons.unityIcon), GUILayout.Height(32), GUILayout.Width(150)))
						SkeletonBaker.BakeToPrefab(m_skeletonDataAsset, m_skeletonData.Skins, "", bakeAnimations, bakeIK, bakeEventOptions);
					
					if (m_skeletonAnimation != null && m_skeletonAnimation.skeleton != null) {
						Skin bakeSkin = m_skeletonAnimation.skeleton.Skin;

						string skinName = "<No Skin>";
						if (bakeSkin == null) {
							skinName = "Default";
							bakeSkin = m_skeletonData.Skins.Items[0];
						} else
							skinName = m_skeletonAnimation.skeleton.Skin.Name;

						using (new GUILayout.VerticalScope()) {
							if (GUILayout.Button(new GUIContent("Bake \"" + skinName + "\"", Icons.unityIcon), GUILayout.Height(32), GUILayout.Width(250)))
								SkeletonBaker.BakeToPrefab(m_skeletonDataAsset, new ExposedList<Skin>(new [] { bakeSkin }), "", bakeAnimations, bakeIK, bakeEventOptions);
							using (new GUILayout.HorizontalScope()) {
								GUILayout.Label(new GUIContent("Skins", Icons.skinsRoot), GUILayout.Width(50));
								if (GUILayout.Button(skinName, EditorStyles.popup, GUILayout.Width(196))) {
									DrawSkinDropdown();
								}
							}
						}
					}
				}

				EditorGUI.indentLevel--;
				EditorGUI.indentLevel--;
			}
			#endif
		}

		void DoReimport () {
			SpineEditorUtilities.ImportSpineContent(new string[] { AssetDatabase.GetAssetPath(skeletonJSON.objectReferenceValue) }, true);

			if (m_previewUtility != null) {
				m_previewUtility.Cleanup();
				m_previewUtility = null;
			}

			RepopulateWarnings();
			OnEnable();

			EditorUtility.SetDirty(m_skeletonDataAsset);
		}

		void DrawAnimationStateInfo () {
			showAnimationStateData = EditorGUILayout.Foldout(showAnimationStateData, "Animation State Data");
			if (!showAnimationStateData)
				return;

			EditorGUI.BeginChangeCheck();
			SpineInspectorUtility.PropertyFieldWideLabel(defaultMix, DefaultMixLabel, 160);

			var animations = new string[m_skeletonData.Animations.Count];
			for (int i = 0; i < animations.Length; i++)
				animations[i] = m_skeletonData.Animations.Items[i].Name;

			for (int i = 0; i < fromAnimation.arraySize; i++) {
				SerializedProperty from = fromAnimation.GetArrayElementAtIndex(i);
				SerializedProperty to = toAnimation.GetArrayElementAtIndex(i);
				SerializedProperty durationProp = duration.GetArrayElementAtIndex(i);
				using (new EditorGUILayout.HorizontalScope()) {
					from.stringValue = animations[EditorGUILayout.Popup(Math.Max(Array.IndexOf(animations, from.stringValue), 0), animations)];
					to.stringValue = animations[EditorGUILayout.Popup(Math.Max(Array.IndexOf(animations, to.stringValue), 0), animations)];
					durationProp.floatValue = EditorGUILayout.FloatField(durationProp.floatValue);
					if (GUILayout.Button("Delete")) {
						duration.DeleteArrayElementAtIndex(i);
						toAnimation.DeleteArrayElementAtIndex(i);
						fromAnimation.DeleteArrayElementAtIndex(i);
					}
				}
			}

			using (new EditorGUILayout.HorizontalScope()) {
				EditorGUILayout.Space();
				if (GUILayout.Button("Add Mix")) {
					duration.arraySize++;
					toAnimation.arraySize++;
					fromAnimation.arraySize++;
				}
				EditorGUILayout.Space();
			}

			if (EditorGUI.EndChangeCheck()) {
				m_skeletonDataAsset.FillStateData();
				EditorUtility.SetDirty(m_skeletonDataAsset);
				serializedObject.ApplyModifiedProperties();
				needToSerialize = true;
			}
		}

		void DrawAnimationList () {
			showAnimationList = EditorGUILayout.Foldout(showAnimationList, new GUIContent(string.Format("Animations [{0}]", m_skeletonData.Animations.Count), Icons.animationRoot));
			if (!showAnimationList)
				return;

			if (m_skeletonAnimation != null && m_skeletonAnimation.state != null) {
				if (GUILayout.Button(new GUIContent("Setup Pose", Icons.skeleton), GUILayout.Width(105), GUILayout.Height(18))) {
					StopAnimation();
					m_skeletonAnimation.skeleton.SetToSetupPose();
					m_requireRefresh = true;
				}
			} else {
				EditorGUILayout.HelpBox("Animations can be previewed if you expand the Preview window below.", MessageType.Info);
			}

			EditorGUILayout.LabelField("Name", "Duration");
			foreach (Spine.Animation animation in m_skeletonData.Animations) {
				using (new GUILayout.HorizontalScope()) {
					if (m_skeletonAnimation != null && m_skeletonAnimation.state != null) {
						var activeTrack = m_skeletonAnimation.state.GetCurrent(0);
						if (activeTrack != null && activeTrack.Animation == animation) {
							if (GUILayout.Button("\u25BA", activePlayButtonStyle, GUILayout.Width(24))) {
								StopAnimation();
							}
						} else {
							if (GUILayout.Button("\u25BA", idlePlayButtonStyle, GUILayout.Width(24))) {
								PlayAnimation(animation.Name, true);
							}
						}
					} else {
						GUILayout.Label("-", GUILayout.Width(24));
					}
					EditorGUILayout.LabelField(new GUIContent(animation.Name, Icons.animation), new GUIContent(animation.Duration.ToString("f3") + "s" + ("(" + (Mathf.RoundToInt(animation.Duration * 30)) + ")").PadLeft(12, ' ')));
				}
			}
		}

		void DrawSlotList () {
			showSlotList = EditorGUILayout.Foldout(showSlotList, new GUIContent("Slots", Icons.slotRoot));

			if (!showSlotList) return;
			if (m_skeletonAnimation == null || m_skeletonAnimation.skeleton == null) return;

			EditorGUI.indentLevel++;
			showAttachments = EditorGUILayout.ToggleLeft("Show Attachments", showAttachments);
			var slotAttachments = new List<Attachment>();
			var slotAttachmentNames = new List<string>();
			var defaultSkinAttachmentNames = new List<string>();
			var defaultSkin = m_skeletonData.Skins.Items[0];
			Skin skin = m_skeletonAnimation.skeleton.Skin ?? defaultSkin;
			var slotsItems = m_skeletonAnimation.skeleton.Slots.Items;

			for (int i = m_skeletonAnimation.skeleton.Slots.Count - 1; i >= 0; i--) {
				Slot slot = slotsItems[i];
				EditorGUILayout.LabelField(new GUIContent(slot.Data.Name, Icons.slot));
				if (showAttachments) {
					
					EditorGUI.indentLevel++;
					slotAttachments.Clear();
					slotAttachmentNames.Clear();
					defaultSkinAttachmentNames.Clear();

					skin.FindNamesForSlot(i, slotAttachmentNames);
					skin.FindAttachmentsForSlot(i, slotAttachments);

					if (skin != defaultSkin) {
						defaultSkin.FindNamesForSlot(i, defaultSkinAttachmentNames);
						defaultSkin.FindNamesForSlot(i, slotAttachmentNames);
						defaultSkin.FindAttachmentsForSlot(i, slotAttachments);
					} else {
						defaultSkin.FindNamesForSlot(i, defaultSkinAttachmentNames);
					}

					for (int a = 0; a < slotAttachments.Count; a++) {
						Attachment attachment = slotAttachments[a];
						string attachmentName = slotAttachmentNames[a];
						Texture2D icon = Icons.GetAttachmentIcon(attachment);
						bool initialState = slot.Attachment == attachment;
						bool toggled = EditorGUILayout.ToggleLeft(new GUIContent(attachmentName, icon), slot.Attachment == attachment);

						if (!defaultSkinAttachmentNames.Contains(attachmentName)) {
							Rect skinPlaceHolderIconRect = GUILayoutUtility.GetLastRect();
							skinPlaceHolderIconRect.width = Icons.skinPlaceholder.width;
							skinPlaceHolderIconRect.height = Icons.skinPlaceholder.height;
							GUI.DrawTexture(skinPlaceHolderIconRect, Icons.skinPlaceholder);
						}

						if (toggled != initialState) {
							slot.Attachment = toggled ? attachment : null;
							m_requireRefresh = true;
						}
					}
					EditorGUI.indentLevel--;
				}
			}
			EditorGUI.indentLevel--;
		}
			
		void RepopulateWarnings () {
			warnings.Clear();

			if (skeletonJSON.objectReferenceValue == null) {
				warnings.Add("Missing Skeleton JSON");
			} else {
				if (SpineEditorUtilities.IsSpineData((TextAsset)skeletonJSON.objectReferenceValue) == false) {
					warnings.Add("Skeleton data file is not a valid JSON or binary file.");
				} else {
					#if !SPINE_TK2D
					bool detectedNullAtlasEntry = false;
					var atlasList = new List<Atlas>();
					for (int i = 0; i < atlasAssets.arraySize; i++) {
						if (atlasAssets.GetArrayElementAtIndex(i).objectReferenceValue == null) {
							detectedNullAtlasEntry = true;
							break;
						} else {
							atlasList.Add(((AtlasAsset)atlasAssets.GetArrayElementAtIndex(i).objectReferenceValue).GetAtlas());
						}
					}

					if (detectedNullAtlasEntry)
						warnings.Add("AtlasAsset elements should not be null.");
					else {
						// Get requirements.
						var missingPaths = SpineEditorUtilities.GetRequiredAtlasRegions(AssetDatabase.GetAssetPath((TextAsset)skeletonJSON.objectReferenceValue));

						foreach (var atlas in atlasList) {
							for (int i = 0; i < missingPaths.Count; i++) {
								if (atlas.FindRegion(missingPaths[i]) != null) {
									missingPaths.RemoveAt(i);
									i--;
								}
							}
						}

						foreach (var str in missingPaths)
							warnings.Add("Missing Region: '" + str + "'");
						
					}
					#else
					if (spriteCollection.objectReferenceValue == null)
						warnings.Add("SkeletonDataAsset requires tk2DSpriteCollectionData.");
					else
						warnings.Add("Your sprite collection may have missing images.");
					#endif
				}
			}
		}

		#region Preview Window
		PreviewRenderUtility m_previewUtility;
		GameObject m_previewInstance;
		Vector2 previewDir;
		SkeletonAnimation m_skeletonAnimation;
		static readonly int SliderHash = "Slider".GetHashCode();
		float m_lastTime;
		bool m_playing;
		bool m_requireRefresh;
		Color m_originColor = new Color(0.3f, 0.3f, 0.3f, 1);

		void StopAnimation () {
			if (m_skeletonAnimation == null) {
				Debug.LogWarning("Animation was stopped but preview doesn't exist. It's possible that the Preview Panel is closed.");
			}

			m_skeletonAnimation.state.ClearTrack(0);
			m_playing = false;
		}

		List<Spine.Event> m_animEvents = new List<Spine.Event>();
		List<float> m_animEventFrames = new List<float>();

		void PlayAnimation (string animName, bool loop) {
			m_animEvents.Clear();
			m_animEventFrames.Clear();

			m_skeletonAnimation.state.SetAnimation(0, animName, loop);

			Spine.Animation a = m_skeletonAnimation.state.GetCurrent(0).Animation;
			foreach (Timeline t in a.Timelines) {
				if (t.GetType() == typeof(EventTimeline)) {
					var et = (EventTimeline)t;
					for (int i = 0; i < et.Events.Length; i++) {
						m_animEvents.Add(et.Events[i]);
						m_animEventFrames.Add(et.Frames[i]);
					}
				}
			}

			m_playing = true;
		}

		void InitPreview () {
			if (this.m_previewUtility == null) {
				this.m_lastTime = Time.realtimeSinceStartup;
				this.m_previewUtility = new PreviewRenderUtility(true);
				var c = this.m_previewUtility.m_Camera;
				c.orthographic = true;
				c.orthographicSize = 1;
				c.cullingMask = -2147483648;
				c.nearClipPlane = 0.01f;
				c.farClipPlane = 1000f;
				this.CreatePreviewInstances();
			}
		}

		void CreatePreviewInstances () {
			this.DestroyPreviewInstances();

			var skeletonDataAsset = (SkeletonDataAsset)target;
			if (skeletonDataAsset.GetSkeletonData(false) == null)
				return;

			if (this.m_previewInstance == null) {
				string skinName = EditorPrefs.GetString(m_skeletonDataAssetGUID + "_lastSkin", "");
				m_previewInstance = SpineEditorUtilities.InstantiateSkeletonAnimation(skeletonDataAsset, skinName).gameObject;

				if (m_previewInstance != null) {
					m_previewInstance.hideFlags = HideFlags.HideAndDontSave;
					m_previewInstance.layer = 0x1f;
					m_skeletonAnimation = m_previewInstance.GetComponent<SkeletonAnimation>();
					m_skeletonAnimation.initialSkinName = skinName;
					m_skeletonAnimation.LateUpdate();
					m_skeletonData = m_skeletonAnimation.skeletonDataAsset.GetSkeletonData(true);
					m_previewInstance.GetComponent<Renderer>().enabled = false;
					m_initialized = true;
				}

				AdjustCameraGoals(true);
			}
		}

		void DestroyPreviewInstances () {
			if (this.m_previewInstance != null) {
				DestroyImmediate(this.m_previewInstance);
				m_previewInstance = null;
			}
			m_initialized = false;
		}

		public override bool HasPreviewGUI () {			
			if (serializedObject.isEditingMultipleObjects) {
				// JOHN: Implement multi-preview.
				return false;
			}

			for (int i = 0; i < atlasAssets.arraySize; i++) {
				var prop = atlasAssets.GetArrayElementAtIndex(i);
				if (prop.objectReferenceValue == null)
					return false;
			}

			return skeletonJSON.objectReferenceValue != null;
		}

		Texture m_previewTex = new Texture();

		public override void OnInteractivePreviewGUI (Rect r, GUIStyle background) {
			this.InitPreview();

			if (Event.current.type == EventType.Repaint) {
				if (m_requireRefresh) {
					this.m_previewUtility.BeginPreview(r, background);
					this.DoRenderPreview(true);
					this.m_previewTex = this.m_previewUtility.EndPreview();
					m_requireRefresh = false;
				}
				if (this.m_previewTex != null)
					GUI.DrawTexture(r, m_previewTex, ScaleMode.StretchToFill, false);
			}

			DrawSkinToolbar(r);
			NormalizedTimeBar(r);
			// MITCH: left a todo: Implement panning
			// this.previewDir = Drag2D(this.previewDir, r);
			MouseScroll(r);
		}

		float m_orthoGoal = 1;
		Vector3 m_posGoal = new Vector3(0, 0, -10);
		double m_adjustFrameEndTime = 0;

		void AdjustCameraGoals (bool calculateMixTime) {
			if (this.m_previewInstance == null)
				return;

			if (calculateMixTime) {
				if (m_skeletonAnimation.state.GetCurrent(0) != null)
					m_adjustFrameEndTime = EditorApplication.timeSinceStartup + m_skeletonAnimation.state.GetCurrent(0).Alpha;
			}
				
			GameObject go = this.m_previewInstance;
			Bounds bounds = go.GetComponent<Renderer>().bounds;
			m_orthoGoal = bounds.size.y;
			m_posGoal = bounds.center + new Vector3(0, 0, -10f);
		}

		void AdjustCameraGoals () {
			AdjustCameraGoals(false);
		}

		void AdjustCamera () {
			if (m_previewUtility == null)
				return;

			if (EditorApplication.timeSinceStartup < m_adjustFrameEndTime)
				AdjustCameraGoals();

			float orthoSet = Mathf.Lerp(this.m_previewUtility.m_Camera.orthographicSize, m_orthoGoal, 0.1f);

			this.m_previewUtility.m_Camera.orthographicSize = orthoSet;

			float dist = Vector3.Distance(m_previewUtility.m_Camera.transform.position, m_posGoal);
			if(dist > 0f) {
				Vector3 pos = Vector3.Lerp(this.m_previewUtility.m_Camera.transform.position, m_posGoal, 0.1f);
				pos.x = 0;
				this.m_previewUtility.m_Camera.transform.position = pos;
				this.m_previewUtility.m_Camera.transform.rotation = Quaternion.identity;
				m_requireRefresh = true;
			}
		}

		void DoRenderPreview (bool drawHandles) {
			GameObject go = this.m_previewInstance;

			if (m_requireRefresh && go != null) {
				go.GetComponent<Renderer>().enabled = true;

				if (!EditorApplication.isPlaying)
					m_skeletonAnimation.Update((Time.realtimeSinceStartup - m_lastTime));

				m_lastTime = Time.realtimeSinceStartup;

				if (!EditorApplication.isPlaying)
					m_skeletonAnimation.LateUpdate();

				if (drawHandles) {			
					Handles.SetCamera(m_previewUtility.m_Camera);
					Handles.color = m_originColor;

					Handles.DrawLine(new Vector3(-1000 * m_skeletonDataAsset.scale, 0, 0), new Vector3(1000 * m_skeletonDataAsset.scale, 0, 0));
					Handles.DrawLine(new Vector3(0, 1000 * m_skeletonDataAsset.scale, 0), new Vector3(0, -1000 * m_skeletonDataAsset.scale, 0));
				}

				this.m_previewUtility.m_Camera.Render();

				if (drawHandles) {
					Handles.SetCamera(m_previewUtility.m_Camera);
					SpineHandles.DrawBoundingBoxes(m_skeletonAnimation.transform, m_skeletonAnimation.skeleton);
					if (showAttachments) SpineHandles.DrawPaths(m_skeletonAnimation.transform, m_skeletonAnimation.skeleton);
				}

				go.GetComponent<Renderer>().enabled = false;
			}
				
		}

		void EditorUpdate () {
			AdjustCamera();

			if (m_playing) {
				m_requireRefresh = true;
				Repaint();
			} else if (m_requireRefresh) {
				Repaint();
			} 
			//else {
				//only needed if using smooth menus
			//}

			if (needToSerialize) {
				needToSerialize = false;
				serializedObject.ApplyModifiedProperties();
			}
		}

		void DrawSkinToolbar (Rect r) {
			if (m_skeletonAnimation == null)
				return;

			if (m_skeletonAnimation.skeleton != null) {
				string label = (m_skeletonAnimation.skeleton != null && m_skeletonAnimation.skeleton.Skin != null) ? m_skeletonAnimation.skeleton.Skin.Name : "default";

				Rect popRect = new Rect(r);
				popRect.y += 32;
				popRect.x += 4;
				popRect.height = 24;
				popRect.width = 40;
				EditorGUI.DropShadowLabel(popRect, new GUIContent("Skin", Icons.skinsRoot));

				popRect.y += 11;
				popRect.width = 150;
				popRect.x += 44;

				if (GUI.Button(popRect, label, EditorStyles.popup)) {
					DrawSkinDropdown();
				}
			}
		}
			
		void NormalizedTimeBar (Rect r) {
			if (m_skeletonAnimation == null)
				return;

			Rect barRect = new Rect(r);
			barRect.height = 32;
			barRect.x += 4;
			barRect.width -= 4;

			GUI.Box(barRect, "");

			Rect lineRect = new Rect(barRect);
			float width = lineRect.width;
			TrackEntry t = m_skeletonAnimation.state.GetCurrent(0);

			if (t != null) {
				int loopCount = (int)(t.TrackTime / t.TrackEnd);
				float currentTime = t.TrackTime - (t.TrackEnd * loopCount);
				float normalizedTime = currentTime / t.Animation.Duration;
				float wrappedTime = normalizedTime % 1;

				lineRect.x = barRect.x + (width * wrappedTime) - 0.5f;
				lineRect.width = 2;

				GUI.color = Color.red;
				GUI.DrawTexture(lineRect, EditorGUIUtility.whiteTexture);
				GUI.color = Color.white;

				for (int i = 0; i < m_animEvents.Count; i++) {
					float fr = m_animEventFrames[i];
					var evRect = new Rect(barRect);
					evRect.x = Mathf.Clamp(((fr / t.Animation.Duration) * width) - (Icons.userEvent.width / 2), barRect.x, float.MaxValue);
					evRect.width = Icons.userEvent.width;
					evRect.height = Icons.userEvent.height;
					evRect.y += Icons.userEvent.height;
					GUI.DrawTexture(evRect, Icons.userEvent);

					Event ev = Event.current;
					if (ev.type == EventType.Repaint) {
						if (evRect.Contains(ev.mousePosition)) {
							Rect tooltipRect = new Rect(evRect);
							GUIStyle tooltipStyle = EditorStyles.helpBox;
							tooltipRect.width = tooltipStyle.CalcSize(new GUIContent(m_animEvents[i].Data.Name)).x;
							tooltipRect.y -= 4;
							tooltipRect.x += 4;
							GUI.Label(tooltipRect,  m_animEvents[i].Data.Name, tooltipStyle);
							GUI.tooltip = m_animEvents[i].Data.Name;
						}
					}
				}
			}
		}

		void MouseScroll (Rect position) {
			Event current = Event.current;
			int controlID = GUIUtility.GetControlID(SliderHash, FocusType.Passive);
			switch (current.GetTypeForControl(controlID)) {
			case EventType.ScrollWheel:
				if (position.Contains(current.mousePosition)) {
					m_orthoGoal += current.delta.y * 0.06f;
					m_orthoGoal = Mathf.Max(0.01f, m_orthoGoal);
					GUIUtility.hotControl = controlID;
					current.Use();
				}
				break;
			}
		}

		// MITCH: left todo:  Implement preview panning
		/*
		static Vector2 Drag2D(Vector2 scrollPosition, Rect position)
		{
			int controlID = GUIUtility.GetControlID(sliderHash, FocusType.Passive);
			UnityEngine.Event current = UnityEngine.Event.current;
			switch (current.GetTypeForControl(controlID))
			{
			case EventType.MouseDown:
				if (position.Contains(current.mousePosition) && (position.width > 50f))
				{
					GUIUtility.hotControl = controlID;
					current.Use();
					EditorGUIUtility.SetWantsMouseJumping(1);
				}
				return scrollPosition;
				
			case EventType.MouseUp:
				if (GUIUtility.hotControl == controlID)
				{
					GUIUtility.hotControl = 0;
				}
				EditorGUIUtility.SetWantsMouseJumping(0);
				return scrollPosition;
				
			case EventType.MouseMove:
				return scrollPosition;
				
			case EventType.MouseDrag:
				if (GUIUtility.hotControl == controlID)
				{
					scrollPosition -= (Vector2) (((current.delta * (!current.shift ? ((float) 1) : ((float) 3))) / Mathf.Min(position.width, position.height)) * 140f);
					scrollPosition.y = Mathf.Clamp(scrollPosition.y, -90f, 90f);
					current.Use();
					GUI.changed = true;
				}
				return scrollPosition;
			}
			return scrollPosition;
		}
		*/

		public override GUIContent GetPreviewTitle () {
			return new GUIContent("Preview");
		}

		public override void OnPreviewSettings () {
			const float SliderWidth = 100;
			if (!m_initialized) {
				GUILayout.HorizontalSlider(0, 0, 2, GUILayout.MaxWidth(SliderWidth));
			} else {
				float speed = GUILayout.HorizontalSlider(m_skeletonAnimation.timeScale, 0, 2, GUILayout.MaxWidth(SliderWidth));

				const float SliderSnap = 0.25f;
				float y = speed / SliderSnap;
				int q = Mathf.RoundToInt(y);
				speed = q * SliderSnap;

				m_skeletonAnimation.timeScale = speed;
			}
		}


		public override Texture2D RenderStaticPreview (string assetPath, UnityEngine.Object[] subAssets, int width, int height) {
			var tex = new Texture2D(width, height, TextureFormat.ARGB32, false);

			this.InitPreview();
			if (this.m_previewUtility.m_Camera == null)
				return null;

			m_requireRefresh = true;
			this.DoRenderPreview(false);
			AdjustCameraGoals(false);
			this.m_previewUtility.m_Camera.orthographicSize = m_orthoGoal / 2;
			this.m_previewUtility.m_Camera.transform.position = m_posGoal;
			this.m_previewUtility.BeginStaticPreview(new Rect(0, 0, width, height));
			this.DoRenderPreview(false);
			tex = this.m_previewUtility.EndStaticPreview();
			return tex;
		}
		#endregion

		#region Skin Dropdown Context Menu
		void DrawSkinDropdown () {
			var menu = new GenericMenu();
			foreach (Skin s in m_skeletonData.Skins)
				menu.AddItem(new GUIContent(s.Name), this.m_skeletonAnimation.skeleton.Skin == s, SetSkin, s);
			
			menu.ShowAsContext();
		}

		void SetSkin (object o) {
			Skin skin = (Skin)o;
			m_skeletonAnimation.initialSkinName = skin.Name;
			m_skeletonAnimation.Initialize(true);
			m_requireRefresh = true;
			EditorPrefs.SetString(m_skeletonDataAssetGUID + "_lastSkin", skin.Name);
		}
		#endregion
	}
		
}
