//
//  ToastView.swift
//  ToastView
//
//  Created by Shadowfacts on 8/14/21.
//  Copyright © 2021 Shadowfacts. All rights reserved.
//

import UIKit

class ToastView: UIView {

    let configuration: ToastConfiguration
    
    private(set) var imageView: UIImageView?
    
    private var panRecognizer: UIPanGestureRecognizer!
    private var shrinkAnimator: UIViewPropertyAnimator?
    private var recognizedGesture = false
    private var handledLongPress = false
    private var shouldDismissOnScroll = false
    private(set) var shouldDismissAutomatically = true
    
    private var offscreenTranslation: CGFloat {
        var translation = bounds.height + configuration.edgeSpacing
        if configuration.edge == .bottom {
            translation += superview?.safeAreaInsets.bottom ?? 0
        } else {
            translation += superview?.safeAreaInsets.top ?? 0
            translation *= -1
        }
        return translation
    }
    
    init(configuration: ToastConfiguration) {
        precondition(configuration.edge != .automatic)
        self.configuration = configuration
        
        super.init(frame: .zero)
        
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        backgroundColor = .tintColor
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowRadius = 5
        layer.shadowOffset = CGSize(width: 0, height: 2.5)
        layer.shadowOpacity = 0.5
        layer.masksToBounds = false
        
        addInteraction(UIPointerInteraction(delegate: self))
        
        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .horizontal
        stack.spacing = 8
        
        if let name = configuration.systemImageName {
            let imageView = UIImageView(image: UIImage(systemName: name))
            imageView.tintColor = .white
            imageView.contentMode = .scaleAspectFit
            stack.addArrangedSubview(imageView)
            self.imageView = imageView
        }
        
        let titleLabel = UILabel()
        titleLabel.text = configuration.title
        titleLabel.textColor = .white
        titleLabel.font = configuration.titleFont
        titleLabel.adjustsFontSizeToFitWidth = true
        titleLabel.adjustsFontForContentSizeCategory = true
        
        if let subtitle = configuration.subtitle {
            let subtitleLabel = UILabel()
            subtitleLabel.text = subtitle
            subtitleLabel.textColor = .white
            subtitleLabel.numberOfLines = 0
            subtitleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14))
            subtitleLabel.adjustsFontForContentSizeCategory = true
            let vStack = UIStackView(arrangedSubviews: [
                titleLabel,
                subtitleLabel
            ])
            vStack.axis = .vertical
            vStack.spacing = 4
            stack.addArrangedSubview(vStack)
        } else {
            stack.addArrangedSubview(titleLabel)
        }
        
        if let actionTitle = configuration.actionTitle {
            let actionLabel = UILabel()
            actionLabel.text = actionTitle
            actionLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .boldSystemFont(ofSize: 16))
            actionLabel.adjustsFontForContentSizeCategory = true
            actionLabel.textColor = .white
            stack.addArrangedSubview(actionLabel)
        }
        
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
            trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
            stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
        ])
        
        panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
        addGestureRecognizer(panRecognizer)
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressRecognized))
        longPress.delegate = self
        addGestureRecognizer(longPress)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        layer.cornerRadius = min(32, bounds.height / 2)
        layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: layer.cornerRadius, cornerHeight: layer.cornerRadius, transform: nil)
    }
    
    func dismissToast(animated: Bool) {
        guard animated else {
            removeFromSuperview()
            configuration.onDismiss?(nil)
            return
        }
        let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut)
        animator.addAnimations {
            self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
        }
        animator.addCompletion { _ in
            self.removeFromSuperview()
        }
        configuration.onDismiss?(animator)
        animator.startAnimation()
    }
    
    func animateAppearance() {
        self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
        let duration = 0.5
        let velocity = CGVector(dx: 0, dy: 0.5)
        let animator = UIViewPropertyAnimator(duration: duration, timingParameters: UISpringTimingParameters(dampingRatio: 0.65, initialVelocity: velocity))
        animator.addAnimations {
            self.transform = .identity
        }
        configuration.onAppear?(animator)
        animator.startAnimation()
    }
    
    func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
        guard configuration.dismissOnScroll else { return }
        scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized))
    }
    
    // MARK: - Interaction
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        recognizedGesture = false
        shouldDismissAutomatically = false
        
        shrinkAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) {
            self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        }
        shrinkAnimator?.startAnimation(afterDelay: 0.1)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        
        handledLongPress = false
        
        if !recognizedGesture {
            guard let shrinkAnimator = shrinkAnimator else {
                return
            }
            
            shrinkAnimator.stopAnimation(true)
            UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut) {
                self.transform = .identity
            }
            configuration.action?(self)
        }
        
        shrinkAnimator = nil
    }
    
    private var panGestureDismissAnimator: UIViewPropertyAnimator?
    
    @objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: self).y
        
        let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
        
        
        switch recognizer.state {
        case .began:
            recognizedGesture = true
            shouldDismissAutomatically = false
            UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
                self.transform = .identity
            }
            panGestureDismissAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut)
            configuration.onDismiss?(panGestureDismissAnimator!)
            
        case .changed:
            var distance: CGFloat
            var alongsideAnimationProgress: CGFloat
            if isDraggingAwayFromDismissalEdge {
                distance = sqrt(abs(translation))
                if configuration.edge == .bottom {
                    distance *= -1
                }
                alongsideAnimationProgress = 0
            } else {
                distance = translation
                alongsideAnimationProgress = abs(translation) / (configuration.edgeSpacing + bounds.height)
            }
            
            transform = CGAffineTransform(translationX: 0, y: distance)
            panGestureDismissAnimator!.fractionComplete = alongsideAnimationProgress
            
        case .ended, .cancelled:
            let velocity = recognizer.velocity(in: self).y
            let distance = isDraggingAwayFromDismissalEdge ? sqrt(abs(translation)) : translation
            
            let minDismissalDistance = configuration.edgeSpacing + bounds.height / 2
            let dismissDueToDistance = configuration.edge == .bottom ? distance > minDismissalDistance : -distance > minDismissalDistance
            
            let minDismissalVelocity: CGFloat = 250
            let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
            
            if dismissDueToDistance || dismissDueToVelocity {
                
                if abs(translation) < abs(offscreenTranslation) {
                    panGestureDismissAnimator!.addAnimations {
                        self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
                    }
                    panGestureDismissAnimator!.addCompletion { _ in
                        self.removeFromSuperview()
                    }
                } else {
                    self.removeFromSuperview()
                }
                
                panGestureDismissAnimator!.startAnimation()
                
            } else {
                let duration = 0.5
                let velocity = distance * duration
                UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
                    self.transform = .identity
                }
                
                panGestureDismissAnimator!.isReversed = true
                panGestureDismissAnimator!.startAnimation()
            }
            
        default:
            break
        }
    }
    
    @objc private func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            shouldDismissOnScroll = true
            
        case .changed:
            let translation = recognizer.translation(in: recognizer.view).y
            if shouldDismissOnScroll && abs(translation) > 50 {
                dismissToast(animated: true)
                shouldDismissOnScroll = false
            }
            
        case .ended, .cancelled:
            shouldDismissOnScroll = false
            
        default:
            break
        }
    }
    
    @objc private func longPressRecognized() {
        guard !handledLongPress else {
            return
        }
        configuration.longPressAction?(self)
        handledLongPress = true
    }
    
}

extension ToastView: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // if another recognizer can recognize simulatenously (e.g., table view cell drag initiation) it should require the toast one to fail
        // otherwise long-pressing on a toast results in the drag beginning instead
        return true
    }
}

extension ToastView: UIPointerInteractionDelegate {
    func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
        return defaultRegion
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
        return UIPointerStyle(effect: .highlight(UITargetedPreview(view: self)))
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction, willEnter region: UIPointerRegion, animator: UIPointerInteractionAnimating) {
        shouldDismissAutomatically = false
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction, willExit region: UIPointerRegion, animator: UIPointerInteractionAnimating) {
        shouldDismissAutomatically = true
    }
}
