apps/mobile/modules/t3-native-controls/ios/T3NativeFilePresentation.swift

import ImageIO
import QuickLook
import UIKit
import UniformTypeIdentifiers

private final class FilePreviewItem: NSObject, QLPreviewItem {
  var previewItemURL: URL?
  var previewItemTitle: String?
}

private final class FilePreviewController: QLPreviewController {
  var onAppear: (() -> Void)?

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    onAppear?()
  }
}

/// Quick Look owns image and document controls, zooming, and source-view transitions.
final class T3NativeFilePresentation: NSObject, QLPreviewControllerDataSource,
  QLPreviewControllerDelegate, UIAdaptivePresentationControllerDelegate {
  let identifier: String
  private var controller: UIViewController?
  private let completion: (Error?) -> Void
  private weak var sources: T3PresentationSources?
  private let sourceIdentifier: String
  private let item = FilePreviewItem()
  private var loading: Task<Void, Never>?
  private var dismissRequested = false
  private var finished = false

  init(identifier: String, sources: T3PresentationSources, sourceIdentifier: String, completion: @escaping (Error?) -> Void) {
    self.identifier = identifier
    self.sources = sources
    self.sourceIdentifier = sourceIdentifier
    self.completion = completion
    super.init()
  }

  func present(url: URL, title: String, from presenter: UIViewController) {
    loading = Task { @MainActor [self] in
      do {
        let file = try await Self.prepareFile(url: url, title: title)
        guard !finished, !Task.isCancelled else {
          try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
          return
        }
        // Quick Look knows which formats it renders; refuse before presenting so the caller
        // can fall back instead of showing an "unsupported format" page.
        guard QLPreviewController.canPreview(file as NSURL) else {
          try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
          throw NSError(
            domain: "T3NativePresentation",
            code: 3,
            userInfo: [NSLocalizedDescriptionKey: "This file type cannot be previewed on this device."]
          )
        }
        item.previewItemURL = file
        item.previewItemTitle = title
        let preview = FilePreviewController()
        preview.delegate = self
        preview.dataSource = self
        preview.onAppear = { [weak self] in self?.resumePendingDismissal() }
        controller = preview
        presenter.present(preview, animated: !UIAccessibility.isReduceMotionEnabled) { [self] in
          resumePendingDismissal()
        }
        preview.presentationController?.delegate = self
      } catch {
        finish(error: error)
      }
    }
  }

  func dismiss() {
    dismissRequested = true
    loading?.cancel()
    guard !finished else { return }
    guard let controller else { finish(); return }
    // Drain Close from viewDidAppear after opening or cancelling an interactive dismissal.
    // Starting a second modal transition while UIKit is settling the first can strand it.
    guard !controller.isBeingPresented, !controller.isBeingDismissed else { return }
    controller.dismiss(animated: !UIAccessibility.isReduceMotionEnabled) { [self] in finish() }
  }

  private func resumePendingDismissal() {
    // Appearance callbacks run before UIKit has cleared the current transition.
    DispatchQueue.main.async { [weak self] in
      if self?.dismissRequested == true { self?.dismiss() }
    }
  }

  func numberOfPreviewItems(in controller: QLPreviewController) -> Int { item.previewItemURL == nil ? 0 : 1 }

  func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
    item
  }

  func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? {
    guard !UIAccessibility.isReduceMotionEnabled else { return nil }
    return sources?.view(for: sourceIdentifier)
  }

  func previewController(_ controller: QLPreviewController, frameFor item: QLPreviewItem,
                         inSourceView view: AutoreleasingUnsafeMutablePointer<UIView?>) -> CGRect {
    guard !UIAccessibility.isReduceMotionEnabled, let source = sources?.view(for: sourceIdentifier) else { return .zero }
    view.pointee = source
    return source.bounds
  }

  func previewControllerDidDismiss(_ controller: QLPreviewController) { finish() }

  func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { finish() }

  private func finish(error: Error? = nil) {
    guard !finished else { return }
    finished = true
    loading?.cancel()
    loading = nil
    if let file = item.previewItemURL {
      try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
    }
    item.previewItemURL = nil
    DispatchQueue.main.async { [completion] in completion(error) }
  }

  /// Copy original bytes so preview and sharing do not mutate a draft or workspace file.
  nonisolated private static func prepareFile(url: URL, title: String) async throws -> URL {
    try Task.checkCancellation()
    let directory = FileManager.default.temporaryDirectory.appendingPathComponent("t3-preview-\(UUID().uuidString)")
    try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
    do {
      let download = directory.appendingPathComponent("original")
      if url.isFileURL {
        try FileManager.default.copyItem(at: url, to: download)
      } else if url.scheme == "data" {
        try Data(contentsOf: url).write(to: download, options: .atomic)
      } else {
        guard ["https", "http"].contains(url.scheme?.lowercased() ?? "") else {
          throw URLError(.unsupportedURL)
        }
        let (temporaryFile, response) = try await URLSession.shared.download(from: url)
        guard let response = response as? HTTPURLResponse, (200..<300).contains(response.statusCode) else {
          throw URLError(.badServerResponse)
        }
        try FileManager.default.moveItem(at: temporaryFile, to: download)
      }
      try Task.checkCancellation()
      // Pictures and PDFs are identified by content so a misnamed file still opens correctly.
      // Anything else keeps its own extension: Quick Look decides from that whether it can
      // render the document (Office, iWork, RTF and more) and refuses before we present.
      let filename = URL(fileURLWithPath: title).lastPathComponent as NSString
      let originalExtension = filename.pathExtension
      var type: UTType?
      if let image = CGImageSourceCreateWithURL(download as CFURL, nil),
        CGImageSourceGetCount(image) > 0, let imageType = CGImageSourceGetType(image),
        let detectedType = UTType(imageType as String) {
        type = detectedType
      } else if CGPDFDocument(download as CFURL) != nil {
        type = .pdf
      } else if originalExtension.lowercased() == "svg" {
        type = .svg
      }
      let fileExtension: String
      if let type {
        fileExtension = UTType(filenameExtension: originalExtension) == type
          ? originalExtension : type.preferredFilenameExtension ?? originalExtension
      } else {
        fileExtension = originalExtension
      }
      let stem = filename.deletingPathExtension
      var name = String(stem.prefix(60)).components(separatedBy: .controlCharacters).joined(separator: "_")
      while name.utf8.count > 200 { name.removeLast() }
      let baseName = name.isEmpty ? "Preview" : name
      let file = directory.appendingPathComponent(
        fileExtension.isEmpty ? baseName : "\(baseName).\(fileExtension)")
      try FileManager.default.moveItem(at: download, to: file)
      return file
    } catch {
      try? FileManager.default.removeItem(at: directory)
      throw error
    }
  }
}