import UIKit

// MARK: -
// MARK: DGElasticPullToRefreshState


enum PullToRefreshState: Int {
	case stopped
	case dragging
	case animatingBounce
	case loading
	case animatingToStopped

	func isAnyOf(_ values: [PullToRefreshState]) -> Bool {
		return values.contains(where: { $0 == self })
	}
}

struct PullRefreshConstants {

	struct KeyPaths {
		static let ContentOffset = "contentOffset"
		static let ContentInset = "contentInset"
		static let Frame = "frame"
		static let PanGestureRecognizerState = "panGestureRecognizer.state"
	}

	public static var WaveMaxHeight: CGFloat = 70.0
	public static var MinOffsetToPull: CGFloat = 95.0
	public static var LoadingContentInset: CGFloat = 50.0
	public static var LoadingViewSize: CGFloat = 30.0

}

// MARK: -
// MARK: DGElasticPullToRefreshView

open class PullToRefreshView: UIView {

	// MARK: -
	// MARK: Vars

	fileprivate var _state: PullToRefreshState = .stopped
	fileprivate(set) var state: PullToRefreshState {
		get {
			return _state
		}
		set {
			let previousValue = state
			_state = newValue

			if previousValue == .dragging && newValue == .animatingBounce {
				loadingView?.startAnimating()
				animateBounce()
			} else if newValue == .loading && actionHandler != nil {
				actionHandler()
			} else if newValue == .animatingToStopped {
				resetScrollViewContentInset(shouldAddObserverWhenFinished: true, animated: true) {
					[weak self] in
					self?.state = .stopped
				}

			} else if newValue == .stopped {
				loadingView?.stopLoading()
			}
		}
	}

	fileprivate var originalContentInsetTop: CGFloat = 0.0 {
		didSet {
			layoutSubviews()
		}
	}
	fileprivate let shapeLayer = CAShapeLayer()

	fileprivate var displayLink: CADisplayLink!

	var actionHandler: BlockVoid!

	var loadingView: PullToRefreshLoadingView? {
		willSet {
			loadingView?.removeFromSuperview()
			if let newValue = newValue {
				addSubview(newValue)
			}
		}
	}

	var observing: Bool = false {
		didSet {
			if observing {
				pullAddObserver(PullRefreshConstants.KeyPaths.ContentOffset)
				pullAddObserver(PullRefreshConstants.KeyPaths.ContentInset)
				pullAddObserver(PullRefreshConstants.KeyPaths.Frame)
				pullAddObserver(PullRefreshConstants.KeyPaths.PanGestureRecognizerState)
			} else {
				pullRemoveObserver(PullRefreshConstants.KeyPaths.ContentOffset)
				pullRemoveObserver(PullRefreshConstants.KeyPaths.ContentInset)
				pullRemoveObserver(PullRefreshConstants.KeyPaths.Frame)
				pullRemoveObserver(PullRefreshConstants.KeyPaths.PanGestureRecognizerState)
			}
		}
	}

	var fillColor: UIColor = .clear {
		didSet {
			shapeLayer.fillColor = fillColor.cgColor
		}
	}

	// MARK: Views

	fileprivate let bounceAnimationHelperView = UIView()

	fileprivate let cControlPointView = UIView()
	fileprivate let l1ControlPointView = UIView()
	fileprivate let l2ControlPointView = UIView()
	fileprivate let l3ControlPointView = UIView()
	fileprivate let r1ControlPointView = UIView()
	fileprivate let r2ControlPointView = UIView()
	fileprivate let r3ControlPointView = UIView()

	// MARK: -
	// MARK: Constructors

