// SPDX-License-Identifier: MIT
import Foundation
import UIKit

/**
 * Zywell-compatible image processor
 * Ports the proven image processing logic from react-native-zywell-thermal-printer iOS
 * while keeping thermal-printer's connection management
 */
class ZywellImageProcessor {

  // Pixel bit patterns for packing 8 pixels into 1 byte
  // Ported from ImageTranster.m:21-27
  private static let p_0: [Int] = [0, 0x80]
  private static let p_1: [Int] = [0, 0x40]
  private static let p_2: [Int] = [0, 0x20]
  private static let p_3: [Int] = [0, 0x10]
  private static let p_4: [Int] = [0, 0x08]
  private static let p_5: [Int] = [0, 0x04]
  private static let p_6: [Int] = [0, 0x02]

  /**
   * Process image file using Zywell logic
   * Main entry point matching Android API
   */
  static func processImageFile(
    imagePath: String,
    widthPx: Int,
    paperWidthMm: Int = 80,
    isCutPaper: Bool = false,
    align: String = "left",
    marginMm: Int = 0
  ) -> Data {
    print("[Native:iOS] ZywellImageProcessor.processImageFile - START: path=\(imagePath), width=\(widthPx), paper=\(paperWidthMm)mm, align=\(align), margin=\(marginMm)mm, isCutPaper=\(isCutPaper)")

    // 1. Load image
    guard let image = UIImage(contentsOfFile: imagePath) else {
      print("[Native:iOS] ZywellImageProcessor.processImageFile - ERROR: Failed to load image")
      return Data()
    }

    print("[Native:iOS] ZywellImageProcessor.processImageFile - Image loaded: \(image.size.width)x\(image.size.height)")

    // 2. Scale image to target width (keep exact requested width for now)
    let requestedWidth = CGFloat(widthPx)
    let aspectRatio = image.size.height / image.size.width
    let targetHeight = requestedWidth * aspectRatio
    var scaledImage = scaleImage(image, targetSize: CGSize(width: requestedWidth, height: targetHeight))

    print("[Native:iOS] ZywellImageProcessor.processImageFile - Image scaled: \(scaledImage.size.width)x\(scaledImage.size.height)")

    // 2.5. Calculate adaptive threshold from ORIGINAL scaled image (before padding)
    // to avoid white padding affecting threshold
    guard let originalGrayPixels = imgToGreyImage(scaledImage) else {
      print("[Native:iOS] ZywellImageProcessor.processImageFile - ERROR: Failed to convert original to grayscale")
      return Data()
    }
    let adaptiveThreshold = calculateAdaptiveThreshold(
      pixels: originalGrayPixels,
      width: Int(scaledImage.size.width),
      height: Int(scaledImage.size.height)
    )
    originalGrayPixels.deallocate()
    print("[Native:iOS] ZywellImageProcessor.processImageFile - Adaptive threshold calculated: \(adaptiveThreshold)")

    // 3. Add margin and/or apply alignment within printable area
    let marginPx = marginMm * 8  // Convert mm to pixels
    let paddedImage = addMarginAndAlign(to: scaledImage, marginPx: marginPx, align: align, paperWidthMm: paperWidthMm)
    print("[Native:iOS] ZywellImageProcessor.processImageFile - Margin and align applied: margin=\(marginMm)mm (\(marginPx)px), align=\(align), paper=\(paperWidthMm)mm, final width=\(paddedImage.size.width)px")

    // 4. Convert to grayscale
    guard let grayPixels = imgToGreyImage(paddedImage) else {
      print("[Native:iOS] ZywellImageProcessor.processImageFile - ERROR: Failed to convert to grayscale")
      return Data()
    }

    // 5. Apply FIXED threshold (from original image)
    let thresholdedPixels = applyFixedThreshold(
      pixels: grayPixels,
      width: Int(paddedImage.size.width),
      height: Int(paddedImage.size.height),
      threshold: adaptiveThreshold
    )

    // 6. Split image into bands (50px height each) - MATCHES ANDROID
    // This is CRITICAL: ESC/POS printers have max height per command (~100 dots)
    // Android uses 50px bands, iOS must do the same!
    let width = Int(paddedImage.size.width)
    let height = Int(paddedImage.size.height)
    let bandHeight = 50
    let numBands = (height + bandHeight - 1) / bandHeight

    print("[Native:iOS] ZywellImageProcessor.processImageFile - BANDS: Processing \(numBands) bands (50px each)")

    var commands = Data()

    // Process each band separately
    for bandIndex in 0..<numBands {
      let startY = bandIndex * bandHeight
      let endY = min(startY + bandHeight, height)
      let currentBandHeight = endY - startY

      // Extract band pixels
      let bandPixels = UnsafeMutablePointer<UInt8>.allocate(capacity: width * currentBandHeight)
      defer { bandPixels.deallocate() }

      for y in 0..<currentBandHeight {
        let srcOffset = (startY + y) * width
        let dstOffset = y * width
        for x in 0..<width {
          bandPixels[dstOffset + x] = thresholdedPixels[srcOffset + x]
        }
      }

      // Convert band to ESC/POS commands
      let bandCommands = convertEachLinePixToCmd(
        src: bandPixels,
        width: width,
        height: currentBandHeight,
        mode: 0
      )

      commands.append(bandCommands)

      print("[Native:iOS] ZywellImageProcessor.processImageFile - BAND \(bandIndex + 1)/\(numBands): \(width)x\(currentBandHeight)px = \(bandCommands.count)b")
    }

    // 7. Add cut paper commands if requested
    if isCutPaper {
      // Feed 3 lines before cut
      commands.append(27)  // ESC
      commands.append(100) // d
      commands.append(3)   // Feed 3 lines

      // Cut paper (GS V m n) - only for 80mm printers
      if paperWidthMm >= 80 {
        commands.append(29)  // GS
        commands.append(86)  // V
        commands.append(66)  // m = 66 (partial cut)
        commands.append(0)   // n = 0

        print("[Native:iOS] ZywellImageProcessor.processImageFile - Cut paper commands added (80mm)")
      } else {
        print("[Native:iOS] ZywellImageProcessor.processImageFile - Feed only, no auto-cut (58mm)")
      }
    }

    print("[Native:iOS] ZywellImageProcessor.processImageFile - SUCCESS: Generated \(commands.count) bytes from \(numBands) bands (matches Android!)")

    // Cleanup
    grayPixels.deallocate()
    thresholdedPixels.deallocate()

    return commands
  }

