package com.bottomsheetbehavior;

import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Vector;

/**
 * Original from BottomSheetBehaviorGoogleMapsLike.java
 * https://github.com/miguelhincapie/CustomBottomSheetBehavior/blob/master/app/src/main/java/co/com/parsoniisolutions/custombottomsheetbehavior/lib/BottomSheetBehaviorGoogleMapsLike.java
 */
public class RNBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

  /**
   * Callback for monitoring events about bottom sheets.
   */
  public abstract static class BottomSheetCallback {

    /**
     * Called when the bottom sheet changes its state.
     *
     * @param bottomSheet The bottom sheet view.
     * @param newState    The new state. This will be one of {@link #STATE_DRAGGING},
     *                    {@link #STATE_SETTLING}, {@link #STATE_ANCHOR_POINT},
     *                    {@link #STATE_EXPANDED},
     *                    {@link #STATE_COLLAPSED}, or {@link #STATE_HIDDEN}.
     */
    public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState);

    /**
     * Called when the bottom sheet is being dragged.
     *
     * @param bottomSheet The bottom sheet view.
     * @param slideOffset The new offset of this bottom sheet within its range, from 0 to 1
     *                    when it is moving upward, and from 0 to -1 when it moving downward.
     */
    public abstract void onSlide(@NonNull View bottomSheet, float slideOffset);
  }

  /**
   * The bottom sheet is dragging.
   */
  public static final int STATE_DRAGGING = 1;

  /**
   * The bottom sheet is settling.
   */
  public static final int STATE_SETTLING = 2;

  /**
   * The bottom sheet is expanded_half_way.
   */
  public static final int STATE_ANCHOR_POINT = 6;

  /**
   * The bottom sheet is expanded.
   */
  public static final int STATE_EXPANDED = 3;

  /**
   * The bottom sheet is collapsed.
   */
  public static final int STATE_COLLAPSED = 4;

  /**
   * The bottom sheet is hidden.
   */
  public static final int STATE_HIDDEN = 5;

  /** @hide */
  @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_ANCHOR_POINT, STATE_SETTLING, STATE_HIDDEN})
  @Retention(RetentionPolicy.SOURCE)
  public @interface State {}

  private static final float HIDE_THRESHOLD = 0.5f;
  private static final float HIDE_FRICTION = 0.1f;

  private float mMinimumVelocity;

  private int mPeekHeight;

  private int mMinOffset;
  private int mMaxOffset;

  private static final int DEFAULT_ANCHOR_POINT = 700;
  private int mAnchorPoint;

  private boolean mHideable;

  private boolean mAnchorEnabled;

  @State
  private int mState = STATE_COLLAPSED;
  @State
  private int mLastStableState = STATE_COLLAPSED;

  private ViewDragHelper mViewDragHelper;

  private boolean mIgnoreEvents;

  private boolean mNestedScrolled;

  private int mParentHeight;

  private WeakReference<V> mViewRef;

  private WeakReference<View> mNestedScrollingChildRef;

  private Vector<BottomSheetCallback> mCallback;

  private int mActivePointerId;

  private int mInitialY;

  private boolean mTouchingScrollingChild;

  private BottomSheetHeaderView mHeader;

  /**
   * Default constructor for instantiating BottomSheetBehaviors.
   */
  public RNBottomSheetBehavior(Context context) {
    ViewConfiguration configuration = ViewConfiguration.get(context);
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
  }

  @Override
  public Parcelable onSaveInstanceState( CoordinatorLayout parent, V child ) {
    return new SavedState(super.onSaveInstanceState(parent, child), mState);
  }

  @Override
  public void onRestoreInstanceState( CoordinatorLayout parent, V child, Parcelable state ) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(parent, child, ss.getSuperState());
    // Intermediate states are restored as collapsed state
    if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
      mState = STATE_COLLAPSED;
    } else {
      mState = ss.state;
    }

    mLastStableState = mState;
  }

  @Override
  public boolean onLayoutChild( CoordinatorLayout parent, V child, int layoutDirection ) {
    // First let the parent lay it out
    if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
      if (ViewCompat.getFitsSystemWindows(parent) &&
          !ViewCompat.getFitsSystemWindows(child)) {
        ViewCompat.setFitsSystemWindows(child, true);
      }
      parent.onLayoutChild(child, layoutDirection);
    }
    // Offset the bottom sheet
    mParentHeight = parent.getHeight();
    mMinOffset = Math.max(0, mParentHeight - child.getHeight());
    mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);

    /**
     * New behavior
     */
    if (mAnchorEnabled && mState == STATE_ANCHOR_POINT) {
      toggleHeaderColor(true);
      ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
    } else if (mState == STATE_EXPANDED) {
      toggleHeaderColor(true);
      ViewCompat.offsetTopAndBottom(child, mMinOffset);
    } else if (mHideable && mState == STATE_HIDDEN) {
      ViewCompat.offsetTopAndBottom(child, mParentHeight);
    } else if (mState == STATE_COLLAPSED) {
      toggleHeaderColor(false);
      ViewCompat.offsetTopAndBottom(child, mMaxOffset);
    }
    if ( mViewDragHelper == null ) {
      mViewDragHelper = ViewDragHelper.create( parent, mDragCallback );
    }
    mViewRef = new WeakReference<>(child);
    mNestedScrollingChildRef = new WeakReference<>( findScrollingChild( child ) );
    return true;
  }

  @Override
  public boolean onInterceptTouchEvent( CoordinatorLayout parent, V child, MotionEvent event ) {
    if ( ! child.isShown() ) {
      return false;
    }

    int action = MotionEventCompat.getActionMasked( event );
    if ( action == MotionEvent.ACTION_DOWN ) {
      reset();
    }

    switch ( action ) {
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
        mTouchingScrollingChild = false;
        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
        // Reset the ignore flag
        if (mIgnoreEvents) {
          mIgnoreEvents = false;
          return false;
        }
        break;
      case MotionEvent.ACTION_DOWN:
        int initialX = (int) event.getX();
        mInitialY = (int) event.getY();
        if(mAnchorEnabled && mState == STATE_ANCHOR_POINT){
          mActivePointerId = event.getPointerId(event.getActionIndex());
          mTouchingScrollingChild = true;
        }else {
          View scroll = mNestedScrollingChildRef.get();
          if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
            mActivePointerId = event.getPointerId(event.getActionIndex());
            mTouchingScrollingChild = true;
          }
        }
        mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
            !parent.isPointInChildBounds(child, initialX, mInitialY);
        break;
      case MotionEvent.ACTION_MOVE:
        break;
    }

    if ( action == MotionEvent.ACTION_CANCEL ) {
      // We don't want to trigger a BottomSheet fling as a result of a Cancel MotionEvent (e.g., parent horizontal scroll view taking over touch events)
      mScrollVelocityTracker.clear();
    }

    if ( ! mIgnoreEvents  &&  mViewDragHelper.shouldInterceptTouchEvent( event ) ) {
      return true;
    }
    // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
    // it is not the top most view of its parent. This is not necessary when the touch event is
    // happening over the scrolling content as nested scrolling logic handles that case.
    View scroll = mNestedScrollingChildRef.get();
    boolean ret = action == MotionEvent.ACTION_MOVE && scroll != null &&
        !mIgnoreEvents && mState != STATE_DRAGGING &&
        !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
        Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
    return ret;
  }

  @Override
  public boolean onTouchEvent( CoordinatorLayout parent, V child, MotionEvent event ) {
    if ( ! child.isShown() ) {
      return false;
    }

    int action = MotionEventCompat.getActionMasked( event );
    if ( mState == STATE_DRAGGING  &&  action == MotionEvent.ACTION_DOWN ) {
      toggleHeaderColor(true);
      return true;
    }

    if (mViewDragHelper == null) {
      mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
    }

    mViewDragHelper.processTouchEvent( event );

    if ( action == MotionEvent.ACTION_DOWN ) {
      reset();
    }

    // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
    // to capture the bottom sheet in case it is not captured and the touch slop is passed.
    if ( action == MotionEvent.ACTION_MOVE  &&  ! mIgnoreEvents ) {
      if ( Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop() ) {
        mViewDragHelper.captureChildView( child, event.getPointerId(event.getActionIndex()) );
      }
    }
    return ! mIgnoreEvents;
  }

  @Override
  public boolean onStartNestedScroll( CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes ) {
    mNestedScrolled = false;
    return ( nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL ) != 0;
  }

  private ScrollVelocityTracker mScrollVelocityTracker = new ScrollVelocityTracker();
  private class ScrollVelocityTracker {
    private long  mPreviousScrollTime = 0;
    private float mScrollVelocity     = 0;

    public void recordScroll( int dy ) {
      long now = System.currentTimeMillis();

      if ( mPreviousScrollTime != 0 ) {
        long elapsed = now - mPreviousScrollTime;
        mScrollVelocity = (float) dy / elapsed * 1000; // pixels per sec
      }

      mPreviousScrollTime = now;
    }

    public void clear() {
      mPreviousScrollTime = 0;
      mScrollVelocity = 0;
    }

    public float getScrollVelocity() {
      return mScrollVelocity;
    }
  }

  @Override
  public void onNestedPreScroll( CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed ) {
    View scrollingChild = mNestedScrollingChildRef.get();
    if ( target != scrollingChild ) {
      return;
    }

    mScrollVelocityTracker.recordScroll( dy );

    int currentTop = child.getTop();
    int newTop     = currentTop - dy;

    // Force stop at the anchor - do not go from collapsed to expanded in one scroll
    if (mAnchorEnabled && (
        ( mLastStableState == STATE_COLLAPSED  &&  newTop < mAnchorPoint )  ||
            ( mLastStableState == STATE_EXPANDED   &&  newTop > mAnchorPoint )
        )) {
      consumed[1] = dy;
      ViewCompat.offsetTopAndBottom( child, mAnchorPoint - currentTop );
      dispatchOnSlide( child.getTop() );
      mNestedScrolled = true;
      return;
    }

    if ( dy > 0 ) { // Upward
      if ( newTop < mMinOffset ) {
        consumed[1] = currentTop - mMinOffset;
        ViewCompat.offsetTopAndBottom( child, -consumed[1] );
        setStateInternal( STATE_EXPANDED );
      } else {
        consumed[1] = dy;
        ViewCompat.offsetTopAndBottom( child, -dy );
        setStateInternal( STATE_DRAGGING );
        toggleHeaderColor(true);
      }
    }
    else
    if ( dy < 0 ) { // Downward
      if ( ! ViewCompat.canScrollVertically(target, -1) ) {
        if ( newTop <= mMaxOffset || mHideable ) {
          consumed[1] = dy;
          ViewCompat.offsetTopAndBottom(child, -dy);
          setStateInternal(STATE_DRAGGING);
        } else {
          consumed[1] = currentTop - mMaxOffset;
          ViewCompat.offsetTopAndBottom(child, -consumed[1]);
          setStateInternal(STATE_COLLAPSED);
          toggleHeaderColor(false);
        }
      }
    }
    dispatchOnSlide(child.getTop());
    mNestedScrolled = true;
  }

  @Override
  public void onStopNestedScroll( CoordinatorLayout coordinatorLayout, V child, View target ) {
    if ( child.getTop() == mMinOffset ) {
      setStateInternal( STATE_EXPANDED );
      mLastStableState = STATE_EXPANDED;
      return;
    }
    if ( target != mNestedScrollingChildRef.get()  ||  ! mNestedScrolled ) {
      return;
    }
    int top;
    int targetState;

    // Are we flinging up?
    float scrollVelocity = mScrollVelocityTracker.getScrollVelocity();
    if ( scrollVelocity > mMinimumVelocity) {
      if (mAnchorEnabled && mLastStableState == STATE_COLLAPSED ) {
        // Fling from collapsed to anchor
        top = mAnchorPoint;
        targetState = STATE_ANCHOR_POINT;
      }
      else
      if (mAnchorEnabled && mLastStableState == STATE_ANCHOR_POINT ) {
        // Fling from anchor to expanded
        top = mMinOffset;
        targetState = STATE_EXPANDED;
      }
      else {
        // We are already expanded
        top = mMinOffset;
        targetState = STATE_EXPANDED;
      }
    }
    else
      // Are we flinging down?
      if ( scrollVelocity < -mMinimumVelocity ) {
        if (mAnchorEnabled && mLastStableState == STATE_EXPANDED ) {
          // Fling to from expanded to anchor
          top = mAnchorPoint;
          targetState = STATE_ANCHOR_POINT;
        }
        else
        if (mAnchorEnabled && mLastStableState == STATE_ANCHOR_POINT ) {
          // Fling from anchor to collapsed
          top = mMaxOffset;
          targetState = STATE_COLLAPSED;
          toggleHeaderColor(false);
        }
        else {
          // We are already collapsed
          top = mMaxOffset;
          targetState = STATE_COLLAPSED;
          toggleHeaderColor(false);
        }
      }
      // Not flinging, just settle to the nearest state
      else {
        // Collapse?
        int currentTop = child.getTop();
        if ( currentTop > mAnchorPoint * 1.25 ) { // Multiply by 1.25 to account for parallax. The currentTop needs to be pulled down 50% of the anchor point before collapsing.
          top = mMaxOffset;
          targetState = STATE_COLLAPSED;
          toggleHeaderColor(false);
        }
        // Expand?
        else
        if ( currentTop < mAnchorPoint * 0.5 ) {
          top = mMinOffset;
          targetState = STATE_EXPANDED;
        }
        // Snap back to the anchor
        else if (mAnchorEnabled) {
          top = mAnchorPoint;
          targetState = STATE_ANCHOR_POINT;
        } else {
          top = mMaxOffset;
          targetState = STATE_COLLAPSED;
        }
      }

    mLastStableState = targetState;
    if ( mViewDragHelper.smoothSlideViewTo( child, child.getLeft(), top ) ) {
      setStateInternal( STATE_SETTLING );
      ViewCompat.postOnAnimation( child, new SettleRunnable( child, targetState ) );
    } else {
      setStateInternal( targetState );
    }
    mNestedScrolled = false;
  }

  @Override
  public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                                  float velocityX, float velocityY) {
    return target == mNestedScrollingChildRef.get() &&
        (mState != STATE_EXPANDED ||
            super.onNestedPreFling(coordinatorLayout, child, target,
                velocityX, velocityY));
  }

  /**
   * Sets the height of the bottom sheet when it is collapsed.
   *
   * @param peekHeight The height of the collapsed bottom sheet in pixels.
   * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_peekHeight
   */
  public final void setPeekHeight(int peekHeight) {
    mPeekHeight = Math.max(0, peekHeight);
    mMaxOffset = mParentHeight - peekHeight;
  }

  /**
   * Gets the height of the bottom sheet when it is collapsed.
   *
   * @return The height of the collapsed bottom sheet.
   * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_peekHeight
   */
  public final int getPeekHeight() {
    return mPeekHeight;
  }

  public void setAnchorPoint(int anchorPoint) {
    mAnchorPoint = anchorPoint;
  }
  public int getAnchorPoint(){
    return mAnchorPoint;
  }

  /**
   * Sets whether this bottom sheet can hide when it is swiped down.
   *
   * @param hideable {@code true} to make this bottom sheet hideable.
   * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_hideable
   */
  public void setHideable(boolean hideable) {
    mHideable = hideable;
  }

  public boolean getAnchorEnabled() {
    return mAnchorEnabled;
  }

  /**
   * Sets whether this bottom sheet can have an anchor point.
   * @param anchorEnabled
   */
  public void setAnchorEnabled(boolean anchorEnabled) {
    mAnchorEnabled = anchorEnabled;
  }

  /**
   * Gets whether this bottom sheet can hide when it is swiped down.
   *
   * @return {@code true} if this bottom sheet can hide.
   * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_hideable
   */
  public boolean isHideable() {
    return mHideable;
  }

  /**
   * Adds a callback to be notified of bottom sheet events.
   *
   * @param callback The callback to notify when bottom sheet events occur.
   */
  public void addBottomSheetCallback(BottomSheetCallback callback) {
    if (mCallback == null)
      mCallback = new Vector<>();

    mCallback.add(callback);
  }

  public void setHeader(BottomSheetHeaderView header) {
    mHeader = header;
  }

  /**
   * Sets the state of the bottom sheet. The bottom sheet will transition to that state with
   * animation.
   *
   * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_ANCHOR_POINT},
   *              {@link #STATE_EXPANDED} or {@link #STATE_HIDDEN}.
   */
  public final void setState( @State int state ) {
    if ( state == mState ) {
      return;
    }

    mLastStableState = state;

    if ( mViewRef == null ) {
      // The view is not laid out yet; modify mState and let onLayoutChild handle it later
      /**
       * New behavior (added: state == STATE_ANCHOR_POINT ||)
       */
      if (state == STATE_COLLAPSED || state == STATE_EXPANDED || state == STATE_ANCHOR_POINT ||
          (mHideable && state == STATE_HIDDEN)) {
        mState = state;
      }
      return;
    }
    V child = mViewRef.get();
    if (child == null) {
      return;
    }
    int top;
    boolean toggleColor = false;
    if (state == STATE_COLLAPSED) {
      top = mMaxOffset;
    }
    else
    if (mAnchorEnabled && state == STATE_ANCHOR_POINT) {
      toggleColor = true;
      top = mAnchorPoint;
    }
    else
    if (state == STATE_EXPANDED) {
      toggleColor = true;
      top = mMinOffset;
    }
    else
    if (mHideable && state == STATE_HIDDEN) {
      top = mParentHeight;
    } else {
      throw new IllegalArgumentException("Illegal state argument: " + state);
    }
    toggleHeaderColor(toggleColor);
    setStateInternal(STATE_SETTLING);
    if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
      ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
    }
  }

  /**
   * Gets the current state of the bottom sheet.
   *
   * @return One of {@link #STATE_EXPANDED}, {@link #STATE_ANCHOR_POINT}, {@link #STATE_COLLAPSED},
   * {@link #STATE_DRAGGING}, and {@link #STATE_SETTLING}.
   */
  @State
  public final int getState() {
    return mState;
  }

  private void setStateInternal(@State int state) {
    if (mState == state) {
      return;
    }
    mState = state;
    View bottomSheet = mViewRef.get();
    if (bottomSheet != null && mCallback != null) {
//            mCallback.onStateChanged(bottomSheet, state);
      notifyStateChangedToListeners(bottomSheet, state);
    }
  }

  private void notifyStateChangedToListeners(@NonNull View bottomSheet, @State int newState) {
    for (BottomSheetCallback bottomSheetCallback:mCallback) {
      bottomSheetCallback.onStateChanged(bottomSheet, newState);
    }
  }

  private void notifyOnSlideToListeners(@NonNull View bottomSheet, float slideOffset) {
    for (BottomSheetCallback bottomSheetCallback:mCallback) {
      bottomSheetCallback.onSlide(bottomSheet, slideOffset);
    }
  }

  private void reset() {
    mActivePointerId = ViewDragHelper.INVALID_POINTER;
  }

  private void toggleHeaderColor(boolean activate) {
    if (mHeader != null) {
      mHeader.toggle(activate);
    }
  }

  private boolean shouldHide(View child, float yvel) {
    if (child.getTop() < mMaxOffset) {
      // It should not hide, but collapse.
      return false;
    }
    final float newTop = child.getTop() + yvel * HIDE_FRICTION;
    return Math.abs(newTop - mMaxOffset) / (float) mPeekHeight > HIDE_THRESHOLD;
  }

  private View findScrollingChild(View view) {
    if (view instanceof NestedScrollingChild) {
      return view;
    }
    if (view instanceof ViewGroup) {
      ViewGroup group = (ViewGroup) view;
      for (int i = 0, count = group.getChildCount(); i < count; i++) {
        View scrollingChild = findScrollingChild(group.getChildAt(i));
        if (scrollingChild != null) {
          return scrollingChild;
        }
      }
    }
    return null;
  }

  private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {

    @Override
    public boolean tryCaptureView( View child, int pointerId ) {
      if ( mState == STATE_DRAGGING ) {
        return false;
      }
      if ( mTouchingScrollingChild ) {
        return false;
      }
      if ( mState == STATE_EXPANDED  &&  mActivePointerId == pointerId ) {
        View scroll = mNestedScrollingChildRef.get();
        if (scroll != null && ViewCompat.canScrollVertically(scroll, -1)) {
          // Let the content scroll up
          return false;
        }
      }
      return mViewRef != null && mViewRef.get() == child;
    }

    @Override
    public void onViewPositionChanged( View changedView, int left, int top, int dx, int dy ) {
      dispatchOnSlide( top );
    }

    @Override
    public void onViewDragStateChanged( int state ) {
      if ( state == ViewDragHelper.STATE_DRAGGING ) {
        setStateInternal( STATE_DRAGGING );
      }
    }

    @Override
    public void onViewReleased( View releasedChild, float xvel, float yvel ) {
      int top;
      @State int targetState;
      if ( yvel < 0 ) { // Moving up
        top = mMinOffset;
        targetState = STATE_EXPANDED;
      }
      else
      if ( mHideable  &&  shouldHide(releasedChild, yvel) ) {
        top = mParentHeight;
        targetState = STATE_HIDDEN;
      }
      else
      if ( yvel == 0.f ) {
        int currentTop = releasedChild.getTop();
        if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
          top = mMinOffset;
          targetState = STATE_EXPANDED;
        } else {
          top = mMaxOffset;
          targetState = STATE_COLLAPSED;
          toggleHeaderColor(false);
        }
      } else {
        top = mMaxOffset;
        targetState = STATE_COLLAPSED;
        toggleHeaderColor(false);
      }

      mLastStableState = targetState;
      if ( mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top) ) {
        setStateInternal(STATE_SETTLING);
        ViewCompat.postOnAnimation(releasedChild,
            new SettleRunnable(releasedChild, targetState));
      } else {
        setStateInternal(targetState);
      }
    }

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
      return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
    }
    int constrain(int amount, int low, int high) {
      return amount < low ? low : (amount > high ? high : amount);
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
      return child.getLeft();
    }

    @Override
    public int getViewVerticalDragRange(View child) {
      if (mHideable) {
        return mParentHeight - mMinOffset;
      } else {
        return mMaxOffset - mMinOffset;
      }
    }
  };

  private void dispatchOnSlide( int top ) {
    View bottomSheet = mViewRef.get();
    if (bottomSheet != null && mCallback != null) {
      if (top > mMaxOffset) {
        notifyOnSlideToListeners(bottomSheet, (float) (mMaxOffset - top) / mPeekHeight);
      } else {
        notifyOnSlideToListeners(bottomSheet, (float) (mMaxOffset - top) / ((mMaxOffset - mMinOffset)));
      }
    }
  }

  private class SettleRunnable implements Runnable {

    private final View mView;

    @State
    private final int mTargetState;

    SettleRunnable( View view, @State int targetState ) {
      mView = view;
      mTargetState = targetState;
    }

    @Override
    public void run() {
      if ( mViewDragHelper != null  &&  mViewDragHelper.continueSettling( true ) ) {
        ViewCompat.postOnAnimation( mView, this );
      } else {
        setStateInternal( mTargetState );
      }
    }
  }

  protected static class SavedState extends View.BaseSavedState {

    @State
    final int state;

    public SavedState( Parcel source ) {
      super( source );
      // noinspection ResourceType
      state = source.readInt();
    }

    public SavedState(Parcelable superState, @State int state) {
      super(superState);
      this.state = state;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeInt(state);
    }

    public static final Parcelable.Creator<SavedState> CREATOR =
        new Parcelable.Creator<SavedState>() {
          @Override
          public SavedState createFromParcel(Parcel source) {
            return new SavedState(source);
          }

          @Override
          public SavedState[] newArray(int size) {
            return new SavedState[size];
          }
        };
  }

  /**
   * A utility function to get the {@link RNBottomSheetBehavior} associated with the {@code view}.
   *
   * @param view The {@link View} with {@link RNBottomSheetBehavior}.
   * @return The {@link RNBottomSheetBehavior} associated with the {@code view}.
   */
  @SuppressWarnings("unchecked")
  public static <V extends View> RNBottomSheetBehavior<V> from(V view) {
    ViewGroup.LayoutParams params = view.getLayoutParams();
    if (!(params instanceof CoordinatorLayout.LayoutParams)) {
      throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
    }
    CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
        .getBehavior();
    if (!(behavior instanceof RNBottomSheetBehavior)) {
      throw new IllegalArgumentException(
          "The view is not associated with RNBottomSheetBehavior");
    }
    return (RNBottomSheetBehavior<V>) behavior;
  }

}