content: /*
   * Copyright (C) 2020 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package androidx.constraintlayout.desktop.graphdemos.utils;
  
  /**
   * This contains the class to provide the logic for an animation to come to a stop.
   * The setup defines a series of velocity gradients that gets to the desired position
   * ending at 0 velocity.
   * The path is computed such that the velocities are continuous
   *
   *
   */
  public class StopLogicEngine implements StopEngine {
      // the velocity at the start of each period
      private float mStage1Velocity, mStage2Velocity, mStage3Velocity;
      private float mStage1Duration, mStage2Duration, mStage3Duration; // the time for each period
      private float mStage1EndPosition, mStage2EndPosition, mStage3EndPosition; // ending position
      private int mNumberOfStages;
      private String mType;
      private boolean mBackwards = false;
      private float mStartPosition;
      private float mLastPosition;
      @SuppressWarnings("unused")
      private boolean mDone = false;
      private static final float EPSILON = 0.00001f;
  
      /**
       * Debugging logic to log the state.
       *
       * @param desc Description to pre append
       * @param time Time during animation
       * @return string useful for debugging the state of the StopLogic
       */
      @Override
      public String debug(String desc, float time) {
          String ret = desc + " ===== " + mType + "\n";
          ret += desc + (mBackwards ? "backwards" : "forward ")
                  + " time = " + time + "  stages " + mNumberOfStages + "\n";
          ret += desc + " dur " + mStage1Duration + " vel "
                  + mStage1Velocity + " pos " + mStage1EndPosition + "\n";
  
          if (mNumberOfStages > 1) {
              ret += desc + " dur " + mStage2Duration + " vel "
                      + mStage2Velocity + " pos " + mStage2EndPosition + "\n";
  
          }
          if (mNumberOfStages > 2) {
              ret += desc + " dur " + mStage3Duration + " vel "
                      + mStage3Velocity + " pos " + mStage3EndPosition + "\n";
          }
  
          if (time <= mStage1Duration) {
              ret += desc + "stage 0" + "\n";
              return ret;
          }
          if (mNumberOfStages == 1) {
              ret += desc + "end stage 0" + "\n";
              return ret;
          }
          time -= mStage1Duration;
          if (time < mStage2Duration) {
  
              ret += desc + " stage 1" + "\n";
              return ret;
          }
          if (mNumberOfStages == 2) {
              ret += desc + "end stage 1" + "\n";
              return ret;
          }
          time -= mStage2Duration;
          if (time < mStage3Duration) {
  
              ret += desc + " stage 2" + "\n";
              return ret;
          }
          ret += desc + " end stage 2" + "\n";
          return ret;
      }
  
      // @TODO: add description
      @Override
      public float getVelocity(float x) {
          if (x <= mStage1Duration) {
              return mStage1Velocity + (mStage2Velocity - mStage1Velocity) * x / mStage1Duration;
          }
          if (mNumberOfStages == 1) {
              return 0;
          }
          x -= mStage1Duration;
          if (x < mStage2Duration) {
  
              return mStage2Velocity + (mStage3Velocity - mStage2Velocity) * x / mStage2Duration;
          }
          if (mNumberOfStages == 2) {
              return mStage2EndPosition;
          }
          x -= mStage2Duration;
          if (x < mStage3Duration) {
  
              return mStage3Velocity - mStage3Velocity * x / mStage3Duration;
          }
          return mStage3EndPosition;
      }
  
      private float calcY(float time) {
          mDone = false;
          if (time <= mStage1Duration) {
              return mStage1Velocity * time + (mStage2Velocity - mStage1Velocity)
                      * time * time / (2 * mStage1Duration);
          }
          if (mNumberOfStages == 1) {
              return mStage1EndPosition;
          }
          time -= mStage1Duration;
          if (time < mStage2Duration) {
  
              return mStage1EndPosition + mStage2Velocity * time
                      + (mStage3Velocity - mStage2Velocity) * time * time / (2 * mStage2Duration);
          }
          if (mNumberOfStages == 2) {
              return mStage2EndPosition;
          }
          time -= mStage2Duration;
          if (time <= mStage3Duration) {
  
              return mStage2EndPosition + mStage3Velocity
                      * time - mStage3Velocity * time * time / (2 * mStage3Duration);
          }
          mDone = true;
          return mStage3EndPosition;
      }
  
      // @TODO: add description
      public void config(float currentPos, float destination, float currentVelocity,
              float maxTime, float maxAcceleration, float maxVelocity) {
          mDone = false;
          mStartPosition = currentPos;
          mBackwards = (currentPos > destination);
          if (mBackwards) {
              setup(-currentVelocity, currentPos - destination,
                      maxAcceleration, maxVelocity, maxTime);
          } else {
              setup(currentVelocity, destination - currentPos, maxAcceleration, maxVelocity, maxTime);
          }
      }
  
      // @TODO: add description
      @Override
      public float getInterpolation(float v) {
          float y = calcY(v);
          mLastPosition = v;
          return mBackwards ? mStartPosition - y : mStartPosition + y;
      }
  
      @Override
      public float getVelocity() {
          return mBackwards ? -getVelocity(mLastPosition) : getVelocity(mLastPosition);
      }
  
      @Override
      public boolean isStopped() {
          return getVelocity() < EPSILON && Math.abs(mStage3EndPosition - mLastPosition) < EPSILON;
      }
  
      private void setup(float velocity, float distance, float maxAcceleration, float maxVelocity,
              float maxTime) {
          mDone = false;
          if (velocity == 0) {
              velocity = 0.0001f;
          }
          this.mStage1Velocity = velocity;
          float min_time_to_stop = velocity / maxAcceleration;
          float stopDistance = min_time_to_stop * velocity / 2;
  
          if (velocity < 0) { // backward
              float timeToZeroVelocity = -velocity / maxAcceleration;
              float reversDistanceTraveled = timeToZeroVelocity * velocity / 2;
              float totalDistance = distance - reversDistanceTraveled;
              float peak_v = (float) Math.sqrt(maxAcceleration * totalDistance);
              if (peak_v < maxVelocity) { // accelerate then decelerate
                  mType = "backward accelerate, decelerate";
                  this.mNumberOfStages = 2;
                  this.mStage1Velocity = velocity;
                  this.mStage2Velocity = peak_v;
                  this.mStage3Velocity = 0;
                  this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
                  this.mStage2Duration = peak_v / maxAcceleration;
                  this.mStage1EndPosition = (velocity + peak_v) * this.mStage1Duration / 2;
                  this.mStage2EndPosition = distance;
                  this.mStage3EndPosition = distance;
                  return;
              }
              mType = "backward accelerate cruse decelerate";
              this.mNumberOfStages = 3;
              this.mStage1Velocity = velocity;
              this.mStage2Velocity = maxVelocity;
              this.mStage3Velocity = maxVelocity;
  
              this.mStage1Duration = (maxVelocity - velocity) / maxAcceleration;
              this.mStage3Duration = maxVelocity / maxAcceleration;
              float accDist = (velocity + maxVelocity) * this.mStage1Duration / 2;
              float decDist = (maxVelocity * this.mStage3Duration) / 2;
              this.mStage2Duration = (distance - accDist - decDist) / maxVelocity;
              this.mStage1EndPosition = accDist;
              this.mStage2EndPosition = (distance - decDist);
              this.mStage3EndPosition = distance;
              return;
          }
  
          if (stopDistance >= distance) { // we cannot make it hit the breaks.
              // we do a force hard stop
              mType = "hard stop";
              float time = 2 * distance / velocity;
              this.mNumberOfStages = 1;
              this.mStage1Velocity = velocity;
              this.mStage2Velocity = 0;
              this.mStage1EndPosition = distance;
              this.mStage1Duration = time;
              return;
          }
  
          float distance_before_break = distance - stopDistance;
          float cruseTime = distance_before_break / velocity; // do we just Cruse then stop?
          if (cruseTime + min_time_to_stop < maxTime) { // close enough maintain v then break
              mType = "cruse decelerate";
              this.mNumberOfStages = 2;
              this.mStage1Velocity = velocity;
              this.mStage2Velocity = velocity;
              this.mStage3Velocity = 0;
              this.mStage1EndPosition = distance_before_break;
              this.mStage2EndPosition = distance;
              this.mStage1Duration = cruseTime;
              this.mStage2Duration = velocity / maxAcceleration;
              return;
          }
  
          float peak_v = (float) Math.sqrt(maxAcceleration * distance + velocity * velocity / 2);
          this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
          this.mStage2Duration = peak_v / maxAcceleration;
          if (peak_v < maxVelocity) { // accelerate then decelerate
              mType = "accelerate decelerate";
              this.mNumberOfStages = 2;
              this.mStage1Velocity = velocity;
              this.mStage2Velocity = peak_v;
              this.mStage3Velocity = 0;
              this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
              this.mStage2Duration = peak_v / maxAcceleration;
              this.mStage1EndPosition = (velocity + peak_v) * this.mStage1Duration / 2;
              this.mStage2EndPosition = distance;
  
              return;
          }
          mType = "accelerate cruse decelerate";
          // accelerate, cruse then decelerate
          this.mNumberOfStages = 3;
          this.mStage1Velocity = velocity;
          this.mStage2Velocity = maxVelocity;
          this.mStage3Velocity = maxVelocity;
  
          this.mStage1Duration = (maxVelocity - velocity) / maxAcceleration;
          this.mStage3Duration = maxVelocity / maxAcceleration;
          float accDist = (velocity + maxVelocity) * this.mStage1Duration / 2;
          float decDist = (maxVelocity * this.mStage3Duration) / 2;
  
          this.mStage2Duration = (distance - accDist - decDist) / maxVelocity;
          this.mStage1EndPosition = accDist;
          this.mStage2EndPosition = (distance - decDist);
          this.mStage3EndPosition = distance;
      }
  
      // Support the simple Decelerate use case
      public static class Decelerate implements StopEngine {
          private float mDestination;
          private float mInitialVelocity;
          private float mAcceleration;
          private float mLastVelocity;
          private float mDuration;
          private float mInitialPos;
          private boolean mDone = false;
  
          @Override
          public String debug(String desc, float time) {
              return mDuration + " " + mLastVelocity;
          }
  
          @Override
          public float getVelocity(float time) {
              if (time > mDuration) {
                  return 0;
              }
              return mLastVelocity = mInitialVelocity + mAcceleration * time;
          }
  
          @Override
          public float getInterpolation(float time) {
              if (time > mDuration) {
                  mDone = true;
                  return mDestination;
              }
              getVelocity(time);
              return mInitialPos + (mInitialVelocity + mAcceleration * time / 2) * time;
          }
  
          @Override
          public float getVelocity() {
              return mLastVelocity;
          }
  
          @Override
          public boolean isStopped() {
              return mDone;
          }
  
          /**
           * Configure simple deceleration controller
           *
           * @param currentPos      the current position
           * @param destination     the destination position
           * @param currentVelocity the currentVelocity change in pos / second
           */
          public void config(float currentPos, float destination, float currentVelocity) {
              mDone = false;
              mDestination = destination;
              mInitialVelocity = currentVelocity;
              mInitialPos = currentPos;
              float distance = mDestination - currentPos;
              mDuration = distance / (currentVelocity / 2);
              mAcceleration = -currentVelocity / mDuration;
          }
      }
  }
  