/*
 *  Copyright (c) 2013, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

package com.m1racle.yuedong.ui.widget.rebound;

import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Classical spring implementing Hooke's law with configurable friction and tension.
 */
public class Spring {

  // unique incrementer id for springs
  private static int ID = 0;

  // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS)
  private static final double MAX_DELTA_TIME_SEC = 0.064;
  // fixed timestep to use in the physics solver in seconds
  private static final double SOLVER_TIMESTEP_SEC = 0.001;
  private SpringConfig mSpringConfig;
  private boolean mOvershootClampingEnabled;

  // storage for the current and prior physics state while integration is occurring
  private static class PhysicsState {
    double position;
    double velocity;
  }

  // unique id for the spring in the system
  private final String mId;
  // all physics simulation objects are final and reused in each processing pass
  private final PhysicsState mCurrentState = new PhysicsState();
  private final PhysicsState mPreviousState = new PhysicsState();
  private final PhysicsState mTempState = new PhysicsState();
  private double mStartValue;
  private double mEndValue;
  private boolean mWasAtRest = true;
  // thresholds for determining when the spring is at rest
  private double mRestSpeedThreshold = 0.005;
  private double mDisplacementFromRestThreshold = 0.005;
  private CopyOnWriteArraySet<SpringListener> mListeners = new CopyOnWriteArraySet<SpringListener>();
  private double mTimeAccumulator = 0;

  private final BaseSpringSystem mSpringSystem;

  /**
   * create a new spring
   */
  Spring(BaseSpringSystem springSystem) {
    if (springSystem == null) {
      throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem");
    }
    mSpringSystem = springSystem;
    mId = "spring:" + ID++;
    setSpringConfig(SpringConfig.defaultConfig);
  }

  /**
   * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be
   * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this,
   * doing so may just cause an exception to be thrown.
   */
  public void destroy() {
    mListeners.clear();
    mSpringSystem.deregisterSpring(this);
  }

  /**
   * get the unique id for this spring
   * @return the unique id
   */
  public String getId() {
    return mId;
  }

  /**
   * set the config class
   * @param springConfig config class for the spring
   * @return this Spring instance for chaining
   */
  public Spring setSpringConfig(SpringConfig springConfig) {
    if (springConfig == null) {
      throw new IllegalArgumentException("springConfig is required");
    }
    mSpringConfig = springConfig;
    return this;
  }

  /**
   * retrieve the spring config for this spring
   * @return the SpringConfig applied to this spring
   */
  public SpringConfig getSpringConfig() {
    return mSpringConfig;
  }

  /**
   * Set the displaced value to determine the displacement for the spring from the rest value.
   * This value is retained and used to calculate the displacement ratio.
   * This also updates the start value of the Spring.
   * @param currentValue the new start and current value for the spring
   * @return the spring for chaining
   */
  public Spring setCurrentValue(double currentValue) {
    mStartValue = currentValue;
    mCurrentState.position = currentValue;
    mSpringSystem.activateSpring(this.getId());
    for (SpringListener listener : mListeners) {
      listener.onSpringUpdate(this);
    }
    return this;
  }

  /**
   * Get the displacement value from the last time setCurrentValue was called.
   * @return displacement value
   */
  public double getStartValue() {
    return mStartValue;
  }

  /**
   * Get the current
   * @return current value
   */
  public double getCurrentValue() {
    return mCurrentState.position;
  }

  /**
   * get the displacement of the springs current value from its rest value.
   * @return the distance displaced by
   */
  public double getCurrentDisplacementDistance() {
    return getDisplacementDistanceForState(mCurrentState);
  }

  /**
   * get the displacement from rest for a given physics state
   * @param state the state to measure from
   * @return the distance displaced by
   */
  private double getDisplacementDistanceForState(PhysicsState state) {
    return Math.abs(mEndValue - state.position);
  }

  /**
   * set the rest value to determine the displacement for the spring
   * @param endValue the endValue for the spring
   * @return the spring for chaining
   */
  public Spring setEndValue(double endValue) {
    if (mEndValue == endValue && isAtRest()) {
      return this;
    }
    mStartValue = getCurrentValue();
    mEndValue = endValue;
    mSpringSystem.activateSpring(this.getId());
    for (SpringListener listener : mListeners) {
      listener.onSpringEndStateChange(this);
    }
    return this;
  }

  /**
   * get the rest value used for determining the displacement of the spring
   * @return the rest value for the spring
   */
  public double getEndValue() {
    return mEndValue;
  }

