/*
 * Loop 1.2.1 created by Issei Aoki modified by Jeremy JAMET
 * https://github.com/igreenwood/loupe
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2020 Issei Aoki
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
package com.igreenwood.loupe

import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.TypedValue
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.widget.ImageView
import android.widget.OverScroller
import androidx.core.view.ViewCompat
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt


class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
    View.OnLayoutChangeListener {

    companion object {
        const val DEFAULT_MAX_ZOOM = 5.0f
        const val DEFAULT_ANIM_DURATION = 250L
        const val DEFAULT_ANIM_DURATION_LONG = 375L
        const val DEFAULT_VIEW_DRAG_FRICTION = 1f
        const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO = 0.5f
        const val DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP = 96
        const val MAX_FLING_VELOCITY = 8000f
        const val MIN_FLING_VELOCITY = 1500f
        const val DEFAULT_DOUBLE_TAP_ZOOM_SCALE = 0.5f
        val DEFAULT_INTERPOLATOR = DecelerateInterpolator()

        fun create(
                imageView: ImageView,
                container: ViewGroup,
                config: Loupe.() -> Unit = { }
        ): Loupe {
            return Loupe(imageView, container).apply(config)
        }
    }

    interface OnViewTranslateListener {
        fun onStart(view: ImageView)
        fun onViewTranslate(view: ImageView, amount: Float)
        fun onDismiss(view: ImageView)
        fun onRestore(view: ImageView)
    }

    interface OnScaleChangedListener {
        fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float)
    }

    // max zoom(> 1f)
    var maxZoom = DEFAULT_MAX_ZOOM
    // use fling gesture for dismiss
    var useFlingToDismissGesture = true
    // flag to enable or disable drag to dismiss
    var useDragToDismiss = true
    // duration millis for dismiss animation
    var dismissAnimationDuration = DEFAULT_ANIM_DURATION
    // duration millis for restore animation
    var restoreAnimationDuration = DEFAULT_ANIM_DURATION
    // duration millis for image animation
    var flingAnimationDuration = DEFAULT_ANIM_DURATION
    // duration millis for double tap scale animation
    var scaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG
    // duration millis for over scale animation
    var overScaleAnimationDuration = DEFAULT_ANIM_DURATION_LONG
    // duration millis for over scrolling animation
    var overScrollAnimationDuration = DEFAULT_ANIM_DURATION
    // view drag friction for swipe to dismiss(1f : drag distance == view move distance. Smaller value, view is moving more slower)
    var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
    // drag distance threshold in dp for swipe to dismiss
    var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
    // on view touched
    var onViewTouchedListener: View.OnTouchListener? = null
    // on view translate listener
    var onViewTranslateListener: OnViewTranslateListener? = null
    // on scale changed
    var onScaleChangedListener: OnScaleChangedListener? = null

    var dismissAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR

    var restoreAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR

    var flingAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR

    var doubleTapScaleAnimationInterpolator: Interpolator = AccelerateDecelerateInterpolator()

    var overScaleAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR

    var overScrollAnimationInterpolator: Interpolator = DEFAULT_INTERPOLATOR

    var doubleTapZoomScale: Float = DEFAULT_DOUBLE_TAP_ZOOM_SCALE // 0f~1f

    var minimumFlingVelocity: Float = MIN_FLING_VELOCITY

    private var flingAnimator: Animator = ValueAnimator()

    // bitmap matrix
    private var transfrom = Matrix()
    // bitmap scale
    private var scale = 1f
    // is ready for drawing bitmap
    private var isReadyToDraw = false
    // view rect - padding (recalculated on size changed)
    private var canvasBounds = RectF()
    // bitmap drawing rect (move on scroll, recalculated on scale changed)
    private var bitmapBounds = RectF()
    // displaying bitmap rect (does not move, recalculated on scale changed)
    private var viewport = RectF()
    // minimum scale of bitmap
    private var minScale = 1f
    // maximum scale of bitmap
    private var maxScale = 1f
    // bitmap (decoded) width
    private var imageWidth = 0f
    // bitmap (decoded) height
    private var imageHeight = 0f

    private val scroller: OverScroller
    private var originalViewBounds = Rect()
    private var dragToDismissThreshold = 0f
    private var dragDismissDistanceInPx = 0f
    private var isViewTranslateAnimationRunning = false
    private var isVerticalScrollEnabled = true
    private var isHorizontalScrollEnabled = true
    private var isBitmapTranslateAnimationRunning = false
    private var isBitmapScaleAnimationRunninng = false
    private var initialY = 0f
    // scaling helper
    private var scaleGestureDetector: ScaleGestureDetector? = null
    // translating helper
    private var gestureDetector: GestureDetector? = null
    private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
        object : ScaleGestureDetector.OnScaleGestureListener {

            override fun onScale(detector: ScaleGestureDetector): Boolean {
                if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
                    return false
                }

                val scaleFactor = detector.scaleFactor
                val focalX = detector.focusX
                val focalY = detector.focusY

                if (detector.scaleFactor == 1.0f) {
                    // scale is not changing
                    return true
                }

                zoomToTargetScale(calcNewScale(scaleFactor), focalX, focalY)

                return true
            }

            override fun onScaleBegin(p0: ScaleGestureDetector): Boolean = true

            override fun onScaleEnd(p0: ScaleGestureDetector) {}

        }

    private val onGestureListener: GestureDetector.OnGestureListener =
        object : GestureDetector.SimpleOnGestureListener() {
            override fun onDown(e: MotionEvent): Boolean = true

            override fun onScroll(
                e1: MotionEvent?,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                if (e2.pointerCount != 1) {
                    return true
                }

                if (scale > minScale) {
                    processScroll(distanceX, distanceY)
                } else if (useDragToDismiss && scale == minScale) {
                    processDrag(distanceY)
                }
                return true
            }

            override fun onFling(
                e1: MotionEvent?,
                e2: MotionEvent,
                velocityX: Float,
                velocityY: Float
            ): Boolean {
                if (scale > minScale) {
                    processFlingBitmap(velocityX, velocityY)
                } else {
                    processFlingToDismiss(velocityY)
                }
                return true
            }

            override fun onDoubleTap(e: MotionEvent): Boolean {
                if (isBitmapScaleAnimationRunninng) {
                    return true
                }

                if (scale > minScale) {
                    zoomOutToMinimumScale()
                } else {
                    zoomInToTargetScale(e)
                }
                return true
            }
        }

    init {
        container.apply {
            background.alpha = 255
            setOnTouchListener(this@Loupe)
            addOnLayoutChangeListener(this@Loupe)
            scaleGestureDetector = ScaleGestureDetector(context, onScaleGestureListener)
            gestureDetector = GestureDetector(context, onGestureListener)
            scroller = OverScroller(context)
            dragDismissDistanceInPx = TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_DIP,
                    dragDismissDistanceInDp.toFloat(),
                    resources.displayMetrics
            )
        }
        imageView.apply {
            imageMatrix = null
            y = 0f
            animate().cancel()
            scaleType = ImageView.ScaleType.MATRIX
        }
    }

    private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
    private var containerRef: WeakReference<ViewGroup> = WeakReference(container)

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouch(view: View?, event: MotionEvent?): Boolean {
        onViewTouchedListener?.onTouch(view, event)

        event ?: return false
        val imageView = imageViewRef.get() ?: return false
        val container = containerRef.get() ?: return false

        container.parent.requestDisallowInterceptTouchEvent(scale != minScale)

        if (!imageView.isEnabled) {
            return false
        }

        if (isViewTranslateAnimationRunning) {
            return false
        }

        val scaleEvent = scaleGestureDetector?.onTouchEvent(event)
        val isScaleAnimationIsRunning = scale < minScale
        if (scaleEvent != scaleGestureDetector?.isInProgress && !isScaleAnimationIsRunning) {
            // handle single touch gesture when scaling process is not running
            gestureDetector?.onTouchEvent(event)
        }
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                flingAnimator.cancel()
            }
            MotionEvent.ACTION_UP -> {
                when {
                    scale == minScale -> {
                        if (!isViewTranslateAnimationRunning) {
                            dismissOrRestoreIfNeeded()
                        }
                    }
                    scale > minScale -> {
                        constrainBitmapBounds(true)
                    }
                    else -> {
                        zoomOutToMinimumScale(true)
                    }
                }
            }
        }

        setTransform()
        imageView.postInvalidate()
        return true
    }

    override fun onLayoutChange(
            view: View?,
            left: Int,
            top: Int,
            right: Int,
            bottom: Int,
            oldLeft: Int,
            oldTop: Int,
            oldRight: Int,
            oldBottom: Int
    ) {
        val imageView = imageViewRef.get() ?: return
        val container = containerRef.get() ?: return

        imageView.run {
            setupLayout(left, top, right, bottom)
            initialY = y
            if (useFlingToDismissGesture) {
                setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_VIEW_HEIGHT_RATIO)
            } else {
                setDragToDismissDistance(DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP)
            }
            container.background.alpha = 255
            setTransform()
            postInvalidate()
        }
    }

    private fun startVerticalTranslateAnimation(velY: Float) {
        val imageView = imageViewRef.get() ?: return

        isViewTranslateAnimationRunning = true
        
        imageView.run {
            val translationY = if (velY > 0) {
                originalViewBounds.top + height - top
            } else {
                originalViewBounds.top - height - top
            }
            animate()
                    .setDuration(dismissAnimationDuration)
                    .setInterpolator(dismissAnimationInterpolator)
                    .translationY(translationY.toFloat())
                    .setUpdateListener {
                        val amount = calcTranslationAmount()
                        changeBackgroundAlpha(amount)
                        onViewTranslateListener?.onViewTranslate(imageView, amount)
                    }
                    .setListener(object : Animator.AnimatorListener {
                        override fun onAnimationStart(p0: Animator) {

                        }

                        override fun onAnimationEnd(p0: Animator) {
                            isViewTranslateAnimationRunning = false
                            onViewTranslateListener?.onDismiss(imageView)
                            cleanup()
                        }

                        override fun onAnimationCancel(p0: Animator) {
                            isViewTranslateAnimationRunning = false
                        }

                        override fun onAnimationRepeat(p0: Animator) {
                            // no op
                        }
                    })
        }
    }

    private fun processFlingBitmap(velocityX: Float, velocityY: Float) {
        val imageView = imageViewRef.get() ?: return

        var (velX, velY) = velocityX / scale to velocityY / scale

        if (velX == 0f && velY == 0f) {
            return
        }

        if (velX > MAX_FLING_VELOCITY) {
            velX = MAX_FLING_VELOCITY
        }

        if (velY > MAX_FLING_VELOCITY) {
            velY = MAX_FLING_VELOCITY
        }

        val (fromX, fromY) = bitmapBounds.left to bitmapBounds.top

        scroller.forceFinished(true)
        scroller.fling(
                fromX.roundToInt(),
                fromY.roundToInt(),
                velX.roundToInt(),
                velY.roundToInt(),
                (viewport.right - bitmapBounds.width()).roundToInt(),
                viewport.left.roundToInt(),
                (viewport.bottom - bitmapBounds.height()).roundToInt(),
                viewport.top.roundToInt()
        )

        ViewCompat.postInvalidateOnAnimation(imageView)

        val toX = scroller.finalX.toFloat()
        val toY = scroller.finalY.toFloat()

        flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
            duration = flingAnimationDuration
            interpolator = flingAnimationInterpolator
            addUpdateListener {
                val amount = it.animatedValue as Float
                val newLeft = lerp(amount, fromX, toX)
                val newTop = lerp(amount, fromY, toY)
                bitmapBounds.offsetTo(newLeft, newTop)
                ViewCompat.postInvalidateOnAnimation(imageView)
                setTransform()
            }
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(p0: Animator) {
                    isBitmapTranslateAnimationRunning = true
                }

                override fun onAnimationEnd(p0: Animator) {
                    isBitmapTranslateAnimationRunning = false
                    constrainBitmapBounds()
                }

                override fun onAnimationCancel(p0: Animator) {
                    isBitmapTranslateAnimationRunning = false
                }

                override fun onAnimationRepeat(p0: Animator) {
                    // no op
                }
            })
        }
        flingAnimator.start()
    }

    private fun processScroll(distanceX: Float, distanceY: Float) {
        val distX = if (isHorizontalScrollEnabled) {
            -distanceX
        } else {
            0f
        }
        val distY = if (isVerticalScrollEnabled) {
            -distanceY
        } else {
            0f
        }
        offsetBitmap(distX, distY)
        setTransform()
    }

    private fun zoomInToTargetScale(e: MotionEvent) {
        val imageView = imageViewRef.get() ?: return
        val startScale = scale
        val endScale = minScale * maxZoom * doubleTapZoomScale
        val focalX = e.x
        val focalY = e.y
        ValueAnimator.ofFloat(startScale, endScale).apply {
            duration = scaleAnimationDuration
            interpolator = doubleTapScaleAnimationInterpolator
            addUpdateListener {
                zoomToTargetScale(it.animatedValue as Float, focalX, focalY)
                ViewCompat.postInvalidateOnAnimation(imageView)
                setTransform()
            }
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(p0: Animator) {
                    isBitmapScaleAnimationRunninng = true
                }

                override fun onAnimationEnd(p0: Animator) {
                    isBitmapScaleAnimationRunninng = false
                    if (endScale == minScale) {
                        zoomToTargetScale(minScale, focalX, focalY)
                        imageView.postInvalidate()
                    }
                }

                override fun onAnimationCancel(p0: Animator) {
                    isBitmapScaleAnimationRunninng = false
                }

                override fun onAnimationRepeat(p0: Animator) {
                    // no op
                }
            })
        }.start()
    }

    private fun zoomOutToMinimumScale(isOverScaling: Boolean = false) {
        val imageView = imageViewRef.get() ?: return
        val startScale = scale
        val endScale = minScale
        val startLeft = bitmapBounds.left
        val startTop = bitmapBounds.top
        val endLeft = canvasBounds.centerX() - imageWidth * minScale * 0.5f
        val endTop = canvasBounds.centerY() - imageHeight * minScale * 0.5f
        ValueAnimator.ofFloat(0f, 1f).apply {
            duration = if (isOverScaling) {
                overScaleAnimationDuration
            } else {
                scaleAnimationDuration
            }
            interpolator = if (isOverScaling) {
                overScaleAnimationInterpolator
            } else {
                doubleTapScaleAnimationInterpolator
            }
            addUpdateListener {
                val value = it.animatedValue as Float
                scale = lerp(value, startScale, endScale)
                val newLeft = lerp(value, startLeft, endLeft)
                val newTop = lerp(value, startTop, endTop)
                calcBounds()
                bitmapBounds.offsetTo(newLeft, newTop)
                constrainBitmapBounds()
                ViewCompat.postInvalidateOnAnimation(imageView)
                setTransform()
            }
            addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(p0: Animator) {
                    isBitmapScaleAnimationRunninng = true
                }

                override fun onAnimationEnd(p0: Animator) {
                    isBitmapScaleAnimationRunninng = false
                    if (endScale == minScale) {
                        scale = minScale
                        calcBounds()
                        constrainBitmapBounds()
                        imageView.postInvalidate()
                    }
                }

                override fun onAnimationCancel(p0: Animator) {
                    isBitmapScaleAnimationRunninng = false
                }

                override fun onAnimationRepeat(p0: Animator) {
                    // no op
                }
            })
        }.start()
    }

    private var lastDistY = Float.NaN

    private fun processDrag(distanceY: Float) {
        val imageView = imageViewRef.get() ?: return

        if (lastDistY.isNaN()) {
            lastDistY = distanceY
            return
        }

        if (imageView.y == initialY) {
            onViewTranslateListener?.onStart(imageView)
        }

        imageView.y -= distanceY * viewDragFriction // if viewDragRatio is 1.0f, view translation speed is equal to user scrolling speed.
        val amount = calcTranslationAmount()
        changeBackgroundAlpha(amount)
        onViewTranslateListener?.onViewTranslate(imageView, amount)
    }

    private fun dismissOrRestoreIfNeeded() {
        if (!isDragging() || isViewTranslateAnimationRunning) {
            return
        }
        dismissOrRestore()
    }

    private fun dismissOrRestore() {
        val imageView = imageViewRef.get() ?: return

        if (shouldTriggerDragToDismissAnimation()) {
            if (useFlingToDismissGesture) {
                startDragToDismissAnimation()
            } else {
                onViewTranslateListener?.onDismiss(imageView)
                cleanup()
            }
        } else {
            restoreViewTransform()
        }
    }

    private fun shouldTriggerDragToDismissAnimation() = dragDistance() > dragToDismissThreshold

    private fun restoreViewTransform() {
        val imageView = imageViewRef.get() ?: return
        imageView.run {
            animate()
                    .setDuration(restoreAnimationDuration)
                    .setInterpolator(restoreAnimationInterpolator)
                    .translationY((originalViewBounds.top - top).toFloat())
                    .setUpdateListener {
                        val amount = calcTranslationAmount()
                        changeBackgroundAlpha(amount)
                        onViewTranslateListener?.onViewTranslate(this, amount)
                    }
                    .setListener(object : Animator.AnimatorListener {
                        override fun onAnimationStart(p0: Animator) {
                            // no op
                        }

                        override fun onAnimationEnd(p0: Animator) {
                            onViewTranslateListener?.onRestore(imageView)
                        }

                        override fun onAnimationCancel(p0: Animator) {
                            // no op
                        }

                        override fun onAnimationRepeat(p0: Animator) {
                            // no op
                        }
                    })
        }
        
    }

    private fun startDragToDismissAnimation() {
        val imageView = imageViewRef.get() ?: return
        
        imageView.run {
            val translationY = if (y - initialY > 0) {
                originalViewBounds.top + height - top
            } else {
                originalViewBounds.top - height - top
            }
            animate()
                    .setDuration(dismissAnimationDuration)
                    .setInterpolator(AccelerateDecelerateInterpolator())
                    .translationY(translationY.toFloat())
                    .setUpdateListener {
                        val amount = calcTranslationAmount()
                        changeBackgroundAlpha(amount)
                        onViewTranslateListener?.onViewTranslate(this, amount)
                    }
                    .setListener(object : Animator.AnimatorListener {
                        override fun onAnimationStart(p0: Animator) {
                            isViewTranslateAnimationRunning = true
                        }

                        override fun onAnimationEnd(p0: Animator) {
                            isViewTranslateAnimationRunning = false
                            onViewTranslateListener?.onDismiss(imageView)
                            cleanup()
                        }

                        override fun onAnimationCancel(p0: Animator) {
                            isViewTranslateAnimationRunning = false
                        }

                        override fun onAnimationRepeat(p0: Animator) {
                            // no op
                        }
                    })
        }
        
    }

    private fun processFlingToDismiss(velocityY: Float) {
        if (useFlingToDismissGesture && !isViewTranslateAnimationRunning) {
            if (abs(velocityY) < minimumFlingVelocity) {
                return
            }
            startVerticalTranslateAnimation(velocityY)
        }
    }

    private fun calcTranslationAmount() =
        constrain(
                0f,
                norm(dragDistance(), 0f, originalViewBounds.height().toFloat()),
                1f
        )

    private fun dragDistance() = abs(viewOffsetY())

    private fun isDragging() = dragDistance() > 0f

    private fun viewOffsetY() = imageViewRef.get()?.y ?: 0 - initialY

    /**
     * targetScale: new scale
     * focalX: focal x in current bitmapBounds
     * focalY: focal y in current bitmapBounds
     */
    private fun zoomToTargetScale(targetScale: Float, focalX: Float, focalY: Float) {
        scale = targetScale
        val lastBounds = RectF(bitmapBounds)
        // scale has changed, recalculate bitmap bounds
        calcBounds()
        // offset to focalPoint
        offsetToZoomFocalPoint(focalX, focalY, lastBounds, bitmapBounds)
        onScaleChangedListener?.onScaleChange(targetScale, focalX, focalY)
    }

    private fun setTransform() {
        val imageView = imageViewRef.get() ?: return
        transfrom.apply {
            reset()
            postTranslate(-imageWidth / 2, -imageHeight / 2)
            postScale(scale, scale)
            postTranslate(bitmapBounds.centerX(), bitmapBounds.centerY())
        }
        imageView.imageMatrix = transfrom
    }

    /**
     * setup layout
     */
    private fun setupLayout(left: Int, top: Int, right: Int, bottom: Int) {
        val imageView = imageViewRef.get() ?: return
        originalViewBounds.set(left, top, right, bottom)
        imageView.run {
            val drawable = imageViewRef.get()?.drawable
            val bitmap = (drawable as? BitmapDrawable)?.bitmap
            if (width == 0 || height == 0 || drawable == null) return
            imageWidth = (bitmap?.width ?: drawable.intrinsicWidth).toFloat()
            imageHeight = (bitmap?.height ?: drawable.intrinsicHeight).toFloat()
            val canvasWidth = (width - paddingLeft - paddingRight).toFloat()
            val canvasHeight = (height - paddingTop - paddingBottom).toFloat()

            calcScaleRange(canvasWidth, canvasHeight, imageWidth, imageHeight)
            calcBounds()
            constrainBitmapBounds()
            isReadyToDraw = true
            invalidate()
        }
    }

    private fun constrainBitmapBounds(animate: Boolean = false) {
        val imageView = imageViewRef.get() ?: return

        if (isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
            return
        }

        val offset = PointF()

        // constrain viewport inside bitmap bounds
        if (viewport.left < bitmapBounds.left) {
            offset.x += viewport.left - bitmapBounds.left
        }

        if (viewport.top < bitmapBounds.top) {
            offset.y += viewport.top - bitmapBounds.top
        }

        if (viewport.right > bitmapBounds.right) {
            offset.x += viewport.right - bitmapBounds.right
        }

        if (viewport.bottom > bitmapBounds.bottom) {
            offset.y += viewport.bottom - bitmapBounds.bottom
        }

        if (offset.equals(0f, 0f)) {
            return
        }

        if (animate) {
            if (!isVerticalScrollEnabled) {
                bitmapBounds.offset(0f, offset.y)
                offset.y = 0f
            }

            if (!isHorizontalScrollEnabled) {
                bitmapBounds.offset(offset.x, 0f)
                offset.x = 0f
            }

            val start = RectF(bitmapBounds)
            val end = RectF(bitmapBounds).apply {
                offset(offset.x, offset.y)
            }
            ValueAnimator.ofFloat(0f, 1f).apply {
                duration = overScrollAnimationDuration
                interpolator = overScrollAnimationInterpolator
                addUpdateListener {
                    val amount = it.animatedValue as Float
                    val newLeft = lerp(amount, start.left, end.left)
                    val newTop = lerp(amount, start.top, end.top)
                    bitmapBounds.offsetTo(newLeft, newTop)
                    ViewCompat.postInvalidateOnAnimation(imageView)
                    setTransform()
                }
            }.start()
        } else {
            bitmapBounds.offset(offset.x, offset.y)
        }
    }

    /**
     * calc canvas/bitmap bounds
     */
    private fun calcBounds() {
        val imageView = imageViewRef.get() ?: return

        imageView.run {
            // calc canvas bounds
            canvasBounds = RectF(
                    paddingLeft.toFloat(),
                    paddingTop.toFloat(),
                    width - paddingRight.toFloat(),
                    height - paddingBottom.toFloat()
            )
        }
        // calc bitmap bounds
        bitmapBounds = RectF(
                canvasBounds.centerX() - imageWidth * scale * 0.5f,
                canvasBounds.centerY() - imageHeight * scale * 0.5f,
                canvasBounds.centerX() + imageWidth * scale * 0.5f,
                canvasBounds.centerY() + imageHeight * scale * 0.5f
        )
        // calc viewport
        viewport = RectF(
                max(canvasBounds.left, bitmapBounds.left),
                max(canvasBounds.top, bitmapBounds.top),
                min(canvasBounds.right, bitmapBounds.right),
                min(canvasBounds.bottom, bitmapBounds.bottom)
        )
        // check scroll availability
        isHorizontalScrollEnabled = true
        isVerticalScrollEnabled = true

        if (bitmapBounds.width() < canvasBounds.width()) {
            isHorizontalScrollEnabled = false
        }

        if (bitmapBounds.height() < canvasBounds.height()) {
            isVerticalScrollEnabled = false
        }
    }

    private fun offsetBitmap(offsetX: Float, offsetY: Float) {
        bitmapBounds.offset(offsetX, offsetY)
    }

    /**
     * calc min/max scale and set initial scale
     */
    private fun calcScaleRange(
            canvasWidth: Float,
            canvasHeight: Float,
            bitmapWidth: Float,
            bitmapHeight: Float
    ) {
        val canvasRatio = canvasHeight / canvasWidth
        val bitmapRatio = bitmapHeight / bitmapWidth
        minScale = if (canvasRatio > bitmapRatio) {
            canvasWidth / bitmapWidth
        } else {
            canvasHeight / bitmapHeight
        }
        scale = minScale
        maxScale = minScale * maxZoom
    }


    private fun calcNewScale(newScale: Float): Float {
        return min(maxScale, newScale * scale)
    }

    private fun constrain(min: Float, value: Float, max: Float): Float {
        return max(min(value, max), min)
    }

    private fun offsetToZoomFocalPoint(
            focalX: Float,
            focalY: Float,
            oldBounds: RectF,
            newBounds: RectF
    ) {
        val oldX = constrain(viewport.left, focalX, viewport.right)
        val oldY = constrain(viewport.top, focalY, viewport.bottom)
        val newX = map(oldX, oldBounds.left, oldBounds.right, newBounds.left, newBounds.right)
        val newY = map(oldY, oldBounds.top, oldBounds.bottom, newBounds.top, newBounds.bottom)
        offsetBitmap(oldX - newX, oldY - newY)
    }

    private fun map(
            value: Float,
            srcStart: Float,
            srcStop: Float,
            dstStart: Float,
            dstStop: Float
    ): Float {
        if (srcStop - srcStart == 0f) {
            return 0f
        }
        return ((value - srcStart) * (dstStop - dstStart) / (srcStop - srcStart)) + dstStart
    }

    private fun lerp(amt: Float, start: Float, stop: Float): Float {
        return start + (stop - start) * amt
    }

    private fun norm(value: Float, start: Float, stop: Float): Float {
        return value / (stop - start)
    }

    private fun changeBackgroundAlpha(amount: Float) {
        val container = containerRef.get() ?: return
        val newAlpha = ((1.0f - amount) * 255).roundToInt()
        container.background.mutate().alpha = newAlpha
    }

    fun setDragToDismissDistance(distance: Int) {
        val imageView = imageViewRef.get() ?: return
        dragToDismissThreshold = TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                distance.toFloat(),
                imageView.context.resources.displayMetrics
        )
    }

    fun setDragToDismissDistance(heightRatio: Float) {
        val imageView = imageViewRef.get() ?: return
        dragToDismissThreshold = imageView.height * heightRatio
    }

    fun dismiss() {
        // Animate down offscreen (the finish listener will call the cleanup method)
        startVerticalTranslateAnimation(MIN_FLING_VELOCITY)
    }

    fun cleanup() {
        containerRef.get()?.apply {
            setOnTouchListener(null)
            removeOnLayoutChangeListener(null)
        }
        imageViewRef.clear()
        containerRef.clear()
    }
}
