// generated by bun run generate from the SwiftUI SDK and codegen/catalog.ts.
// edit the generator or catalog, then regenerate.
import SwiftUI
import UIKit
import Photos
import PhotosUI

private final class LivePhotoViewModel: ObservableObject {
  @Published var assetIdentifier: String = ""
  @Published var autoplay: Bool = false
  @Published var command: String = ""
  @Published var commandRevision: Double = 0
  @Published var accessibility = OneNativeAccessibility()
  @Published var swiftStyle = OneNativeStyle()
  var active = false
  var onSDKEvent: ((String, String) -> Void)?
  func emitSDKEvent(_ name: String, _ value: String) { if active { onSDKEvent?(name, value) } }

  var onPlaybackState: ((String, String, Int) -> Void)?
  private var playbackStateCount = 0
  func playbackState(_ state: String, _ errorCode: String) {
    guard active else { return }
    playbackStateCount += 1
    onPlaybackState?(state, errorCode, playbackStateCount)
  }
}
@objcMembers public final class OneNativeLivePhotoViewView: UIView, OneNativeComposable {
  public var onSDKEvent: ((String, String) -> Void)?

  public var onPlaybackState: ((String, String, Int) -> Void)?
  private var model = LivePhotoViewModel()
  private var controller: OneNativeHostingController<OneNativeStandalone<LivePhotoViewContent>>?
  public override init(frame: CGRect) { super.init(frame: frame) }
  required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
  public func configureAccessibility(_ label: String, hint: String, value: String, identifier: String) {
    let next = OneNativeAccessibility(label: label, hint: hint, value: value, identifier: identifier)
    if model.accessibility != next { model.accessibility = next }
  }
  public func configureStyle(_ style: [String: Any]) {
    let next = OneNativeStyle(dictionary: style)
    if model.swiftStyle != next {
      let previousRows = model.swiftStyle.sdkModifiers.filter { $0.first?.hasPrefix("listRow") == true }
      let nextRows = next.sdkModifiers.filter { $0.first?.hasPrefix("listRow") == true }
      model.swiftStyle = next
      if previousRows != nextRows { compositionParent?.refreshRow(for: self) }
    }
  }
  public func configure(_ assetIdentifier: String, autoplay: Bool, command: String, commandRevision: Double) {
    if model.assetIdentifier != assetIdentifier { model.assetIdentifier = assetIdentifier }
    if model.autoplay != autoplay { model.autoplay = autoplay }
    if model.command != command { model.command = command }
    if model.commandRevision != commandRevision { model.commandRevision = commandRevision }
  }


  private weak var compositionParent: OneNativeCompositionParent?
  public func compositionContent() -> AnyView { AnyView(LivePhotoViewContent(model: model)) }
  // composed, activation follows the parent: a subtree mounted before root attachment
  // stays silent until the root attaches.
  public func composeInto(_ parent: OneNativeCompositionParent) {
    controller?.detach(); controller = nil
    compositionParent = parent
    bindCallbacks()
    model.active = parent.compositionActive
  }
  public func decompose() { compositionParent = nil; model.active = false }
  public func propagateActive(_ active: Bool) { model.active = active }
  public override func didMoveToWindow() { super.didMoveToWindow(); updateHost() }
  public override func layoutSubviews() { super.layoutSubviews(); updateHost() }
  private func bindCallbacks() {
    model.onSDKEvent = { [weak self] name, value in self?.onSDKEvent?(name, value) }

    model.onPlaybackState = { [weak self] state, errorCode, playbackStateCount in self?.onPlaybackState?(state, errorCode, playbackStateCount) }
  }
  private func updateHost() {
    guard compositionParent == nil else { return }
    model.active = false
    guard window != nil else { controller?.detach(); return }
    if controller == nil {
      bindCallbacks()
      controller = OneNativeHostingController(rootView: OneNativeStandalone(content: LivePhotoViewContent(model: model)))
    }
    controller?.attach(to: self)
    model.active = controller?.isAttached == true
  }
  public func reset() {
    compositionParent = nil
    model.active = false; model.onSDKEvent = nil; model.onPlaybackState = nil
    controller?.detach(); controller = nil; model = LivePhotoViewModel()
  }
}
private struct LivePhotoViewContent: View {
  @ObservedObject var model: LivePhotoViewModel
  var body: some View {
    LivePhotoSurface(model: model)
      .oneNativeAccessibility(model.accessibility)
      .oneNativeStyle(model.swiftStyle, emit: model.emitSDKEvent)
  }
}
private final class OneNativeLivePhotoSession: NSObject, ObservableObject, PHLivePhotoViewDelegate {
  weak var view: PHLivePhotoView?
  private var livePhoto: PHLivePhoto?
  private var identifier = ""
  private var generation = 0
  private var requestId = PHInvalidImageRequestID
  private var appliedRevision: Double = 0
  private var pendingCommand: String?
  private var autoplay = false
  private var manualStop = false
  private var state = ""
  private var errorCode = ""
  var report: ((String, String) -> Void)?

  deinit {
    if requestId != PHInvalidImageRequestID {
      PHImageManager.default().cancelImageRequest(requestId)
    }
    view?.delegate = nil
    view?.stopPlayback()
  }

