<commit_before>//
//  Copyright © 2016 Schnaub. All rights reserved.
//

import SwiftyGif
import UIKit

protocol AgrumeCellDelegate: AnyObject {

  var isSingleImageMode: Bool { get }

  func dismissAfterFlick()
  func dismissAfterTap()
  func toggleOverlayVisibility()
}

final class AgrumeCell: UICollectionViewCell {

  var tapBehavior: Agrume.TapBehavior = .dismissIfZoomedOut
  var hasPhysics = true

  private lazy var scrollView = with(UIScrollView()) { scrollView in
    scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    scrollView.delegate = self
    scrollView.zoomScale = 1
    scrollView.maximumZoomScale = 8
    scrollView.isScrollEnabled = false
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
  }
  private lazy var imageView = with(UIImageView()) { imageView in
    imageView.contentMode = .scaleAspectFit
    imageView.clipsToBounds = true
    imageView.layer.allowsEdgeAntialiasing = true
  }
  private lazy var singleTapGesture = with(UITapGestureRecognizer(target: self, action: #selector(singleTap))) { gesture in
    gesture.require(toFail: doubleTapGesture)
    gesture.delegate = self
  }
  private lazy var doubleTapGesture = with(UITapGestureRecognizer(target: self, action: #selector(doubleTap))) { gesture in
    gesture.numberOfTapsRequired = 2
  }
  private lazy var panGesture = with(UIPanGestureRecognizer(target: self, action: #selector(dismissPan))) { gesture in
    gesture.maximumNumberOfTouches = 1
    gesture.delegate = self
  }

  private var animator: UIDynamicAnimator?
  private var flickedToDismiss = false
  private var isDraggingImage = false
  private var imageDragStartingPoint: CGPoint!
  private var imageDragOffsetFromActualTranslation: UIOffset!
  private var imageDragOffsetFromImageCenter: UIOffset!
  private var attachmentBehavior: UIAttachmentBehavior?
  
  private var relativeZoomScaleBeforeUpdate: CGFloat?
  private var relativeCenterBeforeUpdate: CGPoint?

  var image: UIImage? {
    didSet {
      if image?.imageData != nil, let image = image {
        imageView.setGifImage(image)
      } else {
        imageView.image = image
      }
      updateScrollViewAndImageViewForCurrentMetrics()

      if let relativeZoomScaleBeforeUpdate = relativeZoomScaleBeforeUpdate,
         let relativeCenterBeforeUpdate = relativeCenterBeforeUpdate,
               relativeZoomScaleBeforeUpdate > 1 {
        // Since navigation require zoom level to be at 1, so zoomScale > 1 implies that this image set is trigger by image update instead cell re-use.
        // In this case we retain the pre-update zoom scale & position
        zoom(to: CGPoint(x: scrollView.frame.size.width * relativeCenterBeforeUpdate.x, y: scrollView.frame.size.height * relativeCenterBeforeUpdate.y), scale: relativeZoomScaleBeforeUpdate)
      }
    }
  }
  weak var delegate: AgrumeCellDelegate?

  private(set) lazy var swipeGesture = with(UISwipeGestureRecognizer(target: self, action: nil)) { gesture in
    gesture.direction = [.left, .right]
    gesture.delegate = self
  }

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

    backgroundColor = .clear
    contentView.addSubview(scrollView)
    scrollView.addSubview(imageView)
    setupGestureRecognizers()
    if hasPhysics {
      animator = UIDynamicAnimator(referenceView: scrollView)
    }
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override func prepareForReuse() {
    super.prepareForReuse()

    relativeZoomScaleBeforeUpdate = scrollView.zoomScale
    relativeCenterBeforeUpdate = CGPoint(
      x: (scrollView.contentOffset.x + scrollView.frame.width / 2) / scrollView.contentSize.width,
      y: (scrollView.contentOffset.y + scrollView.frame.height / 2) / scrollView.contentSize.height)

    imageView.image = nil
    scrollView.zoomScale = 1
    updateScrollViewAndImageViewForCurrentMetrics()
  }

  private func setupGestureRecognizers() {
    contentView.addGestureRecognizer(singleTapGesture)
    contentView.addGestureRecognizer(doubleTapGesture)
    scrollView.addGestureRecognizer(panGesture)
    contentView.addGestureRecognizer(swipeGesture)
  }

  func cleanup() {
    animator = nil
  }

}

extension AgrumeCell: UIGestureRecognizerDelegate {

  private var notZoomed: Bool {
    scrollView.zoomScale == 1
  }

  private var isImageViewOffscreen: Bool {
    let visibleRect = scrollView.convert(contentView.bounds, from: contentView)
    return animator?.items(in: visibleRect).isEmpty == true
  }

  override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    if notZoomed, let pan = gestureRecognizer as? UIPanGestureRecognizer {
      let velocity = pan.velocity(in: scrollView)
      if delegate?.isSingleImageMode == true {
        return true
      }
      return abs(velocity.y) > abs(velocity.x)
    } else if notZoomed, gestureRecognizer as? UISwipeGestureRecognizer != nil {
      return false
    }
    return true
  }

  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    if gestureRecognizer as? UIPanGestureRecognizer != nil {
      return notZoomed
    }
    return true
  }

  @objc
  private func doubleTap(_ sender: UITapGestureRecognizer) {
    let point = scrollView.convert(sender.location(in: sender.view), from: sender.view)
    
    if notZoomed {
      zoom(to: point, scale: .targetZoomForDoubleTap)
    } else {
      zoom(to: .zero, scale: 1)
    }
  }
  
  private func zoom(to point: CGPoint, scale: CGFloat) {
    let factor = 1 / scrollView.zoomScale
    let translatedZoom = CGPoint(
      x: (point.x + scrollView.contentOffset.x) * factor,
      y: (point.y + scrollView.contentOffset.y) * factor
    )

    let width = scrollView.frame.width / scale
    let height = scrollView.frame.height / scale
    let destination = CGRect(x: translatedZoom.x - width / 2, y: translatedZoom.y - height / 2, width: width, height: height)

    contentView.isUserInteractionEnabled = false
    
    CATransaction.begin()
    CATransaction.setCompletionBlock { [unowned self] in
      self.contentView.isUserInteractionEnabled = true
    }
    scrollView.zoom(to: destination, animated: true)
    CATransaction.commit()
  }

  private func contentInsetForScrollView(atScale: CGFloat) -> UIEdgeInsets {
    let boundsWidth = scrollView.bounds.width
    let boundsHeight = scrollView.bounds.height
    let contentWidth = max(image?.size.width ?? 0, boundsWidth)
    let contentHeight = max(image?.size.height ?? 0, boundsHeight)

    var minContentWidth: CGFloat
    var minContentHeight: CGFloat

    if contentHeight > contentWidth {
      if boundsHeight / boundsWidth < contentHeight / contentWidth {
        minContentHeight = boundsHeight
        minContentWidth = contentWidth * (minContentHeight / contentHeight)
      } else {
        minContentWidth = boundsWidth<commit_msg>don't animate when updating image at the same zoom<commit_after>//
//  Copyright © 2016 Schnaub. All rights reserved.
//

import SwiftyGif
import UIKit

protocol AgrumeCellDelegate: AnyObject {

  var isSingleImageMode: Bool { get }

  func dismissAfterFlick()
  func dismissAfterTap()
  func toggleOverlayVisibility()
}

final class AgrumeCell: UICollectionViewCell {

  var tapBehavior: Agrume.TapBehavior = .dismissIfZoomedOut
  var hasPhysics = true

  private lazy var scrollView = with(UIScrollView()) { scrollView in
    scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    scrollView.delegate = self
    scrollView.zoomScale = 1
    scrollView.maximumZoomScale = 8
    scrollView.isScrollEnabled = false
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
  }
  private lazy var imageView = with(UIImageView()) { imageView in
    imageView.contentMode = .scaleAspectFit
    imageView.clipsToBounds = true
    imageView.layer.allowsEdgeAntialiasing = true
  }
  private lazy var singleTapGesture = with(UITapGestureRecognizer(target: self, action: #selector(singleTap))) { gesture in
    gesture.require(toFail: doubleTapGesture)
    gesture.delegate = self
  }
  private lazy var doubleTapGesture = with(UITapGestureRecognizer(target: self, action: #selector(doubleTap))) { gesture in
    gesture.numberOfTapsRequired = 2
  }
  private lazy var panGesture = with(UIPanGestureRecognizer(target: self, action: #selector(dismissPan))) { gesture in
    gesture.maximumNumberOfTouches = 1
    gesture.delegate = self
  }

  private var animator: UIDynamicAnimator?
  private var flickedToDismiss = false
  private var isDraggingImage = false
  private var imageDragStartingPoint: CGPoint!
  private var imageDragOffsetFromActualTranslation: UIOffset!
  private var imageDragOffsetFromImageCenter: UIOffset!
  private var attachmentBehavior: UIAttachmentBehavior?
  
  private var relativeZoomScaleBeforeUpdate: CGFloat?
  private var relativeCenterBeforeUpdate: CGPoint?

  var image: UIImage? {
    didSet {
      if image?.imageData != nil, let image = image {
        imageView.setGifImage(image)
      } else {
        imageView.image = image
      }
      updateScrollViewAndImageViewForCurrentMetrics()

      if let relativeZoomScaleBeforeUpdate = relativeZoomScaleBeforeUpdate,
         let relativeCenterBeforeUpdate = relativeCenterBeforeUpdate,
               relativeZoomScaleBeforeUpdate > 1 {
        // Since navigation require zoom level to be at 1, so zoomScale > 1 implies that this image set is trigger by image update instead cell re-use.
        // In this case we retain the pre-update zoom scale & position
        zoom(to: CGPoint(x: scrollView.frame.size.width * relativeCenterBeforeUpdate.x, y: scrollView.frame.size.height * relativeCenterBeforeUpdate.y),
             scale: relativeZoomScaleBeforeUpdate,
             animated: false)
      }
    }
  }
  weak var delegate: AgrumeCellDelegate?

  private(set) lazy var swipeGesture = with(UISwipeGestureRecognizer(target: self, action: nil)) { gesture in
    gesture.direction = [.left, .right]
    gesture.delegate = self
  }

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

    backgroundColor = .clear
    contentView.addSubview(scrollView)
    scrollView.addSubview(imageView)
    setupGestureRecognizers()
    if hasPhysics {
      animator = UIDynamicAnimator(referenceView: scrollView)
    }
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override func prepareForReuse() {
    super.prepareForReuse()

    relativeZoomScaleBeforeUpdate = scrollView.zoomScale
    relativeCenterBeforeUpdate = CGPoint(
      x: (scrollView.contentOffset.x + scrollView.frame.width / 2) / scrollView.contentSize.width,
      y: (scrollView.contentOffset.y + scrollView.frame.height / 2) / scrollView.contentSize.height)

    imageView.image = nil
    scrollView.zoomScale = 1
    updateScrollViewAndImageViewForCurrentMetrics()
  }

  private func setupGestureRecognizers() {
    contentView.addGestureRecognizer(singleTapGesture)
    contentView.addGestureRecognizer(doubleTapGesture)
    scrollView.addGestureRecognizer(panGesture)
    contentView.addGestureRecognizer(swipeGesture)
  }

  func cleanup() {
    animator = nil
  }

}

extension AgrumeCell: UIGestureRecognizerDelegate {

  private var notZoomed: Bool {
    scrollView.zoomScale == 1
  }

  private var isImageViewOffscreen: Bool {
    let visibleRect = scrollView.convert(contentView.bounds, from: contentView)
    return animator?.items(in: visibleRect).isEmpty == true
  }

  override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    if notZoomed, let pan = gestureRecognizer as? UIPanGestureRecognizer {
      let velocity = pan.velocity(in: scrollView)
      if delegate?.isSingleImageMode == true {
        return true
      }
      return abs(velocity.y) > abs(velocity.x)
    } else if notZoomed, gestureRecognizer as? UISwipeGestureRecognizer != nil {
      return false
    }
    return true
  }

  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    if gestureRecognizer as? UIPanGestureRecognizer != nil {
      return notZoomed
    }
    return true
  }

  @objc
  private func doubleTap(_ sender: UITapGestureRecognizer) {
    let point = scrollView.convert(sender.location(in: sender.view), from: sender.view)
    
    if notZoomed {
      zoom(to: point, scale: .targetZoomForDoubleTap)
    } else {
      zoom(to: .zero, scale: 1)
    }
  }
  
  private func zoom(to point: CGPoint, scale: CGFloat, animated: Bool = true) {
    let factor = 1 / scrollView.zoomScale
    let translatedZoom = CGPoint(
      x: (point.x + scrollView.contentOffset.x) * factor,
      y: (point.y + scrollView.contentOffset.y) * factor
    )

    let width = scrollView.frame.width / scale
    let height = scrollView.frame.height / scale
    let destination = CGRect(x: translatedZoom.x - width / 2, y: translatedZoom.y - height / 2, width: width, height: height)

    contentView.isUserInteractionEnabled = false
    
    CATransaction.begin()
    CATransaction.setCompletionBlock { [unowned self] in
      self.contentView.isUserInteractionEnabled = true
    }
    scrollView.zoom(to: destination, animated: animated)
    CATransaction.commit()
  }

  private func contentInsetForScrollView(atScale: CGFloat) -> UIEdgeInsets {
    let boundsWidth = scrollView.bounds.width
    let boundsHeight = scrollView.bounds.height
    let contentWidth = max(image?.size.width ?? 0, boundsWidth)
    let contentHeight = max(image?.size.height ?? 0, boundsHeight)

    var minContentWidth: CGFloat
    var minContentHeight: CGFloat

    if contentHeight > contentWidth {
      if boundsHeight / boundsWidth < contentHeight / contentWidth {
        minContentHeight = boundsHeight
        minContentWidth = contentWidth * (minContentHeight / contentHeight)
      } else {
        minContentWidth = boundsWidth
{'fraction_non_alphanumeric': 0.07969720774119832, 'fraction_numerical': 0.002999357280582732, 'mean_word_length': 3.7167396429774335, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2062516', 'n_tokens_mistral': 4275, 'n_tokens_neox': 4060, 'n_words': 990}