	init() {
		super.init(frame: CGRect.zero)

		displayLink = CADisplayLink(target: self, selector: #selector(PullToRefreshView.displayLinkTick))
		displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common)
		displayLink.isPaused = true

		shapeLayer.backgroundColor = UIColor.clear.cgColor
		shapeLayer.fillColor = UIColor.black.cgColor
		shapeLayer.actions = ["path": NSNull(), "position": NSNull(), "bounds": NSNull()]
		layer.addSublayer(shapeLayer)

		addSubview(bounceAnimationHelperView)
		addSubview(cControlPointView)
		addSubview(l1ControlPointView)
		addSubview(l2ControlPointView)
		addSubview(l3ControlPointView)
		addSubview(r1ControlPointView)
		addSubview(r2ControlPointView)
		addSubview(r3ControlPointView)

		NotificationCenter.default.addObserver(self, selector: #selector(PullToRefreshView.applicationWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
	}

	required public init?(coder aDecoder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}

	deinit {
		observing = false
		NotificationCenter.default.removeObserver(self)
		logd("PullToRefreshView deinit")
	}

	@objc
	func applicationWillEnterForeground() {
		if state == .loading {
			layoutSubviews()
		}
	}

	func disassociateDisplayLink() {
		displayLink?.invalidate()
	}


	private var pathSet: Set<String> = []

	private func pullAddObserver(_ keyPath: String) {
		if !pathSet.contains(keyPath) {
			pathSet.insert(keyPath)
			if let sc = self.scrollView {
				sc.addObserver(self, forKeyPath: keyPath, options: .new, context: nil)
			}
		}
	}

	private func pullRemoveObserver(_  keyPath: String) {
		if pathSet.contains(keyPath) {
			pathSet.remove(keyPath)
			if let sc = self.scrollView {
				sc.removeObserver(self, forKeyPath: keyPath)
			}
		}
	}

	// MARK: -
	// MARK: Observer

	override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
		if keyPath == PullRefreshConstants.KeyPaths.ContentOffset {
			if let newContentOffset = change?[NSKeyValueChangeKey.newKey], let sc = scrollView {
				let newContentOffsetY = (newContentOffset as AnyObject).cgPointValue.y
				if state.isAnyOf([.loading, .animatingToStopped]) && newContentOffsetY < -sc.contentInset.top {
					sc.contentOffset.y = -sc.contentInset.top
				} else {
					scrollViewDidChangeContentOffset(dragging: sc.isDragging)
				}
				layoutSubviews()
			}
		} else if keyPath == PullRefreshConstants.KeyPaths.ContentInset {
			if let newContentInset = change?[NSKeyValueChangeKey.newKey] {
				let newContentInsetTop = (newContentInset as AnyObject).uiEdgeInsetsValue.top
				originalContentInsetTop = newContentInsetTop
			}
		} else if keyPath == PullRefreshConstants.KeyPaths.Frame {
			layoutSubviews()
		} else if keyPath == PullRefreshConstants.KeyPaths.PanGestureRecognizerState {
			if let gestureState = scrollView?.panGestureRecognizer.state, gestureState.isAnyOf([.ended, .cancelled, .failed]) {
				scrollViewDidChangeContentOffset(dragging: false)
			}
		}
	}

	// MARK: -
	// MARK: Notifications


	// MARK: -
	// MARK: Methods (Public)

	fileprivate var scrollView: UIScrollView? {
		return superview as? UIScrollView
	}

	public func stopLoading() {
		// Prevent stop close animation
		if state == .animatingToStopped {
			return
		}
		state = .animatingToStopped
	}

	// MARK: Methods (Private)

	fileprivate func isAnimating() -> Bool {
		return state.isAnyOf([.animatingBounce, .animatingToStopped])
	}

	fileprivate func actualContentOffsetY() -> CGFloat {
		guard let sc = scrollView else {
			return 0.0
		}
		return max(-sc.contentInset.top - sc.contentOffset.y, 0)
	}

	fileprivate func currentHeight() -> CGFloat {
		guard let sc = scrollView else {
			return 0.0
		}
		return max(-originalContentInsetTop - sc.contentOffset.y, 0)
	}

	fileprivate func currentWaveHeight() -> CGFloat {
		return min(bounds.height / 3.0 * 1.6, PullRefreshConstants.WaveMaxHeight)
	}