  /**
   * set the velocity on the spring in pixels per second
   * @return the spring for chaining
   */
  public Spring setVelocity(double velocity) {
    mCurrentState.velocity = velocity;
    mSpringSystem.activateSpring(this.getId());
    return this;
  }

  /**
   * get the velocity of the spring
   * @return the current velocity
   */
  public double getVelocity() {
    return mCurrentState.velocity;
  }

  /**
   * Sets the speed at which the spring should be considered at rest.
   * @param restSpeedThreshold speed pixels per second
   * @return the spring for chaining
   */
  public Spring setRestSpeedThreshold(double restSpeedThreshold) {
    mRestSpeedThreshold = restSpeedThreshold;
    return this;
  }

  /**
   * Returns the speed at which the spring should be considered at rest in pixels per second
   * @return speed in pixels per second
   */
  public double getRestSpeedThreshold() {
    return mRestSpeedThreshold;
  }

  /**
   * set the threshold of displacement from rest below which the spring should be considered at rest
   * @param displacementFromRestThreshold displacement to consider resting below
   * @return the spring for chaining
   */
  public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) {
    mDisplacementFromRestThreshold = displacementFromRestThreshold;
    return this;
  }

  /**
   * get the threshold of displacement from rest below which the spring should be considered at rest
   * @return displacement to consider resting below
   */
  public double getRestDisplacementThreshold() {
    return mDisplacementFromRestThreshold;
  }

  /**
   * Force the spring to clamp at its end value to avoid overshooting the target value.
   * @param overshootClampingEnabled whether or not to enable overshoot clamping
   * @return the spring for chaining
   */
  public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) {
    mOvershootClampingEnabled = overshootClampingEnabled;
    return this;
  }

  /**
   * Check if overshoot clamping is enabled.
   * @return is overshoot clamping enabled
   */
  public boolean isOvershootClampingEnabled() {
    return mOvershootClampingEnabled;
  }

  /**
   * Check if the spring is overshooting beyond its target.
   * @return true if the spring is overshooting its target
   */
  public boolean isOvershooting() {
    return (mStartValue < mEndValue && getCurrentValue() > mEndValue) ||
        (mStartValue > mEndValue && getCurrentValue() < mEndValue);
  }

  /**
   * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required
   * realTimeDelta.
   * The math is inlined inside the loop since it made a huge performance impact when there are
   * several springs being advanced.
   * @param time clock time
   * @param realDeltaTime clock drift
   */
  void advance(double realDeltaTime) {

    boolean isAtRest = isAtRest();

    if (isAtRest && mWasAtRest) {
      /* begin debug
      Log.d(TAG, "bailing out because we are at rest:" + getName());
      end debug */
      return;
    }

    // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able
    // to catch up in a subsequent advance if necessary.
    double adjustedDeltaTime = realDeltaTime;
    if (realDeltaTime > MAX_DELTA_TIME_SEC) {
      adjustedDeltaTime = MAX_DELTA_TIME_SEC;
    }

    /* begin debug
    long startTime = System.currentTimeMillis();
    int iterations = 0;
    end debug */

    mTimeAccumulator += adjustedDeltaTime;

    double tension = mSpringConfig.tension;
    double friction = mSpringConfig.friction;

    double position = mCurrentState.position;
    double velocity = mCurrentState.velocity;
    double tempPosition = mTempState.position;
    double tempVelocity = mTempState.velocity;

    double aVelocity, aAcceleration;
    double bVelocity, bAcceleration;
    double cVelocity, cAcceleration;
    double dVelocity, dAcceleration;

    double dxdt, dvdt;

    // iterate over the true time
    while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) {
      /* begin debug
      iterations++;
      end debug */
      mTimeAccumulator -= SOLVER_TIMESTEP_SEC;

      if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) {
        // This will be the last iteration. Remember the previous state in case we need to
        // interpolate
        mPreviousState.position = position;
        mPreviousState.velocity = velocity;
      }

      // Perform an RK4 integration to provide better detection of the acceleration curve via
      // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation
      // of the next and taking a weighted sum of the 4 derivatives as the final output.

      // This math was inlined since it made for big performance improvements when advancing several
      // springs in one pass of the BaseSpringSystem.

      // The initial derivative is based on the current velocity and the calculated acceleration
      aVelocity = velocity;
      aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity;

      // Calculate the next derivatives starting with the last derivative and integrating over the
      // timestep
      tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5;
      tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
      bVelocity = tempVelocity;
      bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

      tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5;
      tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
      cVelocity = tempVelocity;
      cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

      tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC;
      tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC;
      dVelocity = tempVelocity;
      dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

      // Take the weighted sum of the 4 derivatives as the final output.
      dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity);
      dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration);

      position += dxdt * SOLVER_TIMESTEP_SEC;
      velocity += dvdt * SOLVER_TIMESTEP_SEC;
    }

    mTempState.position = tempPosition;
    mTempState.velocity = tempVelocity;

    mCurrentState.position = position;
    mCurrentState.velocity = velocity;

    if (mTimeAccumulator > 0) {
      interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC);
    }

    // End the spring immediately if it is overshooting and overshoot clamping is enabled.
    // Also make sure that if the spring was considered within a resting threshold that it's now
    // snapped to its end value.
    if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) {
      // Don't call setCurrentValue because that forces a call to onSpringUpdate
      mStartValue = mEndValue;
      mCurrentState.position = mEndValue;
      setVelocity(0);
      isAtRest = true;
    }

    /* begin debug
    long endTime = System.currentTimeMillis();
    long elapsedMillis = endTime - startTime;
    Log.d(TAG,
        "iterations:" + iterations +
            " iterationTime:" + elapsedMillis +
            " position:" + mCurrentState.position +
            " velocity:" + mCurrentState.velocity +
            " realDeltaTime:" + realDeltaTime +
            " adjustedDeltaTime:" + adjustedDeltaTime +
            " isAtRest:" + isAtRest +
            " wasAtRest:" + mWasAtRest);
    end debug */

    // NB: do these checks outside the loop so all listeners are properly notified of the state
    //     transition
    boolean notifyActivate = false;
    if (mWasAtRest) {
      mWasAtRest = false;
      notifyActivate = true;
    }
    boolean notifyAtRest = false;
    if (isAtRest) {
      mWasAtRest = true;
      notifyAtRest = true;
    }
    for (SpringListener listener : mListeners) {
      // starting to move
      if (notifyActivate) {
        listener.onSpringActivate(this);
      }

      // updated
      listener.onSpringUpdate(this);

      // coming to rest
      if (notifyAtRest) {
        listener.onSpringAtRest(this);
      }
    }
  }

  /**
   * Check if this spring should be advanced by the system.  * The rule is if the spring is
   * currently at rest and it was at rest in the previous advance, the system can skip this spring
   * @return should the system process this spring
   */
  public boolean systemShouldAdvance() {
    return !isAtRest() || !wasAtRest();
  }

  /**
   * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending
   * callbacks are fired as the spring comes to a rest.
   * @return true if the spring was at rest in the prior iteration
   */
  public boolean wasAtRest() {
    return mWasAtRest;
  }

  /**
   * check if the current state is at rest
   * @return is the spring at rest
   */
  public boolean isAtRest() {
    return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold &&
        getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold;
  }

  /**
   * Set the spring to be at rest by making its end value equal to its current value and setting
   * velocity to 0.
   */
  public Spring setAtRest() {
    mEndValue = mCurrentState.position;
    mTempState.position = mCurrentState.position;
    mCurrentState.velocity = 0;
    return this;
  }

  /**
   * linear interpolation between the previous and current physics state based on the amount of
   * timestep remaining after processing the rendering delta time in timestep sized chunks.
   * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state
   */
  private void interpolate(double alpha) {
    mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha);
    mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha);
  }

  /** listeners **/

  /**
   * add a listener
   * @param newListener to add
   * @return the spring for chaining
   */
  public Spring addListener(SpringListener newListener) {
    if (newListener == null) {
      throw new IllegalArgumentException("newListener is required");
    }
    mListeners.add(newListener);
    return this;
  }

  /**
   * remove a listener
   * @param listenerToRemove to remove
   * @return the spring for chaining
   */
  public Spring removeListener(SpringListener listenerToRemove) {
    if (listenerToRemove == null) {
      throw new IllegalArgumentException("listenerToRemove is required");
    }
    mListeners.remove(listenerToRemove);
    return this;
  }

  /**
   * remove all of the listeners
   * @return the spring for chaining
   */
  public Spring removeAllListeners() {
    mListeners.clear();
    return this;
  }

  /**
   * This method checks to see that the current spring displacement value is equal to the input,
   * accounting for the spring's rest displacement threshold.
   * @param value The value to compare the spring value to
   * @return Whether the displacement value from the spring is within the bounds of the compare
   * value, accounting for threshold
   */
  public boolean currentValueIsApproximately(double value) {
    return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold();
  }

}