  func attach(_ view: PHLivePhotoView) {
    self.view = view
    view.delegate = self
    view.livePhoto = livePhoto
  }

  func detach(_ view: PHLivePhotoView) {
    view.delegate = nil
    view.stopPlayback()
    if self.view === view { self.view = nil }
  }

  func clear() {
    generation += 1
    if requestId != PHInvalidImageRequestID {
      PHImageManager.default().cancelImageRequest(requestId)
      requestId = PHInvalidImageRequestID
    }
    manualStop = true
    view?.stopPlayback()
    view?.livePhoto = nil
    livePhoto = nil
    identifier = ""
    pendingCommand = nil
  }

  func configure(_ nextIdentifier: String, autoplay: Bool, command: String, revision: Double) {
    self.autoplay = autoplay
    if identifier != nextIdentifier {
      clear()
      identifier = nextIdentifier
      state = ""
      errorCode = ""
      emit("loading")
      guard !nextIdentifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
        fail("E_LIVE_PHOTO_INPUT")
        return
      }
      let permission = PHPhotoLibrary.authorizationStatus(for: .readWrite)
      guard permission == .authorized || permission == .limited else {
        fail("E_LIVE_PHOTO_PERMISSION")
        return
      }
      guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [nextIdentifier], options: nil).firstObject else {
        fail("E_LIVE_PHOTO_NOT_FOUND")
        return
      }
      guard asset.mediaSubtypes.contains(.photoLive) else {
        fail("E_LIVE_PHOTO_NOT_LIVE")
        return
      }
      let currentGeneration = generation
      let options = PHLivePhotoRequestOptions()
      options.deliveryMode = .highQualityFormat
      options.isNetworkAccessAllowed = false
      requestId = PHImageManager.default().requestLivePhoto(for: asset,
        targetSize: CGSize(width: 1280, height: 1280), contentMode: .aspectFit,
        options: options) { [weak self] photo, info in
        DispatchQueue.main.async { [weak self] in
          guard let self, self.generation == currentGeneration else { return }
          if info?[PHImageResultIsDegradedKey] as? Bool == true { return }
          self.requestId = PHInvalidImageRequestID
          guard let photo, info?[PHImageErrorKey] == nil else {
            self.fail("E_LIVE_PHOTO_LOAD")
            return
          }
          self.livePhoto = photo
          self.view?.livePhoto = photo
          self.emit("ready")
          if let pending = self.pendingCommand {
            self.pendingCommand = nil
            self.execute(pending)
          } else if self.autoplay {
            self.execute("play")
          }
        }
      }
    }
    guard revision > appliedRevision else { return }
    appliedRevision = revision
    if livePhoto != nil { execute(command) }
    else if state != "failed" { pendingCommand = command }
  }

  private func execute(_ command: String) {
    guard livePhoto != nil else { return }
    switch command {
    case "play":
      manualStop = false
      view?.startPlayback(with: .full)
    case "stop":
      manualStop = true
      view?.stopPlayback()
      emit("ready")
    default: break
    }
  }

  private func fail(_ code: String) {
    pendingCommand = nil
    emit("failed", code)
  }

  private func emit(_ nextState: String, _ code: String = "") {
    let nextCode = nextState == "failed" ? code : ""
    guard state != nextState || errorCode != nextCode else { return }
    state = nextState
    errorCode = nextCode
    report?(nextState, nextCode)
  }

  func livePhotoView(_ livePhotoView: PHLivePhotoView,
    willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
    manualStop = false
    emit("playing")
  }

  func livePhotoView(_ livePhotoView: PHLivePhotoView,
    didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
    if !manualStop { emit("ended") }
  }
}

private struct OneNativeLivePhotoContainer: UIViewRepresentable {
  let session: OneNativeLivePhotoSession

  func makeCoordinator() -> OneNativeLivePhotoSession { session }
  func makeUIView(context: Context) -> PHLivePhotoView {
    let view = PHLivePhotoView()
    view.contentMode = .scaleAspectFit
    session.attach(view)
    return view
  }
  func updateUIView(_ view: PHLivePhotoView, context: Context) {}
  static func dismantleUIView(_ view: PHLivePhotoView, coordinator: OneNativeLivePhotoSession) {
    coordinator.detach(view)
  }
}

private struct LivePhotoSurface: View {
  @ObservedObject var model: LivePhotoViewModel
  @StateObject private var session = OneNativeLivePhotoSession()

  private func configure() {
    session.configure(model.assetIdentifier, autoplay: model.autoplay,
      command: model.command, revision: model.commandRevision)
  }

  var body: some View {
    OneNativeLivePhotoContainer(session: session)
      .onAppear {
        session.report = { state, errorCode in
          model.playbackState(state, errorCode)
        }
        // the generated host marks its model active after attaching the view.
        // defer the first status so loading and immediate failures reach fabric.
        DispatchQueue.main.async {
          guard session.report != nil else { return }
          configure()
        }
      }
      .onChange(of: model.assetIdentifier) { configure() }
      .onChange(of: model.autoplay) { configure() }
      .onChange(of: model.commandRevision) { configure() }
      .onDisappear {
        session.report = nil
        session.clear()
      }
  }
}
