// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import UIKit

/// Lets any UIView communicate when its coordinates in its window change.
///
/// This is useful to know when a view moved on the screen, even when it didn't change frame in its
/// own parent.
///
/// To get notified when the view moved in its window:
///
///     let myView = UIView()
///     myView.cr_onWindowCoordinatesChanged = { view in
///       // Print the window coordinates.
///       print("\(view) moved to \(view convertRect:view.bounds toView:nil)")
///     }
///     let parentView = UIView()
///     parentView.addSubview(myView)
///     let window = UIWindow()
///     window.addSubview(parentView)  // → Calls the closure a first time.
///     parentView.frame = CGRect(x: 10, y: 20, width: 30, height: 40)  // → Calls the closure.
///
///  Even though `myView`'s frame itself was not modified in `parentView`, the closure is called, as
///  actually, `myView` moved transitively in its window.
///
@objc
extension UIView {
  /// MARK: Public

  /// Called when the window coordinates of the view changed.
  ///
  /// The view is passed as argument to the closure. Use it to avoid retaining the view in the
  /// closure, otherwise the view will leak and never get deinitialized.
  @objc public var cr_onWindowCoordinatesChanged: ((UIView) -> Void)? {
    get {
      objc_getAssociatedObject(self, UIView.OnWindowCoordinatesChangedKey) as? (UIView) -> Void
    }
    set {
      objc_setAssociatedObject(
        self, UIView.OnWindowCoordinatesChangedKey, newValue, .OBJC_ASSOCIATION_COPY)
      if newValue != nil {
        // Make sure UIView supports window observing.
        Self.cr_supportsWindowObserving = true
        observation = observe(\.window, options: [.initial]) { [weak self] _, _ in
          guard let self = self else { return }
          if Thread.isMainThread {
            MainActor.assumeIsolated {
              self.windowCoordinatesChangeHandler()
            }
          } else {
            DispatchQueue.main.sync {
              self.windowCoordinatesChangeHandler()
            }
          }
        }
      } else {
        observation = nil
      }
    }
  }

  /// Whether to notify of layout changes synchronously vs
  /// asynchronously (the default). Default value is false.
  ///
  /// Sometimes, there are timing issues if the execution is asynchronous,
  /// so it provides an ability to execute simultaneously.
  /// For example, when switching between horizontal and vertical screens,
  /// view B synchronously relies on the position change of view A to update
  /// its constraints and perform the screen rotation animation.
  /// If A updates the layout asynchronously, B cannot perform the screen
  /// rotation animation correctly.
  @objc public var cr_forcesSynchronousLayoutUpdates: Bool {
    get {
      (objc_getAssociatedObject(self, UIView.ForcesSynchronousLayoutUpdatesKey) as? NSNumber)?
        .boolValue ?? false
    }
    set {
      objc_setAssociatedObject(
        self, UIView.ForcesSynchronousLayoutUpdatesKey, NSNumber.init(value: newValue),
        .OBJC_ASSOCIATION_COPY)
    }
  }

  /// MARK: Private

  /// The currently set observation of the window property.
  private var observation: NSKeyValueObservation? {
    get {
      objc_getAssociatedObject(self, UIView.ObservationKey) as? NSKeyValueObservation
    }
    set {
      objc_setAssociatedObject(
        self, UIView.ObservationKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
  }

  /// Called when the window coordinates of the view changed as a change handler of `observe()`.
  private func windowCoordinatesChangeHandler() {
    if self.window != nil {
      self.addMirrorViewInWindow()
      // Additionally, call the closure here as the view moved to a window.
      self.cr_onWindowCoordinatesChanged?(self)
    } else {
      self.removeMirrorViewInWindow()
    }
  }

  /// Inserts a direct subview to the receiver's window, with constraints such that the mirror view
  /// always has the same window coordinates as the receiver. The mirror view calls the
  /// `onWindowCoordinatesChanged` closure when its bounds change.
  private func addMirrorViewInWindow() {
    let mirrorViewInWindow = NotifyingView()
    mirrorViewInWindow.backgroundColor = .clear
    mirrorViewInWindow.isUserInteractionEnabled = false
    mirrorViewInWindow.translatesAutoresizingMaskIntoConstraints = false
    mirrorViewInWindow.onLayoutChanged = { [weak self] _ in
      guard let self = self else { return }
      if self.cr_forcesSynchronousLayoutUpdates == true {
        self.cr_onWindowCoordinatesChanged?(self)
      } else {
        // Callback on the next turn of the run loop to wait for AutoLayout to have updated the
        // entire hierarchy. (It can happen that AutoLayout updates the mirror view before the
        // mirrored view.)
        DispatchQueue.main.async { [weak self] in
          guard let self = self else { return }
          self.cr_onWindowCoordinatesChanged?(self)
        }
      }
    }

    guard let window = window else { fatalError() }
    window.insertSubview(mirrorViewInWindow, at: 0)
    NSLayoutConstraint.activate([
      mirrorViewInWindow.topAnchor.constraint(equalTo: topAnchor),
      mirrorViewInWindow.bottomAnchor.constraint(equalTo: bottomAnchor),
      mirrorViewInWindow.leadingAnchor.constraint(equalTo: leadingAnchor),
      mirrorViewInWindow.trailingAnchor.constraint(equalTo: trailingAnchor),
    ])

    self.mirrorViewInWindow = mirrorViewInWindow
  }

  /// Removes the mirror view added by a call to `addMirrorViewInWindow`.
  private func removeMirrorViewInWindow() {
    mirrorViewInWindow?.onLayoutChanged = nil
    mirrorViewInWindow?.removeFromSuperview()
    mirrorViewInWindow = nil
  }

  /// The currently set mirror view.
  private var mirrorViewInWindow: NotifyingView? {
    get {
      objc_getAssociatedObject(self, UIView.MirrorViewInWindowKey) as? NotifyingView
    }
    set {
      objc_setAssociatedObject(
        self, UIView.MirrorViewInWindowKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
  }

  /// A simple view that calls a closure when its bounds and center changed.
  private class NotifyingView: UIView {
    var onLayoutChanged: ((UIView) -> Void)?

    override var bounds: CGRect {
      didSet {
        onLayoutChanged?(self)
      }
    }

    override var center: CGPoint {
      didSet {
        onLayoutChanged?(self)
      }
    }
  }

  /// Keys for storing associated objects.
  @UniqueAddress private static var OnWindowCoordinatesChangedKey
  @UniqueAddress private static var ObservationKey
  @UniqueAddress private static var MirrorViewInWindowKey
  @UniqueAddress private static var ForcesSynchronousLayoutUpdatesKey
}

/// A property wrapper to more safely support associated object keys.
/// https://github.com/atrick/swift-evolution/blob/diagnose-implicit-raw-bitwise/proposals/nnnn-implicit-raw-bitwise-conversion.md#associated-object-string-keys
@propertyWrapper
struct UniqueAddress {
  private var _placeholder: Int8 = 0

  var wrappedValue: UnsafeRawPointer {
    mutating get {
      // This is "ok" only as long as the wrapped property appears inside of something with a stable
      // address (a global/static variable or class property) and the pointer is never read or
      // written through, only used for its unique value.
      return withUnsafeBytes(of: &self) {
        return $0.baseAddress.unsafelyUnwrapped
      }
    }
  }
}
