package com.rey.material.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

import com.rey.material.R;
import com.rey.material.app.ThemeManager;
import com.rey.material.drawable.RippleDrawable;
import com.rey.material.util.ColorUtil;
import com.rey.material.util.ThemeUtil;
import com.rey.material.util.TypefaceUtil;
import com.rey.material.util.ViewUtil;

/**
 * Created by Ret on 3/18/2015.
 */
public class Slider extends View implements ThemeManager.OnThemeChangedListener{

    private RippleManager mRippleManager;
    protected int mStyleId;
    protected int mCurrentStyle = ThemeManager.THEME_UNDEFINED;

    private Paint mPaint;
    private RectF mDrawRect;
    private RectF mTempRect;
    private Path mLeftTrackPath;
    private Path mRightTrackPath;
    private Path mMarkPath;

    private int mMinValue = 0;
    private int mMaxValue = 100;
    private int mStepValue = 1;

    private boolean mDiscreteMode = false;

    private int mPrimaryColor;
    private int mSecondaryColor;
    private int mTrackSize = -1;
    private Paint.Cap mTrackCap = Paint.Cap.BUTT;
    private int mThumbBorderSize = -1;
    private int mThumbRadius = -1;
    private int mThumbFocusRadius = -1;
    private int mThumbTouchRadius = -1;
    private float mThumbPosition = -1;
    private Typeface mTypeface = Typeface.DEFAULT;
    private int mTextSize = -1;
    private int mTextColor = 0xFFFFFFFF;
    private int mGravity = Gravity.CENTER;
    private int mTravelAnimationDuration = -1;
    private int mTransformAnimationDuration = -1;
    private Interpolator mInterpolator;
    private int mBaselineOffset;

    private int mTouchSlop;
    private PointF mMemoPoint;
    private boolean mIsDragging;
    private float mThumbCurrentRadius;
    private float mThumbFillPercent;
    private boolean mAlwaysFillThumb = false;
    private int mTextHeight;
    private int mMemoValue;
    private String mValueText;

    private ThumbRadiusAnimator mThumbRadiusAnimator;
    private ThumbStrokeAnimator mThumbStrokeAnimator;
    private ThumbMoveAnimator mThumbMoveAnimator;

    private boolean mIsRtl = false;

    /**
     * Interface definition for a callback to be invoked when thumb's position changed.
     */
    public interface OnPositionChangeListener{
        /**
         * Called when thumb's position changed.
         *
         * @param view The view fire this event.
         * @param fromUser Indicate the change is from user touch event or not.
         * @param oldPos The old position of thumb.
         * @param newPos The new position of thumb.
         * @param oldValue The old value.
         * @param newValue The new value.
         */
        void onPositionChanged(Slider view, boolean fromUser, float oldPos, float newPos, int oldValue, int newValue);
    }

    private OnPositionChangeListener mOnPositionChangeListener;

    public interface ValueDescriptionProvider{

        String getDescription(int value);

    }

    private ValueDescriptionProvider mValueDescriptionProvider;

    public Slider(Context context) {
        super(context);

        init(context, null, 0, 0);
    }

    public Slider(Context context, AttributeSet attrs) {
        super(context, attrs);

        init(context, attrs, 0, 0);
    }

    public Slider(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init(context, attrs, defStyleAttr, 0);
    }

    protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        //default color
        mPrimaryColor = ThemeUtil.colorControlActivated(context, 0xFF000000);
        mSecondaryColor = ThemeUtil.colorControlNormal(context, 0xFF000000);

        mDrawRect = new RectF();
        mTempRect = new RectF();
        mLeftTrackPath = new Path();
        mRightTrackPath = new Path();

        mThumbRadiusAnimator = new ThumbRadiusAnimator();
        mThumbStrokeAnimator = new ThumbStrokeAnimator();
        mThumbMoveAnimator = new ThumbMoveAnimator();

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMemoPoint = new PointF();

        applyStyle(context, attrs, defStyleAttr, defStyleRes);

