context: /******************************************************************************
   * 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.
   *****************************************************************************/
  
  using System;
  using System.Collections.Generic;
  
  namespace Spine {
  	public class AnimationState {
  		static readonly Animation EmptyAnimation = new Animation("<empty>", new ExposedList<Timeline>(), 0);
  		internal const int Subsequent = 0, First = 1, Dip = 2, DipMix = 3;
  
  		private AnimationStateData data;
  
  		Pool<TrackEntry> trackEntryPool = new Pool<TrackEntry>();
  		private readonly ExposedList<TrackEntry> tracks = new ExposedList<TrackEntry>();
  		private readonly ExposedList<Event> events = new ExposedList<Event>();
  		private readonly EventQueue queue; // Initialized by constructor.
  
  		private readonly HashSet<int> propertyIDs = new HashSet<int>();
  		private readonly ExposedList<TrackEntry> mixingTo = new ExposedList<TrackEntry>();
  		private bool animationsChanged;
  
  		private float timeScale = 1;
  
  		public AnimationStateData Data { get { return data; } }
  		/// <summary>A list of tracks that have animations, which may contain nulls.</summary>
  		public ExposedList<TrackEntry> Tracks { get { return tracks; } }
  		public float TimeScale { get { return timeScale; } set { timeScale = value; } }
  
  		public delegate void TrackEntryDelegate (TrackEntry trackEntry);
  		public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;
  
  		public delegate void TrackEntryEventDelegate (TrackEntry trackEntry, Event e);
  		public event TrackEntryEventDelegate Event;
  
  		public AnimationState (AnimationStateData data) {
  			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
  			this.data = data;
  			this.queue = new EventQueue(
  				this,
  				delegate { this.animationsChanged = true; },
  				trackEntryPool
  			);
  		}
  			
  		/// <summary>
  		/// Increments the track entry times, setting queued animations as current if needed</summary>
  		/// <param name="delta">delta time</param>
  		public void Update (float delta) {
  			delta *= timeScale;
  			var tracksItems = tracks.Items;
  			for (int i = 0, n = tracks.Count; i < n; i++) {
  				TrackEntry current = tracksItems[i];
  				if (current == null) continue;
  
  				current.animationLast = current.nextAnimationLast;
  				current.trackLast = current.nextTrackLast;
  
  				float currentDelta = delta * current.timeScale;
  
  				if (current.delay > 0) {
  					current.delay -= currentDelta;
  					if (current.delay > 0) continue;
  					currentDelta = -current.delay;
  					current.delay = 0;
  				}
  
  				TrackEntry next = current.next;
  				if (next != null) {
  					// When the next entry's delay is passed, change to the next entry, preserving leftover time.
  					float nextTime = current.trackLast - next.delay;
  					if (nextTime >= 0) {
  						next.delay = 0;
  						next.trackTime = nextTime + (delta * next.timeScale);
  						current.trackTime += currentDelta;
  						SetCurrent(i, next, true);
  						while (next.mixingFrom != null) {
  							next.mixTime += currentDelta;
  							next = next.mixingFrom;
  						}
  						continue;
  					}
  				} else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
  					// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
  					tracksItems[i] = null;
  
  					queue.End(current);
  					DisposeNext(current);
  					continue;
  				}
  				if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) {
  					// End mixing from entries once all have completed.
  					var from = current.mixingFrom;
  					current.mixingFrom = null;
  					while (from != null) {
  						queue.End(from);
  						from = from.mixingFrom;
  					}
  				}
  
  				current.trackTime += currentDelta;
  			}
  
  			queue.Drain();
  		}
  
  		/// <summary>Returns true when all mixing from entries are complete.</summary>
  		private bool UpdateMixingFrom (TrackEntry to, float delta) {
  			TrackEntry from = to.mixingFrom;
  			if (from == null) return true;
  
  			bool finished = UpdateMixingFrom(from, delta);
  
  			// Require mixTime > 0 to ensure the mixing from entry was applied at least once.
  			if (to.mixTime > 0 && (to.mixTime >= to.mixDuration || to.timeScale == 0)) {
  				// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
  				if (from.totalAlpha == 0 || to.mixDuration == 0) {
  					to.mixingFrom = from.mixingFrom;
  					to.interruptAlpha = from.interruptAlpha;
  					queue.End(from);
  				}
  				return finished;
  			}
  
  			from.animationLast = from.nextAnimationLast;
  			from.trackLast = from.nextTrackLast;
  			from.trackTime += delta * from.timeScale;
  			to.mixTime += delta * to.timeScale;
  			return false;
  		}
  
  		/// <summary>
  		/// Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the 
  		/// animation state can be applied to multiple skeletons to pose them identically.</summary>
  		public bool Apply (Skeleton skeleton) {
  			if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
  			if (animationsChanged) AnimationsChanged();
  
  			var events = this.events;
  
  			bool applied = false;
  			var tracksItems = tracks.Items;
  			for (int i = 0, m = tracks.Count; i < m; i++) {
  				TrackEntry current = tracksItems[i];
  				if (current == null || current.delay > 0) continue;
  				applied = true;
  				MixPose currentPose = i == 0 ? MixPose.Current : MixPose.CurrentLayered;
  
  				// Apply mixing from entries first.
  				float mix = current.alpha;
  				if (current.mixingFrom != null)
  					mix *= ApplyMixingFrom(current, skeleton, currentPose);
  				else if (current.trackTime >= current.trackEnd && current.next == null) //
  					mix = 0; // Set to setup pose the last time the entry will be applied.
  
  				// Apply current entry.
  				float animationLast = current.animationLast, animationTime = current.AnimationTime;
  				int timelineCount = current.animation.timelines.Count;
  				var timelines = current.animation.timelines;
  				var timelinesItems = timelines.Items;
  				if (mix == 1) {
  					for (int ii = 0; ii < timelineCount; ii++)
  						timelinesItems[ii].Apply(skeleton, animationLast, animationTime, events, 1, MixPose.Setup, MixDirection.In);
  				} else {
  					var timelineData = current.timelineData.Items;
  
  					bool firstFrame = current.timelinesRotation.Count == 0;
  					if (firstFrame) current.timelinesRotation.EnsureCapacity(timelines.Count << 1);
  					var timelinesRotation = current.timelinesRotation.Items;
  
  					for (int ii = 0; ii < timelineCount; ii++) {
  						Timeline timeline = timelinesItems[ii];
  						MixPose pose = timelineData[ii] >= AnimationState.First ? MixPose.Setup : currentPose;
  						var rotateTimeline = timeline as RotateTimeline;
  						if (rotateTimeline != null)
  							ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, pose, timelinesRotation, ii << 1, firstFrame);
  						else
  							timeline.Apply(skeleton, animationLast, animationTime, events, mix, pose, MixDirection.In);
  					}
  				}
  				QueueEvents(current, animationTime);
  				events.Clear(false);
  				current.nextAnimationLast = animationTime;
  				current.nextTrackLast = current.trackTime;
  			}
  
  			queue.Drain();
  			return applied;
  		}
  
  		private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton, MixPose currentPose) {
  			TrackEntry from = to.mixingFrom;
  			if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, currentPose);
  
  			float mix;
  			if (to.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
  				mix = 1;
  			else {
  				mix = to.mixTime / to.mixDuration;
  				if (mix > 1) mix = 1;
  			}
  
  			var eventBuffer = mix < from.eventThreshold ? this.events : null;
  			bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
  			float animationLast = from.animationLast, animationTime = from.AnimationTime;
  			var timelines = from.animation.timelines;
  			int timelineCount = timelines.Count;
  			var timelinesItems = timelines.Items;
  			var timelineData = from.timelineData.Items;
  			var timelineDipMix = from.timelineDipMix.Items;
  
  			bool firstFrame = from.timelinesRotation.Count == 0;
  			if (firstFrame) from.timelinesRotation.Resize(timelines.Count << 1); // from.timelinesRotation.setSize
  			var timelinesRotation = from.timelinesRotation.Items;
  
  			MixPose pose;
  			float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix), alpha;
  			from.totalAlpha = 0;
  			for (int i = 0; i < timelineCount; i++) {
  				Timeline timeline = timelinesItems[i];
  				switch (timelineData[i]) {
  				case Subsequent:
  					if (!attachments && timeline is AttachmentTimeline) continue;
  					if (!drawOrder && timeline is DrawOrderTimeline) continue;
  					pose = currentPose;
  					alpha = alphaMix;
  					break;
  				case First:
  					pose = MixPose.Setup;
  					alpha = alphaMix;
  					break;
  				case Dip:
  					pose = MixPose.Setup;
  					alpha = alphaDip;
  					break;
  				default:
  					pose = MixPose.Setup;
  					alpha = alphaDip;
  					var dipMix = timelineDipMix[i];
  					alpha *= Math.Max(0, 1 - dipMix.mixTime / dipMix.mixDuration);
  					break;
  				}
  				from.totalAlpha += alpha;
  				var rotateTimeline = timeline as RotateTimeline;
  				if (rotateTimeline != null) {
  					ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, pose, timelinesRotation, i << 1, firstFrame);
  				} else {
  					timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, pose, MixDirection.Out);
  				}
  			}
  
  			if (to.mixDuration > 0) QueueEvents(from, animationTime);
  			this.events.Clear(false);
  			from.nextAnimationLast = animationTime;
  			from.nextTrackLast = from.trackTime;
  
  			return mix;
  		}
  
  		static private void ApplyRotateTimeline (RotateTimeline rotateTimeline, Skeleton skeleton, float time, float alpha, MixPose pose,
  			float[] timelinesRotation, int i, bool firstFrame) {
  
  			if (firstFrame) timelinesRotation[i] = 0;
  
  			if (alpha == 1) {
  				rotateTimeline.Apply(skeleton, 0, time, null, 1, pose, MixDirection.In);
  				return;
  			}
  
  			Bone bone = skeleton.bones.Items[rotateTimeline.boneIndex];
  			float[] frames = rotateTimeline.frames;
  			if (time < frames[0]) {
  				if (pose == MixPose.Setup) bone.rotation = bone.data.rotation;
  				return;
  			}
  
  			float r2;
  			if (time >= frames[frames.Length - RotateTimeline.ENTRIES]) // Time is after last frame.
  				r2 = bone.data.rotation + frames[frames.Length + RotateTimeline.PREV_ROTATION];
  			else {
  				// Interpolate between the previous frame and the current frame.
  				int frame = Animation.BinarySearch(frames, time, RotateTimeline.ENTRIES);
  				float prevRotation = frames[frame + RotateTimeline.PREV_ROTATION];
  				float frameTime = frames[frame];
  				float percent = rotateTimeline.GetCurvePercent((frame >> 1) - 1,
  					1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime));
  
  				r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation;
  				r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
  				r2 = prevRotation + r2 * percent + bone.data.rotation;
  				r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
  			}
  
  			// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
  			float r1 = pose == MixPose.Setup ? bone.data.rotation : bone.rotation;
  			float total, diff = r2 - r1;
  			if (diff == 0) {
  				total = timelinesRotation[i];
  			} else {
  				diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
  				float lastTotal, lastDiff;
  				if (firstFrame) {
  					lastTotal = 0;
  					lastDiff = diff;
  				} else {
  					lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops.
  					lastDiff = timelinesRotation[i + 1]; // Difference between bones.
  				}
  				bool current = diff > 0, dir = lastTotal >= 0;
  				// Detect cross at 0 (not 180).
  				if (Math.Sign(lastDiff) != Math.Sign(diff) && Math.Abs(lastDiff) <= 90) {
  					// A cross after a 360 rotation is a loop.
  					if (Math.Abs(lastTotal) > 180) lastTotal += 360 * Math.Sign(lastTotal);
  					dir = current;
  				}
  				total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal.
  				if (dir != current) total += 360 * Math.Sign(lastTotal);
  				timelinesRotation[i] = total;
  			}
  			timelinesRotation[i + 1] = diff;
  			r1 += total * alpha;
  			bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360;
  		}
  
  		private void QueueEvents (TrackEntry entry, float animationTime) {
  			float animationStart = entry.animationStart, animationEnd = entry.animationEnd;
  			float duration = animationEnd - animationStart;
  			float trackLastWrapped = entry.trackLast % duration;
  
  			// Queue events before complete.
  			var events = this.events;
  			var eventsItems = events.Items;
  			int i = 0, n = events.Count;
  			for (; i < n; i++) {
  				var e = eventsItems[i];
  				if (e.time < trackLastWrapped) break;
  				if (e.time > animationEnd) continue; // Discard events outside animation start/end.
  				queue.Event(entry, e);
  			}
  
  			// Queue complete if completed a loop iteration or the animation.
  			if (entry.loop ? (trackLastWrapped > entry.trackTime % duration)
  				: (animationTime >= animationEnd && entry.animationLast < animationEnd)) {
  				queue.Complete(entry);
  			}
  
  			// Queue events after complete.
  			for (; i < n; i++) {
  				Event e = eventsItems[i];
  				if (e.time < animationStart) continue; // Discard events outside animation start/end.
  				queue.Event(entry, eventsItems[i]);
  			}			
  		}
  
  		/// <summary>
  		/// Removes all animations from all tracks, leaving skeletons in their previous pose. 
  		/// It may be desired to use <see cref="AnimationState.SetEmptyAnimations(float)"/> to mix the skeletons back to the setup pose, 
  		/// rather than leaving them in their previous pose.</summary>
  		public void ClearTracks () {
  			bool oldDrainDisabled = queue.drainDisabled;
  			queue.drainDisabled = true;
  			for (int i = 0, n = tracks.Count; i < n; i++) {
  				ClearTrack(i);
  			}
  			tracks.Clear();
  			queue.drainDisabled = oldDrainDisabled;
  			queue.Drain();
  		}
  
  		/// <summary>
  		/// Removes all animations from the tracks, leaving skeletons in their previous pose. 
  		/// It may be desired to use <see cref="AnimationState.SetEmptyAnimations(float)"/> to mix the skeletons back to the setup pose, 
  		/// rather than leaving them in their previous pose.</summary>
  		public void ClearTrack (int trackIndex) {
  			if (trackIndex >= tracks.Count) return;
  			TrackEntry current = tracks.Items[trackIndex];
  			if (current == null) return;
  
  			queue.End(current);
  
  			DisposeNext(current);
  
  			TrackEntry entry = current;
  			while (true) {
  				TrackEntry from = entry.mixingFrom;
  				if (from == null) break;
  				queue.End(from);
  				entry.mixingFrom = null;
  				entry = from;
  			}
  
  			tracks.Items[current.trackIndex] = null;
  
  			queue.Drain();
  		}
  
  		/// <summary>Sets the active TrackEntry for a given track number.</summary>
  		private void SetCurrent (int index, TrackEntry current, bool interrupt) {
  			TrackEntry from = ExpandToIndex(index);
  			tracks.Items[index] = current;
  
  			if (from != null) {
  				if (interrupt) queue.Interrupt(from);
  				current.mixingFrom = from;
  				current.mixTime = 0;
  
  				// Store interrupted mix percentage.
  				if (from.mixingFrom != null && from.mixDuration > 0)
  					current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration);
  
  				from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in.
  			}
  
  			queue.Start(current); // triggers AnimationsChanged
  		}
  
  
  		/// <summary>Sets an animation by name. <seealso cref="SetAnimation(int, Animation, bool)" /></summary>
  		public TrackEntry SetAnimation (int trackIndex, string animationName, bool loop) {
  			Animation animation = data.skeletonData.FindAnimation(animationName);
  			if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName");
  			return SetAnimation(trackIndex, animation, loop);
  		}
  
  		/// <summary>Sets the current animation for a track, discarding any queued animations.</summary>
  		/// <param name="loop">If true, the animation will repeat.
  		/// If false, it will not, instead its last frame is applied if played beyond its duration.
  		/// In either case <see cref="TrackEntry.TrackEnd"/> determines when the track is cleared. </param>
  		/// <returns>
  		/// A track entry to allow further customization of animation playback. References to the track entry must not be kept 
  		/// after <see cref="AnimationState.Dispose"/>.</returns>
  		public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) {
  			if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null.");
  			bool interrupt = true;
  
gt: 			TrackEntry current = ExpandToIndex(trackIndex);