//
//  NativeImage.swift
//  Pods
//
//  Created by Marc Rousavy on 25.07.25.
//

import UIKit
import NitroModules

private func ceilPixelDimension(_ value: CGFloat) -> CGFloat {
  let rounded = value.rounded()
  if abs(value - rounded) < 0.001 {
    return rounded
  }
  return ceil(value)
}

/**
 * A protocol that represents a native image.
 * This can be used to downcast from `HybridImageSpec`
 * which gives you a concrete `UIImage`.
 *
 * If you want to use other Images with Image Loaders or Image Views,
 * make sure your native image class conforms to this protocol.
 */
public protocol NativeImage {
  var uiImage: UIImage { get }
}

/**
 * Extension for `HybridObject`s that are `NativeImage`: we can bind `memorySize` to the `uiImage`'s memory size*
 */
extension HybridObject where Self: NativeImage {
  var memorySize: Int {
    return uiImage.memorySize
  }
}

public extension NativeImage {
  var width: Double { uiImage.size.width }
  var height: Double { uiImage.size.height }

  func toRawPixelData(allowGpu _: Bool?) throws -> RawPixelData {
    return try uiImage.toRawPixelData()
  }
  func toRawPixelDataAsync(allowGpu: Bool?) -> Promise<RawPixelData> {
    return Promise.async {
      return try self.toRawPixelData(allowGpu: allowGpu)
    }
  }

  func toEncodedImageData(format: ImageFormat, quality: Double?) throws -> EncodedImageData {
    return try uiImage.toEncodedImageData(format: format, quality: quality ?? 100)
  }

  func toEncodedImageDataAsync(format: ImageFormat, quality: Double?) -> Promise<EncodedImageData> {
    return Promise.async {
      return try self.toEncodedImageData(format: format, quality: quality)
    }
  }

  func rotate(degrees: Double, allowFastFlagRotation: Bool?) -> any HybridImageSpec {
    if allowFastFlagRotation == true,
       degrees.truncatingRemainder(dividingBy: 90) == 0,
       let cgImage = uiImage.cgImage {
      // Fast path: we can apply `orientation` instead
      let steps = Int(degrees / 90.0) // can be negative
      let newOrientation = uiImage.imageOrientation.rotated(byRightAngles: steps)
      let rotated = UIImage(cgImage: cgImage, scale: uiImage.scale, orientation: newOrientation)
      return HybridImage(uiImage: rotated)
    } else {
      // Slow path: we actually rotate using UIGraphicsImageRenderer
      let sourceSize = uiImage.size
      let angleInRadians = CGFloat(degrees * .pi / 180)
      let rotatedBounds = CGRect(origin: .zero, size: sourceSize)
        .applying(CGAffineTransform(rotationAngle: angleInRadians))
      let outputSize = CGSize(width: ceilPixelDimension(rotatedBounds.width),
                              height: ceilPixelDimension(rotatedBounds.height))
      let format = UIGraphicsImageRendererFormat()
      format.scale = 1
      let renderer = UIGraphicsImageRenderer(size: outputSize, format: format)
      let rotatedImage = renderer.image { context in
        // 1. Move to the center of the image so our origin is the center
        context.cgContext.translateBy(x: outputSize.width / 2, y: outputSize.height / 2)
        // 2. Rotate by the requested angle
        context.cgContext.rotate(by: angleInRadians)
        // 3. Draw the Image offset by half the frame so we counter our center origin from step 1.
        let rect = CGRect(x: -(sourceSize.width / 2),
                          y: -(sourceSize.height / 2),
                          width: sourceSize.width,
                          height: sourceSize.height)
        uiImage.draw(in: rect)
      }
      return HybridImage(uiImage: rotatedImage)
    }
  }
  func rotateAsync(degrees: Double, allowFastFlagRotation: Bool?) -> Promise<any HybridImageSpec> {
    return Promise.async {
      return self.rotate(degrees: degrees, allowFastFlagRotation: allowFastFlagRotation)
    }
  }

  func resize(width: Double, height: Double) throws -> any HybridImageSpec {
    guard width > 0 else {
      throw RuntimeError.error(withMessage: "Width cannot be less than 0! (width: \(width))")
    }
    guard height > 0 else {
      throw RuntimeError.error(withMessage: "Height cannot be less than 0! (height: \(height))")
    }
    let targetSize = CGSize(width: width, height: height)

    let format = UIGraphicsImageRendererFormat()
    format.scale = 1
    let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
    let resizedImage = renderer.image { context in
      let targetRect = CGRect(origin: .zero, size: targetSize)
      uiImage.draw(in: targetRect)
    }
    return HybridImage(uiImage: resizedImage)
  }

