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

import ExpoModulesCore
import SDWebImage
import SDWebImageAVIFCoder
import SDWebImageSVGCoder

public final class ImageModule: Module {
  lazy var prefetcher = SDWebImagePrefetcher.shared

  public func definition() -> ModuleDefinition {
    Name("ExpoImage")

    OnCreate {
      ImageModule.registerCoders()
      ImageModule.registerLoaders()
    }

    View(ImageView.self) {
      Events(
        "onLoadStart",
        "onProgress",
        "onError",
        "onLoad",
        "onDisplay"
      )

      Prop("source") { (view: ImageView, sources: Either<[ImageSource], SharedRef<UIImage>>?) in
        if let imageRef: SharedRef<UIImage> = sources?.get() {
          // Unset an array of traditional sources and just render the image ref right away.
          view.sources = nil
          view.renderSourceImage(imageRef.ref)
        } else {
          // Update an array of sources. Image will start loading once the all props are updated.
          view.sources = sources?.get()
          view.sourceImage = nil
        }
      }

      Prop("placeholder") { (view, placeholders: [ImageSource]?) in
        view.placeholderSources = placeholders ?? []
      }

      Prop("contentFit") { (view, contentFit: ContentFit?) in
        view.contentFit = contentFit ?? .cover
      }

      Prop("placeholderContentFit") { (view, placeholderContentFit: ContentFit?) in
        view.placeholderContentFit = placeholderContentFit ?? .scaleDown
      }

      Prop("contentPosition") { (view, contentPosition: ContentPosition?) in
        view.contentPosition = contentPosition ?? .center
      }

      Prop("transition") { (view, transition: ImageTransition?) in
        view.transition = transition
      }

      Prop("blurRadius") { (view, blurRadius: Double?) in
        let radius = blurRadius ?? .zero
        // the implementation uses Apple's CIGaussianBlur internally
        // we divide the radius to achieve more consistent cross-platform appearance
        // the value was found experimentally
        view.blurRadius = radius / 2.0
      }

      Prop("tintColor") { (view, tintColor: UIColor?) in
        view.imageTintColor = tintColor
      }

      Prop("priority") { (view, priority: ImagePriority?) in
        view.loadingOptions.remove([.lowPriority, .highPriority])

        if let priority = priority?.toSDWebImageOptions() {
          view.loadingOptions.insert(priority)
        }
      }

      Prop("cachePolicy") { (view, cachePolicy: ImageCachePolicy?) in
        view.cachePolicy = cachePolicy ?? .disk
      }

      Prop("enableLiveTextInteraction") { (view, enableLiveTextInteraction: Bool?) in
        #if !os(tvOS)
        view.enableLiveTextInteraction = enableLiveTextInteraction ?? false
        #endif
      }

      Prop("accessible") { (view, accessible: Bool?) in
        view.sdImageView.isAccessibilityElement = accessible ?? false
      }

      Prop("accessibilityLabel") { (view, label: String?) in
        view.sdImageView.accessibilityLabel = label
      }

      Prop("recyclingKey") { (view, key: String?) in
        view.recyclingKey = key
      }

      Prop("allowDownscaling") { (view, allowDownscaling: Bool?) in
        view.allowDownscaling = allowDownscaling ?? true
      }

      Prop("autoplay") { (view, autoplay: Bool?) in
        view.autoplay = autoplay ?? true
      }

      Prop("sfEffect") { (view, sfEffect: [SFSymbolEffect]?) in
        view.sfEffect = sfEffect
      }

      Prop("symbolWeight") { (view, symbolWeight: String?) in
        view.symbolWeight = symbolWeight
      }

      Prop("symbolSize") { (view, symbolSize: Double?) in
        view.symbolSize = symbolSize
      }

      Prop("useAppleWebpCodec", true) { (view, useAppleWebpCodec: Bool) in
        view.useAppleWebpCodec = useAppleWebpCodec
      }

      Prop("enforceEarlyResizing", false) { (view, enforceEarlyResizing: Bool) in
        view.enforceEarlyResizing = enforceEarlyResizing
      }

      Prop("preferHighDynamicRange", false) { (view, preferHighDynamicRange: Bool) in
        if #available(iOS 17.0, macCatalyst 17.0, tvOS 17.0, *) {
          view.sdImageView.preferredImageDynamicRange = preferHighDynamicRange ? .constrainedHigh : .unspecified
        }
      }

      AsyncFunction("startAnimating") { (view: ImageView) in
        if view.isSFSymbolSource {
          view.startSymbolAnimation()
        } else {
          view.sdImageView.startAnimating()
        }
      }

      AsyncFunction("stopAnimating") { (view: ImageView) in
        if view.isSFSymbolSource {
          view.stopSymbolAnimation()
        } else {
          view.sdImageView.stopAnimating()
        }
      }

      AsyncFunction("lockResourceAsync") { (view: ImageView) in
        view.lockResource = true
      }

      AsyncFunction("unlockResourceAsync") { (view: ImageView) in
        view.lockResource = false
      }

      AsyncFunction("reloadAsync") { (view: ImageView) in
        view.reload(force: true)
      }

      OnViewDidUpdateProps { view in
        view.reload()
      }
    }