	fileprivate func currentPath() -> CGPath {
		let width: CGFloat = scrollView?.bounds.width ?? 0.0

		let bezierPath = UIBezierPath()
		let animating = isAnimating()

		bezierPath.move(to: CGPoint(x: 0.0, y: 0.0))
		bezierPath.addLine(to: CGPoint(x: 0.0, y: l3ControlPointView.dg_center(animating).y))
		bezierPath.addCurve(to: l1ControlPointView.dg_center(animating), controlPoint1: l3ControlPointView.dg_center(animating), controlPoint2: l2ControlPointView.dg_center(animating))
		bezierPath.addCurve(to: r1ControlPointView.dg_center(animating), controlPoint1: cControlPointView.dg_center(animating), controlPoint2: r1ControlPointView.dg_center(animating))
		bezierPath.addCurve(to: r3ControlPointView.dg_center(animating), controlPoint1: r1ControlPointView.dg_center(animating), controlPoint2: r2ControlPointView.dg_center(animating))
		bezierPath.addLine(to: CGPoint(x: width, y: 0.0))

		bezierPath.close()

		return bezierPath.cgPath
	}

	fileprivate func scrollViewDidChangeContentOffset(dragging: Bool) {
		let offsetY = actualContentOffsetY()

		if state == .stopped && dragging {
			state = .dragging
		} else if state == .dragging && dragging == false {
			if offsetY >= PullRefreshConstants.MinOffsetToPull {
				state = .animatingBounce
			} else {
				state = .stopped
			}
		} else if state.isAnyOf([.dragging, .stopped]) {
			let pullProgress: CGFloat = offsetY / PullRefreshConstants.MinOffsetToPull
			loadingView?.setPullProgress(pullProgress)
		}
	}

	fileprivate func resetScrollViewContentInset(shouldAddObserverWhenFinished: Bool, animated: Bool, completion: (() -> ())?) {
		guard let sc = scrollView else {
			return
		}

		var contentInset = sc.contentInset
		contentInset.top = originalContentInsetTop

		if state == .animatingBounce {
			contentInset.top += currentHeight()
		} else if state == .loading {
			contentInset.top += PullRefreshConstants.LoadingContentInset
		}

		pullRemoveObserver(PullRefreshConstants.KeyPaths.ContentInset)

		let animationBlock = {
			sc.contentInset = contentInset
		}
		let completionBlock: BlockVoid = {
			if shouldAddObserverWhenFinished && self.observing {
				self.pullAddObserver(PullRefreshConstants.KeyPaths.ContentInset)
			}
			completion?()
		}

		if animated {
			startDisplayLink()
			UIView.animate(withDuration: 0.4, animations: animationBlock, completion: { _ in
				self.stopDisplayLink()
				completionBlock()
			})
		} else {
			animationBlock()
			completionBlock()
		}
	}

	fileprivate func animateBounce() {
		guard let sc = scrollView else {
			return
		}
		if (!self.observing) {
			return
		}


		resetScrollViewContentInset(shouldAddObserverWhenFinished: false, animated: false, completion: nil)

		let centerY = PullRefreshConstants.LoadingContentInset
		let duration = 0.9

		sc.isScrollEnabled = false
		startDisplayLink()
		pullRemoveObserver(PullRefreshConstants.KeyPaths.ContentOffset)
		pullRemoveObserver(PullRefreshConstants.KeyPaths.ContentInset)
		UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.43, initialSpringVelocity: 0.0, options: [], animations: { [weak self] in
			self?.cControlPointView.center.y = centerY
			self?.l1ControlPointView.center.y = centerY
			self?.l2ControlPointView.center.y = centerY
			self?.l3ControlPointView.center.y = centerY
			self?.r1ControlPointView.center.y = centerY
			self?.r2ControlPointView.center.y = centerY
			self?.r3ControlPointView.center.y = centerY
		}, completion: { [weak self] _ in
			self?.stopDisplayLink()
			self?.resetScrollViewContentInset(shouldAddObserverWhenFinished: true, animated: false, completion: nil)
			if let strongSelf = self, let scc = strongSelf.scrollView {
				self?.pullAddObserver(PullRefreshConstants.KeyPaths.ContentOffset)
				scc.isScrollEnabled = true
			}
			self?.state = .loading
		})