  func resizeAsync(width: Double, height: Double) -> Promise<any HybridImageSpec> {
    return Promise.async {
      return try self.resize(width: width, height: height)
    }
  }

  func crop(startX: Double, startY: Double, endX: Double, endY: Double) throws -> any HybridImageSpec {
    let targetWidth = endX - startX
    let targetHeight = endY - startY
    guard targetWidth > 0 else {
      throw RuntimeError.error(withMessage: "Width cannot be less than 0! (startX: \(startX) - endX: \(endX) = \(targetWidth))")
    }
    guard targetHeight > 0 else {
      throw RuntimeError.error(withMessage: "Height cannot be less than 0! (startY: \(startY) - endY: \(endY) = \(targetHeight))")
    }
    guard let cgImage = uiImage.cgImage else {
      throw RuntimeError.error(withMessage: "This image does not have an underlying .cgImage!")
    }

    let targetRect = CGRect(x: startX,
                            y: startY,
                            width: targetWidth,
                            height: targetHeight)
    guard let croppedCgImage = cgImage.cropping(to: targetRect) else {
      throw RuntimeError.error(withMessage: "Failed to crop CGImage to \(targetRect)!")
    }
    let croppedUiImage = UIImage(cgImage: croppedCgImage)
    return HybridImage(uiImage: croppedUiImage)
  }

  func cropAsync(startX: Double, startY: Double, endX: Double, endY: Double) -> Promise<any HybridImageSpec> {
    return Promise.async {
      return try self.crop(startX: startX, startY: startY, endX: endX, endY: endY)
    }
  }

  func mirrorHorizontally() -> any HybridImageSpec {
    let mirrored = uiImage.withHorizontallyFlippedOrientation()
    return HybridImage(uiImage: mirrored)
  }

  func mirrorHorizontallyAsync() -> Promise<any HybridImageSpec> {
    return Promise.async {
      return mirrorHorizontally()
    }
  }

  private func saveImage(to path: String, format: ImageFormat, quality: Double) throws {
    let data = try uiImage.getData(in: format, quality: quality)
    let url = URL(fileURLWithPath: path.normalizedFilePath)
    let directory = url.deletingLastPathComponent()
    try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
    try data.write(to: url)
  }

  func saveToFileAsync(path: String, format: ImageFormat, quality: Double?) -> Promise<Void> {
    return Promise.async(.utility) {
      try self.saveImage(to: path, format: format, quality: quality ?? 100.0)
    }
  }

  func saveToTemporaryFileAsync(format: ImageFormat, quality: Double?) -> Promise<String> {
    return Promise.async(.utility) {
      let tempDirectory = FileManager.default.temporaryDirectory
      let fileName = "\(UUID().uuidString).\(format.stringValue)"
      let file = tempDirectory.appendingPathComponent(fileName)
      let path = file.path

      try self.saveImage(to: path, format: format, quality: quality ?? 100.0)
      return path
    }
  }

  func toThumbHash() throws -> ArrayBuffer {
    let thumbHash = imageToThumbHash(image: uiImage)
    return try ArrayBuffer.copy(data: thumbHash)
  }

  func toThumbHashAsync() -> Promise<ArrayBuffer> {
    return Promise.async {
      return try self.toThumbHash()
    }
  }

  func renderInto(image newImage: any HybridImageSpec, x: Double, y: Double, width: Double, height: Double) throws -> any HybridImageSpec {
    guard let newImage = newImage as? NativeImage else {
      throw RuntimeError.error(withMessage: "The given image (\(newImage)) is not a `NativeImage`!")
    }
    // 1. Prepare a UIImage rendered
    let format = UIGraphicsImageRendererFormat()
    format.scale = 1
    let renderer = UIGraphicsImageRenderer(size: uiImage.size, format: format)
    let renderedImage = renderer.image { context in
      // 2. Render our own image (copy)
      self.uiImage.draw(at: .zero)

      // 3. Render the new image into our copy
      let rect = CGRect(x: x, y: y, width: width, height: height)
      newImage.uiImage.draw(in: rect)
    }
    // 4. Wrap the resulting UIImage in a HybridImage
    return HybridImage(uiImage: renderedImage)
  }

  func renderIntoAsync(image newImage: any HybridImageSpec, x: Double, y: Double, width: Double, height: Double) -> Promise<any HybridImageSpec> {
    return Promise.async {
      return try self.renderInto(image: newImage, x: x, y: y, width: width, height: height)
    }
  }
}