        if(!isInEditMode())
            mStyleId = ThemeManager.getStyleId(context, attrs, defStyleAttr, defStyleRes);
    }

    public void applyStyle(int resId){
        ViewUtil.applyStyle(this, resId);
        applyStyle(getContext(), null, 0, resId);
    }

    protected void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
        getRippleManager().onCreate(this, context, attrs, defStyleAttr, defStyleRes);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes);
        int minValue = getMinValue();
        int maxValue = getMaxValue();
        boolean valueRangeDefined = false;
        int value = -1;
        boolean valueDefined = false;
        String familyName = null;
        int style = Typeface.NORMAL;
        boolean textStyleDefined = false;
        for(int i = 0, count = a.getIndexCount(); i < count; i++){
            int attr = a.getIndex(i);
            if(attr == R.styleable.Slider_sl_discreteMode)
                mDiscreteMode = a.getBoolean(attr, false);
            else if(attr == R.styleable.Slider_sl_primaryColor)
                mPrimaryColor = a.getColor(attr, 0);
            else if(attr == R.styleable.Slider_sl_secondaryColor)
                mSecondaryColor = a.getColor(attr, 0);
            else if(attr == R.styleable.Slider_sl_trackSize)
                mTrackSize = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_sl_trackCap) {
                int cap = a.getInteger(attr, 0);
                if(cap == 0)
                    mTrackCap = Paint.Cap.BUTT;
                else if(cap == 1)
                    mTrackCap = Paint.Cap.ROUND;
                else
                    mTrackCap = Paint.Cap.SQUARE;
            }
            else if(attr == R.styleable.Slider_sl_thumbBorderSize)
                mThumbBorderSize = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_sl_thumbRadius)
                mThumbRadius = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_sl_thumbFocusRadius)
                mThumbFocusRadius = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_sl_thumbTouchRadius)
                mThumbTouchRadius = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_sl_travelAnimDuration) {
                mTravelAnimationDuration = a.getInteger(attr, 0);
                mTransformAnimationDuration = mTravelAnimationDuration;
            }
            else if(attr == R.styleable.Slider_sl_alwaysFillThumb) {
                mAlwaysFillThumb = a.getBoolean(R.styleable.Slider_sl_alwaysFillThumb, false);
            }
            else if(attr == R.styleable.Slider_sl_interpolator){
                int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0);
                mInterpolator = AnimationUtils.loadInterpolator(context, resId);
            }
            else if(attr == R.styleable.Slider_android_gravity)
                mGravity = a.getInteger(attr, 0);
            else if(attr == R.styleable.Slider_sl_minValue) {
                minValue = a.getInteger(attr, 0);
                valueRangeDefined = true;
            }
            else if(attr == R.styleable.Slider_sl_maxValue) {
                maxValue = a.getInteger(attr, 0);
                valueRangeDefined = true;
            }
            else if(attr == R.styleable.Slider_sl_stepValue)
                mStepValue = a.getInteger(attr, 0);
            else if(attr == R.styleable.Slider_sl_value) {
                value = a.getInteger(attr, 0);
                valueDefined = true;
            }
            else if(attr == R.styleable.Slider_sl_fontFamily) {
                familyName = a.getString(attr);
                textStyleDefined = true;
            }
            else if(attr == R.styleable.Slider_sl_textStyle) {
                style = a.getInteger(attr, 0);
                textStyleDefined = true;
            }
            else if(attr == R.styleable.Slider_sl_textColor)
                mTextColor = a.getColor(attr, 0);
            else if(attr == R.styleable.Slider_sl_textSize)
                mTextSize = a.getDimensionPixelSize(attr, 0);
            else if(attr == R.styleable.Slider_android_enabled)
                setEnabled(a.getBoolean(attr, true));
            else if(attr == R.styleable.Slider_sl_baselineOffset)
                mBaselineOffset = a.getDimensionPixelOffset(attr, 0);
        }

        a.recycle();

        if(mTrackSize < 0)
            mTrackSize = ThemeUtil.dpToPx(context, 2);

        if(mThumbBorderSize < 0)
            mThumbBorderSize = ThemeUtil.dpToPx(context, 2);

        if(mThumbRadius < 0)
            mThumbRadius = ThemeUtil.dpToPx(context, 10);

        if(mThumbFocusRadius < 0)
            mThumbFocusRadius = ThemeUtil.dpToPx(context, 14);

        if(mTravelAnimationDuration < 0){
            mTravelAnimationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
            mTransformAnimationDuration = mTravelAnimationDuration;
        }

        if(mInterpolator == null)
            mInterpolator = new DecelerateInterpolator();

        if(valueRangeDefined)
            setValueRange(minValue, maxValue, false);

        if(valueDefined)
            setValue(value, false);
        else if(mThumbPosition < 0)
            setValue(mMinValue, false);

        if(textStyleDefined)
            mTypeface = TypefaceUtil.load(context, familyName, style);

        if(mTextSize < 0)
            mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material);

        mPaint.setTextSize(mTextSize);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setTypeface(mTypeface);

        measureText();
        invalidate();
    }

    @Override
    public void onThemeChanged(ThemeManager.OnThemeChangedEvent event) {
        int style = ThemeManager.getInstance().getCurrentStyle(mStyleId);
        if(mCurrentStyle != style){
            mCurrentStyle = style;
            applyStyle(mCurrentStyle);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if(mStyleId != 0) {
            ThemeManager.getInstance().registerOnThemeChangedListener(this);
            onThemeChanged(null);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        RippleManager.cancelRipple(this);
        if(mStyleId != 0)
            ThemeManager.getInstance().unregisterOnThemeChangedListener(this);
    }

    private void measureText(){
        if(mValueText == null)
            return;

        Rect temp = new Rect();
        mPaint.setTextSize(mTextSize);
        float width = mPaint.measureText(mValueText);
        float maxWidth = (float)(mThumbRadius * Math.sqrt(2) * 2 - ThemeUtil.dpToPx(getContext(), 8));
        if(width > maxWidth){
            float textSize = mTextSize * maxWidth / width;
            mPaint.setTextSize(textSize);
        }

        mPaint.getTextBounds(mValueText, 0, mValueText.length(), temp);
        mTextHeight = temp.height();
    }

    private String getValueText(){
        int value = getValue();
        if(mValueText == null || mMemoValue != value){
            mMemoValue = value;
            mValueText = mValueDescriptionProvider == null ? String.valueOf(mMemoValue) : mValueDescriptionProvider.getDescription(mMemoValue);
            measureText();
        }

        return mValueText;
    }

    /**
     * @return The minimum selectable value.
     */
    public int getMinValue(){
        return mMinValue;
    }

    /**
     * @return The maximum selectable value.
     */
    public int getMaxValue(){
        return mMaxValue;
    }

    /**
     * @return The step value.
     */
    public int getStepValue(){
        return mStepValue;
    }

    /**
     * Set the randge of selectable value.
     * @param min The minimum selectable value.
     * @param max The maximum selectable value.
     * @param animation Indicate that should show animation when thumb's current position changed.
     */
    public void setValueRange(int min, int max, boolean animation){
        if(max < min || (min == mMinValue && max == mMaxValue))
            return;

        float oldValue = getExactValue();
        float oldPosition = getPosition();
        mMinValue = min;
        mMaxValue = max;

        setValue(oldValue, animation);
        if(mOnPositionChangeListener != null && oldPosition == getPosition() && oldValue != getExactValue())
            mOnPositionChangeListener.onPositionChanged(this, false, oldPosition, oldPosition, Math.round(oldValue), getValue());
    }

    /**
     * @return The selected value.
     */
    public int getValue(){
        return Math.round(getExactValue());
    }

    /**
     * @return The exact selected value.
     */
    public float getExactValue(){
        return (mMaxValue - mMinValue) * getPosition() + mMinValue;
    }

    /**
     * @return The current position of thumb in [0..1] range.
     */
    public float getPosition(){
        return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition;
    }

    /**
     * Set current position of thumb.
     * @param pos The position in [0..1] range.
     * @param animation Indicate that should show animation when change thumb's position.
     */
    public void setPosition(float pos, boolean animation){
        setPosition(pos, animation, animation, false);
    }

    private void setPosition(float pos, boolean moveAnimation, boolean transformAnimation, boolean fromUser){
        boolean change = getPosition() != pos;
        int oldValue = getValue();
        float oldPos = getPosition();

        if(!moveAnimation || !mThumbMoveAnimator.startAnimation(pos)){
            mThumbPosition = pos;

            if(transformAnimation) {
                if(!mIsDragging)
                    mThumbRadiusAnimator.startAnimation(mThumbRadius);
                mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1);
            }
            else{
                mThumbCurrentRadius = mThumbRadius;
                mThumbFillPercent = (mAlwaysFillThumb || mThumbPosition != 0) ? 1 : 0;
                invalidate();
            }
        }

        int newValue = getValue();
        float newPos = getPosition();

        if(change && mOnPositionChangeListener != null)
            mOnPositionChangeListener.onPositionChanged(this, fromUser, oldPos, newPos, oldValue, newValue);
    }

    /**
     * Changes the primary color and invalidates the view to force a redraw.
     * @param color New color to assign to mPrimaryColor.
     */
    public void setPrimaryColor(int color) {
        mPrimaryColor = color;
        invalidate();
    }

    /**
     * Changes the secondary color and invalidates the view to force a redraw.
     * @param color New color to assign to mSecondaryColor.
     */
    public void setSecondaryColor(int color) {
        mSecondaryColor = color;
        invalidate();
    }

    /**
     * Set if we want the thumb to always be filled.
     * @param alwaysFillThumb Do we want it to always be filled.
     */
    public void setAlwaysFillThumb(boolean alwaysFillThumb) {
        mAlwaysFillThumb = alwaysFillThumb;
    }

    /**
     * Set the selected value of this Slider.
     * @param value The selected value.
     * @param animation Indicate that should show animation when change thumb's position.
     */
    public void setValue(float value, boolean animation){
        value = Math.min(mMaxValue, Math.max(value, mMinValue));
        setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation);
    }

    /**
     * Set a listener will be called when thumb's position changed.
     * @param listener The {@link OnPositionChangeListener} will be called.
     */
    public void setOnPositionChangeListener(OnPositionChangeListener listener){
        mOnPositionChangeListener = listener;
    }

    public void setValueDescriptionProvider(ValueDescriptionProvider provider){
        mValueDescriptionProvider = provider;
    }

    @Override
    public void setBackgroundDrawable(Drawable drawable) {
        Drawable background = getBackground();
        if(background instanceof RippleDrawable && !(drawable instanceof RippleDrawable))
            ((RippleDrawable) background).setBackgroundDrawable(drawable);
        else
            super.setBackgroundDrawable(drawable);
    }

    protected RippleManager getRippleManager(){
        if(mRippleManager == null){
            synchronized (RippleManager.class){
                if(mRippleManager == null)
                    mRippleManager = new RippleManager();
            }
        }

        return mRippleManager;
    }

    @Override
    public void setOnClickListener(OnClickListener l) {
        RippleManager rippleManager = getRippleManager();
        if (l == rippleManager)
            super.setOnClickListener(l);
        else {
            rippleManager.setOnClickListener(l);
            setOnClickListener(rippleManager);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        switch (widthMode) {
            case MeasureSpec.UNSPECIFIED:
                widthSize = getSuggestedMinimumWidth();
                break;
            case MeasureSpec.AT_MOST:
                widthSize = Math.min(widthSize, getSuggestedMinimumWidth());
                break;
        }

        switch (heightMode) {
            case MeasureSpec.UNSPECIFIED:
                heightSize = getSuggestedMinimumHeight();
                break;
            case MeasureSpec.AT_MOST:
                heightSize = Math.min(heightSize, getSuggestedMinimumHeight());
                break;
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    public int getSuggestedMinimumWidth() {
        return (mDiscreteMode ? (int)(mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight();
    }

    @Override
    public int getSuggestedMinimumHeight() {
        return (mDiscreteMode ? (int)(mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom();
    }

    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        boolean rtl = layoutDirection == LAYOUT_DIRECTION_RTL;
        if(mIsRtl != rtl) {
            mIsRtl = rtl;
            invalidate();
        }
    }

    @Override
    public int getBaseline() {
        int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        int baseline;

        if(mDiscreteMode){
            int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2)));
            int height = mThumbRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    baseline = Math.max(getPaddingTop(), fullHeight - height) + mThumbRadius;
                    break;
                case Gravity.BOTTOM:
                    baseline = getMeasuredHeight() - getPaddingBottom();
                    break;
                default:
                    baseline = Math.round(Math.max((getMeasuredHeight() - height) / 2f, fullHeight - height) + mThumbRadius);
                    break;
            }
        }
        else{
            int height = mThumbFocusRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    baseline = getPaddingTop() + mThumbFocusRadius;
                    break;
                case Gravity.BOTTOM:
                    baseline = getMeasuredHeight() - getPaddingBottom();
                    break;
                default:
                    baseline = Math.round((getMeasuredHeight() - height) / 2f + mThumbFocusRadius);
                    break;
            }
        }

        return baseline + mBaselineOffset;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mDrawRect.left = getPaddingLeft() + mThumbRadius;
        mDrawRect.right = w - getPaddingRight() - mThumbRadius;

        int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

        if(mDiscreteMode){
            int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2)));
            int height = mThumbRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height);
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
                case Gravity.BOTTOM:
                    mDrawRect.bottom = h - getPaddingBottom();
                    mDrawRect.top = mDrawRect.bottom - height;
                    break;
                default:
                    mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height);
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
            }
        }
        else{
            int height = mThumbFocusRadius * 2;
            switch (align) {
                case Gravity.TOP:
                    mDrawRect.top = getPaddingTop();
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
                case Gravity.BOTTOM:
                    mDrawRect.bottom = h - getPaddingBottom();
                    mDrawRect.top = mDrawRect.bottom - height;
                    break;
                default:
                    mDrawRect.top = (h - height) / 2f;
                    mDrawRect.bottom = mDrawRect.top + height;
                    break;
            }
        }
    }

    private boolean isThumbHit(float x, float y, float radius){
        float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left;
        float cy = mDrawRect.centerY();

        return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius;
    }

    private double distance(float x1, float y1, float x2, float y2){
        return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
    }

    private float correctPosition(float position){
        if(!mDiscreteMode)
            return position;

        int totalOffset = mMaxValue - mMinValue;
        int valueOffset = Math.round(totalOffset * position);
        int stepOffset = valueOffset / mStepValue;
        int lowerValue = stepOffset * mStepValue;
        int higherValue = Math.min(totalOffset, (stepOffset + 1) * mStepValue);

        if(valueOffset - lowerValue < higherValue - valueOffset)
            position = lowerValue / (float)totalOffset;
        else
            position = higherValue / (float)totalOffset;

        return position;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        super.onTouchEvent(event);
        getRippleManager().onTouchEvent(this, event);

        if(!isEnabled())
            return false;

        float x = event.getX();
        float y = event.getY();
        if(mIsRtl)
            x = 2 * mDrawRect.centerX() - x;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mIsDragging = isThumbHit(x, y, mThumbTouchRadius > 0 ? mThumbTouchRadius : mThumbRadius) && !mThumbMoveAnimator.isRunning();
                mMemoPoint.set(x, y);
                if(mIsDragging) {
                    mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius);

                    if(getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if(mIsDragging) {
                    if(mDiscreteMode) {
                        float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
                        setPosition(position, true, true, true);
                    }
                    else{
                        float offset = (x - mMemoPoint.x) / mDrawRect.width();
                        float position = Math.min(1f, Math.max(0f, mThumbPosition + offset));
                        setPosition(position, false, true, true);
                        mMemoPoint.x = x;
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if(mIsDragging) {
                    mIsDragging = false;
                    setPosition(getPosition(), true, true, true);

                    if(getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(false);
                }
                else if(distance(mMemoPoint.x, mMemoPoint.y, x, y) <= mTouchSlop){
                    float position = correctPosition(Math.min(1f, Math.max(0f, (x - mDrawRect.left) / mDrawRect.width())));
                    setPosition(position, true, true, true);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if(mIsDragging) {
                    mIsDragging = false;
                    setPosition(getPosition(), true, true, true);

                    if(getParent() != null)
                        getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        return true;
    }

    private void getTrackPath(float x, float y, float radius){
        float halfStroke = mTrackSize / 2f;

        mLeftTrackPath.reset();
        mRightTrackPath.reset();

        if(radius - 1f < halfStroke){
            if(mTrackCap != Paint.Cap.ROUND){
                if(x > mDrawRect.left){
                    mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
                    mLeftTrackPath.lineTo(x, y - halfStroke);
                    mLeftTrackPath.lineTo(x, y + halfStroke);
                    mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
                    mLeftTrackPath.close();
                }

                if(x < mDrawRect.right){
                    mRightTrackPath.moveTo(mDrawRect.right, y + halfStroke);
                    mRightTrackPath.lineTo(x, y + halfStroke);
                    mRightTrackPath.lineTo(x, y - halfStroke);
                    mRightTrackPath.lineTo(mDrawRect.right, y - halfStroke);
                    mRightTrackPath.close();
                }
            }
            else{
                if(x > mDrawRect.left){
                    mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
                    mLeftTrackPath.arcTo(mTempRect, 90, 180);
                    mLeftTrackPath.lineTo(x, y - halfStroke);
                    mLeftTrackPath.lineTo(x, y + halfStroke);
                    mLeftTrackPath.close();
                }

                if(x < mDrawRect.right){
                    mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
                    mRightTrackPath.arcTo(mTempRect, 270, 180);
                    mRightTrackPath.lineTo(x, y + halfStroke);
                    mRightTrackPath.lineTo(x, y - halfStroke);
                    mRightTrackPath.close();
                }
            }
        }
        else{
            if(mTrackCap != Paint.Cap.ROUND){
                mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
                float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);

                if(x - radius > mDrawRect.left){
                    mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke);
                    mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
                    mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke);
                    mLeftTrackPath.close();
                }

                if(x + radius < mDrawRect.right){
                    mRightTrackPath.moveTo(mDrawRect.right, y - halfStroke);
                    mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
                    mRightTrackPath.lineTo(mDrawRect.right, y + halfStroke);
                    mRightTrackPath.close();
                }
            }
            else{
                float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180);

                if(x - radius > mDrawRect.left){
                    float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180);

                    mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke);
                    mLeftTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2);

                    mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
                    mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2);
                    mLeftTrackPath.close();
                }

                if(x + radius < mDrawRect.right){
                    float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke));
                    mRightTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke));

                    angle2 = (float)(angle2 / Math.PI * 180);
                    mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke);
                    mRightTrackPath.arcTo(mTempRect, angle2, -angle2 * 2);

                    mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f);
                    mRightTrackPath.arcTo(mTempRect, -angle, angle * 2);
                    mRightTrackPath.close();
                }
            }
        }
    }

    private Path getMarkPath(Path path, float cx, float cy, float radius, float factor){
        if(path == null)
            path = new Path();
        else
            path.reset();

        float x1 = cx - radius;
        float y1 = cy;
        float x2 = cx + radius;
        float y2 = cy;
        float x3 = cx;
        float y3 = cy + radius;

        float nCx = cx;
        float nCy = cy - radius * factor;

        // calculate first arc
        float angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
        float nRadius = (float)distance(nCx, nCy, x1, y1);
        mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
        path.moveTo(x1, y1);
        path.arcTo(mTempRect, 180 - angle, 180 + angle * 2);

        if(factor > 0.9f)
            path.lineTo(x3, y3);
        else{
            // find center point for second arc
            float x4 = (x2 + x3) / 2;
            float y4 = (y2 + y3) / 2;

            double d1 = distance(x2, y2, x4, y4);
            double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4);

            nCx = (float)(x4 - Math.cos(Math.PI / 4) * d2);
            nCy = (float)(y4 - Math.sin(Math.PI / 4) * d2);

            // calculate second arc
            angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI);
            float angle2 = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
            nRadius = (float)distance(nCx, nCy, x2, y2);
            mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
            path.arcTo(mTempRect, angle, angle2 - angle);

            // calculate third arc
            nCx = cx * 2 - nCx;
            angle = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI);
            angle2 = (float)(Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI);
            mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius);
            path.arcTo(mTempRect, angle + (float)Math.PI / 4, angle2 - angle);
        }

        path.close();

        return path;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        super.draw(canvas);

        float x = mDrawRect.width() * mThumbPosition + mDrawRect.left;
        if(mIsRtl)
            x = 2 * mDrawRect.centerX() - x;
        float y = mDrawRect.centerY();
        int filledPrimaryColor = ColorUtil.getMiddleColor(mSecondaryColor, isEnabled() ? mPrimaryColor : mSecondaryColor, mThumbFillPercent);

        getTrackPath(x, y, mThumbCurrentRadius);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mIsRtl ? filledPrimaryColor : mSecondaryColor);
        canvas.drawPath(mRightTrackPath, mPaint);
        mPaint.setColor(mIsRtl ? mSecondaryColor : filledPrimaryColor);
        canvas.drawPath(mLeftTrackPath, mPaint);

        mPaint.setColor(filledPrimaryColor);
        if(mDiscreteMode){
            float factor = 1f - mThumbCurrentRadius / mThumbRadius;

            if(factor > 0){
                mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor);
                mPaint.setStyle(Paint.Style.FILL);
                int saveCount = canvas.save();
                canvas.translate(0, -mThumbRadius * 2 * factor);
                canvas.drawPath(mMarkPath, mPaint);
                mPaint.setColor(ColorUtil.getColor(mTextColor, factor));
                canvas.drawText(getValueText(), x, y + mTextHeight / 2f - mThumbRadius * factor, mPaint);
                canvas.restoreToCount(saveCount);
            }

            float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
            if(radius > 0) {
                mPaint.setColor(filledPrimaryColor);
                canvas.drawCircle(x, y, radius, mPaint);
            }
        }
        else{
            float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize;
            if(mThumbFillPercent == 1)
                mPaint.setStyle(Paint.Style.FILL);
            else{
                float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize;
                radius = radius - strokeWidth / 2f;
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setStrokeWidth(strokeWidth);
            }
            canvas.drawCircle(x, y, radius, mPaint);
        }
    }

    class ThumbRadiusAnimator implements Runnable{

        boolean mRunning = false;
        long mStartTime;
        float mStartRadius;
        int mRadius;

        public void resetAnimation(){
            mStartTime = SystemClock.uptimeMillis();
            mStartRadius = mThumbCurrentRadius;
        }

        public boolean startAnimation(int radius) {
            if(mThumbCurrentRadius == radius)
                return false;

            mRadius = radius;

            if(getHandler() != null){
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            }
            else {
                mThumbCurrentRadius = mRadius;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbCurrentRadius = mRadius;
            if(getHandler() != null)
                getHandler().removeCallbacks(this);
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
            float value = mInterpolator.getInterpolation(progress);

            mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius;

            if(progress == 1f)
                stopAnimation();

            if(mRunning) {
                if(getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }

    class ThumbStrokeAnimator implements Runnable{

        boolean mRunning = false;
        long mStartTime;
        float mStartFillPercent;
        int mFillPercent;

        public void resetAnimation(){
            mStartTime = SystemClock.uptimeMillis();
            mStartFillPercent = mThumbFillPercent;
        }

        public boolean startAnimation(int fillPercent) {
            if(mThumbFillPercent == fillPercent)
                return false;

            mFillPercent = fillPercent;

            if(getHandler() != null){
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            }
            else {
                mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
            if(getHandler() != null)
                getHandler().removeCallbacks(this);
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration);
            float value = mInterpolator.getInterpolation(progress);

            mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);

            if(progress == 1f)
                stopAnimation();

            if(mRunning) {
                if(getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }

    class ThumbMoveAnimator implements Runnable{

        boolean mRunning = false;
        long mStartTime;
        float mStartFillPercent;
        float mStartRadius;
        float mStartPosition;
        float mPosition;
        float mFillPercent;
        int mDuration;

        public boolean isRunning(){
            return mRunning;
        }

        public float getPosition(){
            return mPosition;
        }

        public void resetAnimation(){
            mStartTime = SystemClock.uptimeMillis();
            mStartPosition = mThumbPosition;
            mStartFillPercent = mThumbFillPercent;
            mStartRadius = mThumbCurrentRadius;
            mFillPercent = mPosition == 0 ? 0 : 1;
            mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration;
        }

        public boolean startAnimation(float position) {
            if(mThumbPosition == position)
                return false;

            mPosition = position;

            if(getHandler() != null){
                resetAnimation();
                mRunning = true;
                getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                invalidate();
                return true;
            }
            else {
                mThumbPosition = position;
                invalidate();
                return false;
            }
        }

        public void stopAnimation() {
            mRunning = false;
            mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius;
            mThumbFillPercent = mAlwaysFillThumb ? 1 : mFillPercent;
            mThumbPosition = mPosition;
            if(getHandler() != null)
                getHandler().removeCallbacks(this);
            invalidate();
        }

        @Override
        public void run() {
            long curTime = SystemClock.uptimeMillis();
            float progress = Math.min(1f, (float)(curTime - mStartTime) / mDuration);
            float value = mInterpolator.getInterpolation(progress);

            if(mDiscreteMode){
                if(mIsDragging) {
                    mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                    mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
                }
                else{
                    float p1 = (float)mTravelAnimationDuration / mDuration;
                    float p2 = (float)(mTravelAnimationDuration + mTransformAnimationDuration)/ mDuration;
                    if(progress < p1) {
                        value = mInterpolator.getInterpolation(progress / p1);
                        mThumbCurrentRadius = mStartRadius * (1f - value);
                        mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                        mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);
                    }
                    else if(progress > p2){
                        mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2);
                    }
                }
            }
            else{
                mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition;
                mThumbFillPercent = mAlwaysFillThumb ? 1 : ((mFillPercent - mStartFillPercent) * value + mStartFillPercent);

                if(progress < 0.2)
                    mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius);
                else if(progress >= 0.8)
                    mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5);
            }


            if(progress == 1f)
                stopAnimation();

            if(mRunning) {
                if(getHandler() != null)
                    getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
                else
                    stopAnimation();
            }

            invalidate();
        }

    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);

        ss.position = getPosition();
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;

        super.onRestoreInstanceState(ss.getSuperState());
        setPosition(ss.position, false);
        requestLayout();
    }

    static class SavedState extends BaseSavedState {
        float position;

        /**
         * Constructor called from {@link Slider#onSaveInstanceState()}
         */
        SavedState(Parcelable superState) {
            super(superState);
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in) {
            super(in);
            position = in.readFloat();
        }

        @Override
        public void writeToParcel(@NonNull Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeFloat(position);
        }

        @Override
        public String toString() {
            return "Slider.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " pos=" + position + "}";
        }

        public static final Creator<SavedState> CREATOR
                = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

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