		bounceAnimationHelperView.center = CGPoint(x: 0.0, y: originalContentInsetTop + currentHeight())
		UIView.animate(withDuration: duration * 0.4, animations: { [weak self] in
			if let contentInsetTop = self?.originalContentInsetTop {
				self?.bounceAnimationHelperView.center = CGPoint(x: 0.0, y: contentInsetTop + PullRefreshConstants.LoadingContentInset)
			}
		}, completion: nil)
	}

	// MARK: -
	// MARK: CADisplayLink

	fileprivate func startDisplayLink() {
		displayLink.isPaused = false
	}

	fileprivate func stopDisplayLink() {
		displayLink.isPaused = true
	}

	@objc
	func displayLinkTick() {
		let width = bounds.width
		var height: CGFloat = 0.0

		if state == .animatingBounce {
			guard let sc = scrollView else {
				return
			}

			sc.contentInset.top = bounceAnimationHelperView.dg_center(isAnimating()).y
			sc.contentOffset.y = -sc.contentInset.top

			height = sc.contentInset.top - originalContentInsetTop

			frame = CGRect(x: 0.0, y: -height - 1.0, width: width, height: height)
		} else if state == .animatingToStopped {
			height = actualContentOffsetY()
		}

		shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
		shapeLayer.path = currentPath()

		layoutLoadingView()
	}

	// MARK: -
	// MARK: Layout

	fileprivate func layoutLoadingView() {
		let width = bounds.width
		let height: CGFloat = bounds.height

		let loadingViewSize: CGFloat = PullRefreshConstants.LoadingViewSize
		let minOriginY = (PullRefreshConstants.LoadingContentInset - loadingViewSize) / 2.0
		let originY: CGFloat = max(min((height - loadingViewSize) / 2.0, minOriginY), 0.0)

		loadingView?.frame = CGRect(x: (width - loadingViewSize) / 2.0, y: originY, width: loadingViewSize, height: loadingViewSize)
		loadingView?.maskLayer.frame = convert(shapeLayer.frame, to: loadingView)
		loadingView?.maskLayer.path = shapeLayer.path
	}

	override open func layoutSubviews() {
		super.layoutSubviews()

		if let sc = scrollView, state != .animatingBounce {
			let width = sc.bounds.width
			let height = currentHeight()

			frame = CGRect(x: 0.0, y: -height, width: width, height: height)

			if state.isAnyOf([.loading, .animatingToStopped]) {
				cControlPointView.center = CGPoint(x: width / 2.0, y: height)
				l1ControlPointView.center = CGPoint(x: 0.0, y: height)
				l2ControlPointView.center = CGPoint(x: 0.0, y: height)
				l3ControlPointView.center = CGPoint(x: 0.0, y: height)
				r1ControlPointView.center = CGPoint(x: width, y: height)
				r2ControlPointView.center = CGPoint(x: width, y: height)
				r3ControlPointView.center = CGPoint(x: width, y: height)
			} else {
				let locationX = sc.panGestureRecognizer.location(in: sc).x

				let waveHeight = currentWaveHeight()
				let baseHeight = bounds.height - waveHeight

				let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0)
				let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width)

				let leftPartWidth = locationX - minLeftX
				let rightPartWidth = maxRightX - locationX

				cControlPointView.center = CGPoint(x: locationX, y: baseHeight + waveHeight * 1.36)
				l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
				l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight)
				l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight)
				r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
				r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight)
				r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight)
			}

			shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
			shapeLayer.path = currentPath()

			layoutLoadingView()
		}
	}

}