    Function("configureCache") { (config: ImageCacheConfig) in
      ImageModule.configureCache(config: config)
    }

    AsyncFunction("prefetch") { (urls: [URL], cachePolicy: ImageCachePolicy, headersMap: [String: String]?, promise: Promise) in
      var context = SDWebImageContext()
      let sdCacheType = cachePolicy.toSdCacheType().rawValue
      context[.queryCacheType] = SDImageCacheType.none.rawValue
      context[.storeCacheType] = SDImageCacheType.none.rawValue
      context[.originalQueryCacheType] = sdCacheType
      context[.originalStoreCacheType] = sdCacheType

      var imagesLoaded = 0
      var failed = false

      if headersMap != nil {
        context[.downloadRequestModifier] = SDWebImageDownloaderRequestModifier(headers: headersMap)
      }

      urls.forEach { url in
        SDWebImagePrefetcher.shared.prefetchURLs([url], context: context, progress: nil, completed: { _, skipped in
          if skipped > 0 && !failed {
            failed = true
            promise.resolve(false)
          } else {
            imagesLoaded = imagesLoaded + 1
            if imagesLoaded == urls.count {
              promise.resolve(true)
            }
          }
        })
      }
    }

    AsyncFunction("generateBlurhashAsync") { (source: Either<Image, URL>, numberOfComponents: CGSize, promise: Promise) in
      let parsedNumberOfComponents = (width: Int(numberOfComponents.width), height: Int(numberOfComponents.height))
      generatePlaceholder(source: source) { (image: UIImage) in
        if let blurhashString = blurhash(fromImage: image, numberOfComponents: parsedNumberOfComponents) {
          promise.resolve(blurhashString)
        } else {
          promise.reject(BlurhashGenerationException())
        }
      }
    }

    AsyncFunction("generateThumbhashAsync") { (source: Either<Image, URL>, promise: Promise) in
      generatePlaceholder(source: source) { (image: UIImage) in
        let blurhashString = thumbHash(fromImage: image)
        promise.resolve(blurhashString.base64EncodedString())
      }
    }

    AsyncFunction("clearMemoryCache") { () -> Bool in
      SDImageCache.shared.clearMemory()
      return true
    }

    AsyncFunction("clearDiskCache") { (promise: Promise) in
      SDImageCache.shared.clearDisk {
        promise.resolve(true)
      }
    }

    AsyncFunction("getCachePathAsync") { (cacheKey: String, promise: Promise) in
      /*
       We need to check if the image exists in the cache first since `cachePath` will
       return a path regardless of whether or not the image exists.
       */
      SDImageCache.shared.diskImageExists(withKey: cacheKey) { exists in
        if exists {
          let cachePath = SDImageCache.shared.cachePath(forKey: cacheKey)

          promise.resolve(cachePath)
        } else {
          promise.resolve(nil)
        }
      }
    }

    AsyncFunction("loadAsync") { (source: ImageSource, options: ImageLoadOptions?) -> Image? in
      let image = try await ImageLoadTask(source, options: options ?? ImageLoadOptions()).load()
      return Image(image)
    }

    Class(Image.self) {
      Property("width", \.ref.size.width)
      Property("height", \.ref.size.height)
      Property("scale", \.ref.scale)
      Property("isAnimated", \.isAnimated)
      Property("mediaType") { image in
        return imageFormatToMediaType(image.ref.sd_imageFormat)
      }
    }
  }

  func generatePlaceholder(
    source: Either<Image, URL>,
    generator: @escaping (UIImage) -> Void
  ) {
    if let image: Image = source.get() {
      generator(image.ref)
    } else if let url: URL = source.get() {
      let downloader = SDWebImageDownloader()
      downloader.downloadImage(with: url, progress: nil, completed: { image, _, _, _ in
        DispatchQueue.global().async {
          if let downloadedImage = image {
            generator(downloadedImage)
          }
        }
      })
    }
  }

  static func registerCoders() {
    SDImageCodersManager.shared.addCoder(WebPCoder.shared)
    SDImageCodersManager.shared.addCoder(PSDCoder.shared)
    SDImageCodersManager.shared.addCoder(SDImageAVIFCoder.shared)
    SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
    SDImageCodersManager.shared.addCoder(SDImageHEICCoder.shared)
  }

  static func registerLoaders() {
    SDImageLoadersManager.shared.addLoader(BlurhashLoader())
    SDImageLoadersManager.shared.addLoader(ThumbhashLoader())
    SDImageLoadersManager.shared.addLoader(PhotoLibraryAssetLoader())
    SDImageLoadersManager.shared.addLoader(SFSymbolLoader())
  }

  static func configureCache(config: ImageCacheConfig) {
    if let maxMemoryCount = config.maxMemoryCount {
      SDImageCache.shared.config.maxMemoryCount = maxMemoryCount
    }
    if let maxDiskSize = config.maxDiskSize {
      SDImageCache.shared.config.maxDiskSize = maxDiskSize
    }
    if let maxMemoryCost = config.maxMemoryCost {
      SDImageCache.shared.config.maxMemoryCost = maxMemoryCost
    }
  }
}