  /**
   * Scale image to target size
   * Ported from ImageTranster.m:imgWithImage:scaledToFillSize:
   */
  private static func scaleImage(_ image: UIImage, targetSize: CGSize) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0)
    image.draw(in: CGRect(origin: .zero, size: targetSize))
    let scaledImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return scaledImage
  }

  /**
   * Add margin and/or apply alignment to image
   * @param image Source image
   * @param marginPx Margin in pixels (only affects left/right align)
   * @param align Alignment: "left", "center", or "right"
   * @param paperWidthMm Paper width in mm (58 or 80)
   */
  private static func addMarginAndAlign(to image: UIImage, marginPx: Int, align: String, paperWidthMm: Int) -> UIImage {
    let originalWidth = image.size.width
    let originalHeight = image.size.height

    // Standard printable width based on paper size
    let paperWidthPx: CGFloat = paperWidthMm <= 58 ? 384 : 576

    // Final width = printable width (rounded to multiple of 8)
    let newWidth = ceil(paperWidthPx / 8.0) * 8.0

    // If no change needed, return original
    if newWidth == originalWidth && align == "left" && marginPx == 0 {
      return image
    }

    // Calculate x offset based on alignment within printable area
    let xOffset: CGFloat
    switch align.lowercased() {
    case "center":
      xOffset = (newWidth - originalWidth) / 2.0  // Center in printable area
    case "right":
      xOffset = newWidth - originalWidth - CGFloat(marginPx)  // Right with margin
    default: // left
      xOffset = CGFloat(marginPx)  // Left with margin
    }

    UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: originalHeight), false, 1.0)

    // Fill with white background
    let context = UIGraphicsGetCurrentContext()!
    context.setFillColor(UIColor.white.cgColor)
    context.fill(CGRect(x: 0, y: 0, width: newWidth, height: originalHeight))

    // Draw original image with offset
    image.draw(in: CGRect(x: xOffset, y: 0, width: originalWidth, height: originalHeight))

    let paddedImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    return paddedImage
  }

  /**
   * Convert image to grayscale pixels
   * Ported from ImageTranster.m:imgToGreyImage:
   */
  private static func imgToGreyImage(_ image: UIImage) -> UnsafeMutablePointer<UInt8>? {
    let width = Int(image.size.width)
    let height = Int(image.size.height)

    // Create RGB pixel buffer
    let rgbImage = UnsafeMutablePointer<UInt32>.allocate(capacity: width * height)
    defer { rgbImage.deallocate() }

    let colorSpace = CGColorSpaceCreateDeviceRGB()
    guard let context = CGContext(
      data: rgbImage,
      width: width,
      height: height,
      bitsPerComponent: 8,
      bytesPerRow: width * 4,
      space: colorSpace,
      bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue | CGBitmapInfo.byteOrder32Little.rawValue
    ) else { return nil }

    context.interpolationQuality = .high
    context.setShouldAntialias(false)
    context.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: width, height: height))

    // Convert to grayscale
    let grayData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)

    for y in 0..<height {
      for x in 0..<width {
        let rgbPixel = rgbImage[y * width + x]
        let r = UInt32((rgbPixel >> 24) & 0xFF)
        let g = UInt32((rgbPixel >> 16) & 0xFF)
        let b = UInt32((rgbPixel >> 8) & 0xFF)
        let gray = (r + g + b) / 3
        grayData[y * width + x] = UInt8(gray)
      }
    }

    return grayData
  }

  /**
   * Apply adaptive K-means threshold
   * Ported from ImageTranster.m:img_format_K_threshold:
   */
  private static func imgFormatKThreshold(
    pixels: UnsafeMutablePointer<UInt8>,
    width: Int,
    height: Int
  ) -> UnsafeMutablePointer<UInt8> {
    let result = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)

    // Calculate adaptive threshold (average gray value)
    var grayTotal = 0
    for i in 0..<(width * height) {
      grayTotal += Int(pixels[i])
    }
    let grayAverage = grayTotal / (width * height)

    print("[Native:iOS] ZywellImageProcessor.imgFormatKThreshold - Adaptive threshold: \(grayAverage)")

    // Apply threshold
    for i in 0..<(width * height) {
      let gray = Int(pixels[i])
      result[i] = gray > grayAverage ? 0 : 1  // 0=white, 1=black
    }

    return result
  }

  /**
   * Calculate adaptive threshold from grayscale pixels
   * Returns average gray value to use as threshold
   */
  private static func calculateAdaptiveThreshold(
    pixels: UnsafeMutablePointer<UInt8>,
    width: Int,
    height: Int
  ) -> Int {
    var grayTotal = 0
    for i in 0..<(width * height) {
      grayTotal += Int(pixels[i])
    }
    return grayTotal / (width * height)
  }

  /**
   * Apply fixed threshold to grayscale pixels
   * @param pixels Grayscale pixel data (may include padding)
   * @param threshold Fixed threshold value (calculated from original image)
   */
  private static func applyFixedThreshold(
    pixels: UnsafeMutablePointer<UInt8>,
    width: Int,
    height: Int,
    threshold: Int
  ) -> UnsafeMutablePointer<UInt8> {
    let result = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)

    print("[Native:iOS] ZywellImageProcessor.applyFixedThreshold - Applying threshold: \(threshold)")

    // Apply threshold
    for i in 0..<(width * height) {
      let gray = Int(pixels[i])
      result[i] = gray > threshold ? 0 : 1  // 0=white, 1=black
    }

    return result
  }

  /**
   * Convert pixels to ESC/POS raster commands (1 line per command)
   * Ported from ImageTranster.m:convertEachLinePixToCmd:
   */
  private static func convertEachLinePixToCmd(
    src: UnsafeMutablePointer<UInt8>,
    width: Int,
    height: Int,
    mode: Int
  ) -> Data {
    var data = Data()
    let bytesPerLine = width / 8

    var k = 0
    for i in 0..<height {
      // GS v 0 m xL xH yL yH d1...dk
      data.append(29)  // GS
      data.append(118) // v
      data.append(48)  // 0
      data.append(UInt8(mode & 1))  // m
      data.append(UInt8(bytesPerLine % 256))  // xL
      data.append(UInt8(bytesPerLine / 256))  // xH
      data.append(1)  // yL = 1 line per command
      data.append(0)  // yH

      // Pack 8 pixels into 1 byte
      for _ in 0..<bytesPerLine {
        let byte = p_0[Int(src[k])] +
                   p_1[Int(src[k+1])] +
                   p_2[Int(src[k+2])] +
                   p_3[Int(src[k+3])] +
                   p_4[Int(src[k+4])] +
                   p_5[Int(src[k+5])] +
                   p_6[Int(src[k+6])] +
                   Int(src[k+7])
        data.append(UInt8(byte))
        k += 8
      }
    }

    return data
  }
}
