// Copyright 2024-present 650 Industries. All rights reserved.

import AVFoundation
import UniformTypeIdentifiers
import Photos
import ExpoModulesCore

internal struct VideoUtils {
  static func tryCopyingVideo(at: URL, to: URL) throws {
    do {
      // we copy the file as `moveItem(at:,to:)` throws an error in iOS 13 due to missing permissions
      try FileManager.default.copyItem(at: at, to: to)
    } catch {
      throw FailedToPickVideoException()
        .causedBy(error)
    }
  }

  /**
   @returns duration in milliseconds
   */
  static func readDurationFrom(url: URL) -> Double {
    let asset = AVURLAsset(url: url)
    return Double(asset.duration.value) / Double(asset.duration.timescale) * 1000
  }

  static func readSizeFrom(url: URL) -> CGSize? {
    let asset = AVURLAsset(url: url)
    guard let assetTrack = asset.tracks(withMediaType: .video).first else {
      return nil
    }
    // The video could be rotated and the resulting transform can result in a negative width/height.
    let size = assetTrack.naturalSize.applying(assetTrack.preferredTransform)
    return CGSize(width: abs(size.width), height: abs(size.height))
  }

  static func readVideoUrlFrom(mediaInfo: MediaInfo) -> URL? {
    return mediaInfo[.mediaURL] as? URL ?? mediaInfo[.referenceURL] as? URL
  }

  /**
   Asynchronously transcodes asset provided as `sourceAssetUrl` according to `exportPreset`.
   Result URL is returned to the `completion` closure.
   Transcoded video is saved at `destinationUrl`, unless `exportPreset` is set to `passthrough`.
   In this case, `sourceAssetUrl` is returned.
   */
  static func transcodeVideoAsync(
    sourceAssetUrl: URL,
    destinationUrl: URL,
    outputFileType: AVFileType,
    exportPreset: VideoExportPreset
  ) async throws -> URL {
    if case .passthrough = exportPreset {
      return sourceAssetUrl
    }
    let asset = AVURLAsset(url: sourceAssetUrl)
    let preset = exportPreset.toAVAssetExportPreset()
    let canBeTranscoded = await AVAssetExportSession.compatibility(ofExportPreset: preset, with: asset, outputFileType: outputFileType)

    guard canBeTranscoded else {
      throw UnsupportedVideoExportPresetException(preset.description)
    }
    guard let exportSession = AVAssetExportSession(asset: asset, presetName: preset) else {
      throw FailedToTranscodeVideoException()
    }

    exportSession.outputFileType = outputFileType
    exportSession.outputURL = destinationUrl

    await exportSession.export()

    if case exportSession.status = .failed {
      let error = exportSession.error
      throw FailedToTranscodeVideoException().causedBy(error)
    }
    return destinationUrl
  }

  static func loadVideoRepresentation(provider: NSItemProvider, urlTransformer: @escaping (URL) throws -> URL) async throws -> URL {
    return try await withCheckedThrowingContinuation { continuation in
      let typeId = UTType.movie.identifier

      provider.loadFileRepresentation(forTypeIdentifier: typeId) { url, error in
        guard let url else {
          return continuation.resume(throwing: FailedToReadVideoException().causedBy(error))
        }
        do {
          // The provided URL is only temporary – the system deletes that file when the completion handler returns.
          // Since we're using it asynchronously, we need to copy the video to another location.
          let newUrl = try urlTransformer(url)
          try VideoUtils.tryCopyingVideo(at: url, to: newUrl)
          continuation.resume(returning: newUrl)
        } catch {
          continuation.resume(throwing: error)
        }
      }
    }
  }
}
