import Photos
import Dispatch
import SDWebImage
import ExpoModulesCore

/**
 A custom loader for assets from the Photo Library. It handles all urls with the `ph` scheme.
 */
final class PhotoLibraryAssetLoader: NSObject, SDImageLoader {
  // MARK: - SDImageLoader

  func canRequestImage(for url: URL?) -> Bool {
    return isPhotoLibraryAssetUrl(url)
  }

  func requestImage(
    with url: URL?,
    options: SDWebImageOptions = [],
    context: SDWebImageContext?,
    progress progressBlock: SDImageLoaderProgressBlock?,
    completed completedBlock: SDImageLoaderCompletedBlock? = nil
  ) -> SDWebImageOperation? {
    guard isPhotoLibraryStatusAuthorized() else {
      let error = makeNSError(description: "Unauthorized access to the Photo Library")
      completedBlock?(nil, nil, error, false)
      return nil
    }
    let operation = PhotoLibraryAssetLoaderOperation()

    DispatchQueue.global(qos: .userInitiated).async {
      guard let url = url, let assetLocalIdentifier = assetLocalIdentifier(fromUrl: url) else {
        let error = makeNSError(description: "Unable to obtain the asset identifier from the url: '\(String(describing: url?.absoluteString))'")
        completedBlock?(nil, nil, error, false)
        return
      }
      guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetLocalIdentifier], options: .none).firstObject else {
        let error = makeNSError(description: "Asset with identifier '\(assetLocalIdentifier)' not found in the Photo Library")
        completedBlock?(nil, nil, error, false)
        return
      }
      operation.requestId = requestAsset(
        asset,
        url: url,
        context: context,
        progressBlock: progressBlock,
        completedBlock: completedBlock
      )
    }
    return operation
  }

  func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
    // The lack of permission is one of the reasons of failed request,
    // but in that single case we don't want to blacklist the url as
    // the permission might be granted later and then the retry should be possible.
    return isPhotoLibraryStatusAuthorized()
  }
}

/**
 Returns a bool value whether the given url references the Photo Library asset.
 */
internal func isPhotoLibraryAssetUrl(_ url: URL?) -> Bool {
  return url?.scheme == "ph"
}

/**
 Returns the local identifier of the asset from the given `ph://` url.
 These urls have the form of "ph://26687849-33F9-4402-8EC0-A622CD011D70",
 where the asset local identifier is used as the host part.
 */
private func assetLocalIdentifier(fromUrl url: URL) -> String? {
  return url.host
}

/**
 Checks whether the app is authorized to read the Photo Library.
 */
private func isPhotoLibraryStatusAuthorized() -> Bool {
  let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
  return status == .authorized || status == .limited
}

/**
 Requests the image of the given asset object and returns the request identifier.
 */
private func requestAsset(
  _ asset: PHAsset,
  url: URL,
  context: SDWebImageContext?,
  progressBlock: SDImageLoaderProgressBlock?,
  completedBlock: SDImageLoaderCompletedBlock?
) -> PHImageRequestID {
  let options = PHImageRequestOptions()
  options.isSynchronous = false
  options.version = .current
  options.deliveryMode = .highQualityFormat
  options.resizeMode = .fast
  options.normalizedCropRect = .zero
  options.isNetworkAccessAllowed = true

  if let progressBlock = progressBlock {
    options.progressHandler = { progress, _, _, _ in
      // The `progress` is a double from 0.0 to 1.0, but the loader needs integers so we map it to 0...100 range
      let progressPercentage = Int((progress * 100.0).rounded())
      progressBlock(progressPercentage, 100, url)
    }
  }

  var targetSize = PHImageManagerMaximumSize

  // We compute the minimal size required to display the image to avoid having to downsample it later
  if let scale = context?[ImageView.screenScaleKey] as? Double,
    let containerSize = context?[ImageView.frameSizeKey] as? CGSize,
    let contentFit = context?[ImageView.contentFitKey] as? ContentFit {
    targetSize = idealSize(
      contentPixelSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight),
      containerSize: containerSize,
      scale: scale,
      contentFit: contentFit
    ).rounded(.up) * scale
  }

  return PHImageManager.default().requestImage(
    for: asset,
    targetSize: targetSize,
    contentMode: .aspectFit,
    options: options,
    resultHandler: { image, info in
      // This value can be `true` only when network access is allowed and the photo is stored in the iCloud.
      let isDegraded: Bool = info?[PHImageResultIsDegradedKey] as? Bool ?? false

      completedBlock?(image, nil, nil, !isDegraded)
    }
  )
}

/**
 Loader operation specialized for the Photo Library by keeping the request identifier.
 */
private class PhotoLibraryAssetLoaderOperation: NSObject, SDWebImageOperation {
  var canceled: Bool = false
  var requestId: PHImageRequestID?

  // MARK: - SDWebImageOperation

  func cancel() {
    if let requestId = requestId {
      PHImageManager.default().cancelImageRequest(requestId)
    }
    canceled = true
  }
}
