/**
   * 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.
   */
  public void apply(Skeleton skeleton) {
    if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
    if (animationsChanged) animationsChanged();

    Array<Event> events = this.events;

    for (int i = 0, n = tracks.size; i < n; i++) {
      TrackEntry current = tracks.get(i);
      if (current == null || current.delay > 0) continue;

      // Apply mixing from entries first.
      float mix = current.alpha;
      if (current.mixingFrom != null) mix *= applyMixingFrom(current, skeleton);
      else if (current.trackTime >= current.trackEnd) //
      mix = 0; // Set to setup pose the last time the entry will be applied.

      // Apply current entry.
      float animationLast = current.animationLast, animationTime = current.getAnimationTime();
      int timelineCount = current.animation.timelines.size;
      Object[] timelines = current.animation.timelines.items;
      if (mix == 1) {
        for (int ii = 0; ii < timelineCount; ii++)
          ((Timeline) timelines[ii])
              .apply(skeleton, animationLast, animationTime, events, 1, true, false);
      } else {
        boolean firstFrame = current.timelinesRotation.size == 0;
        if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
        float[] timelinesRotation = current.timelinesRotation.items;

        boolean[] timelinesFirst = current.timelinesFirst.items;
        for (int ii = 0; ii < timelineCount; ii++) {
          Timeline timeline = (Timeline) timelines[ii];
          if (timeline instanceof RotateTimeline) {
            applyRotateTimeline(
                timeline,
                skeleton,
                animationTime,
                mix,
                timelinesFirst[ii],
                timelinesRotation,
                ii << 1,
                firstFrame);
          } else
            timeline.apply(
                skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false);
        }
      }
      queueEvents(current, animationTime);
      events.clear();
      current.nextAnimationLast = animationTime;
      current.nextTrackLast = current.trackTime;
    }

    queue.drain();
  }
  private float applyMixingFrom(TrackEntry entry, Skeleton skeleton) {
    TrackEntry from = entry.mixingFrom;
    if (from.mixingFrom != null) applyMixingFrom(from, skeleton);

    float mix;
    if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
    mix = 1;
    else {
      mix = entry.mixTime / entry.mixDuration;
      if (mix > 1) mix = 1;
    }

    Array<Event> events = mix < from.eventThreshold ? this.events : null;
    boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
    float animationLast = from.animationLast, animationTime = from.getAnimationTime();
    int timelineCount = from.animation.timelines.size;
    Object[] timelines = from.animation.timelines.items;
    boolean[] timelinesFirst = from.timelinesFirst.items;
    float alpha = from.alpha * entry.mixAlpha * (1 - mix);

    boolean firstFrame = from.timelinesRotation.size == 0;
    if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
    float[] timelinesRotation = from.timelinesRotation.items;

    for (int i = 0; i < timelineCount; i++) {
      Timeline timeline = (Timeline) timelines[i];
      boolean setupPose = timelinesFirst[i];
      if (timeline instanceof RotateTimeline)
        applyRotateTimeline(
            timeline,
            skeleton,
            animationTime,
            alpha,
            setupPose,
            timelinesRotation,
            i << 1,
            firstFrame);
      else {
        if (!setupPose) {
          if (!attachments && timeline instanceof AttachmentTimeline) continue;
          if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
        }
        timeline.apply(skeleton, animationLast, animationTime, events, alpha, setupPose, true);
      }
    }

    if (entry.mixDuration > 0) queueEvents(from, animationTime);
    this.events.clear();
    from.nextAnimationLast = animationTime;
    from.nextTrackLast = from.trackTime;

    return mix;
  }
  private void applyRotateTimeline(
      Timeline timeline,
      Skeleton skeleton,
      float time,
      float alpha,
      boolean setupPose,
      float[] timelinesRotation,
      int i,
      boolean firstFrame) {

    if (firstFrame) timelinesRotation[i] = 0;

    if (alpha == 1) {
      timeline.apply(skeleton, 0, time, null, 1, setupPose, false);
      return;
    }

    RotateTimeline rotateTimeline = (RotateTimeline) timeline;
    Bone bone = skeleton.bones.get(rotateTimeline.boneIndex);
    float[] frames = rotateTimeline.frames;
    if (time < frames[0]) { // Time is before first frame.
      if (setupPose) bone.rotation = bone.data.rotation;
      return;
    }

    float r2;
    if (time >= frames[frames.length - ENTRIES]) // Time is after last frame.
    r2 = bone.data.rotation + frames[frames.length + PREV_ROTATION];
    else {
      // Interpolate between the previous frame and the current frame.
      int frame = Animation.binarySearch(frames, time, ENTRIES);
      float prevRotation = frames[frame + PREV_ROTATION];
      float frameTime = frames[frame];
      float percent =
          rotateTimeline.getCurvePercent(
              (frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));

      r2 = frames[frame + 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 = setupPose ? 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.
      }
      boolean current = diff > 0, dir = lastTotal >= 0;
      // Detect cross at 0 (not 180).
      if (Math.signum(lastDiff) != Math.signum(diff) && Math.abs(lastDiff) <= 90) {
        // A cross after a 360 rotation is a loop.
        if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal);
        dir = current;
      }
      total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal.
      if (dir != current) total += 360 * Math.signum(lastTotal);
      timelinesRotation[i] = total;
    }
    timelinesRotation[i + 1] = diff;
    r1 += total * alpha;
    bone.rotation = r1 - (16384 - (int) (16384.499999999996 - r1 / 360)) * 360;
  }