package com.ffii.core.utils

import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.rendering.PDFRenderer
import org.apache.pdfbox.rendering.ImageType
import java.awt.Graphics2D
import java.awt.image.BufferedImage
import java.io.File
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.Socket
import java.util.*

/**
 * Utility class for generating and sending print jobs to a Zebra printer using ZPL.
 * This class requires the 'org.apache.pdfbox:pdfbox' dependency to be included in your project.
 */
open class ZebraPrinterUtil {

    // Enum to represent valid ZPL print directions
    enum class PrintDirection(val zplCode: String, val degree: Double) {
        NORMAL("N", 0.0),     // Normal (0 degrees)
        ROTATED("R", 90.0),    // Rotated 90 degrees clockwise
        INVERTED("I", 180.0),   // Inverted 180 degrees
        BOTTOM_UP("B", 270.0)   // Bottom-up, 270 degrees clockwise
    }

    companion object {
        /**
         * Converts the first page of a PDF document into a ZPL command and sends it to the specified printer.
         *
         * @param pdfFile The PDF file to be printed.
         * @param printerIp The IP address of the Zebra printer.
         * @param printerPort The port of the Zebra printer, typically 9100.
         * @param printQty The qty of print, default 1
         * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270)
         * @throws Exception if there is an error during file processing or printing.
         */
        fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL, dpi: Int? = 203) {

            // Check if the file exists and is readable
            if (!pdfFile.exists() || !pdfFile.canRead()) {
                throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}")
            }
            val renderDpi = dpi ?: 203
            val safeIp = printerIp.trim()
            if (safeIp.isEmpty()) throw IllegalArgumentException("Error: Printer IP is blank")

            try {
                // Serialize all jobs per printer to prevent interleaving.
                PrinterLockManager.withLock(safeIp, printerPort) {
                    // 1. Load the PDF document
                    PDDocument.load(pdfFile).use { document ->
                        val renderer = PDFRenderer(document)
                        val totalPages = document.numberOfPages

                        println("DEBUG: PDF has $totalPages pages")

                        // 2. Open ONE connection and stream the whole job.
                        Socket().use { socket ->
                            socket.tcpNoDelay = true
                            socket.connect(InetSocketAddress(safeIp, printerPort), 5_000)

                            val os: OutputStream = socket.getOutputStream()
                            val copies = printQty ?: 1

                            // Print each page for the specified quantity
                            repeat(copies.coerceAtLeast(1)) { copyIndex ->
                                println("DEBUG: Printing copy ${copyIndex + 1} of ${copies.coerceAtLeast(1)}")

                                // Iterate through all pages in the PDF
                                for (pageIndex in 0 until totalPages) {
                                    println("DEBUG: Processing page ${pageIndex + 1} of $totalPages")

                                    // Render each page of the PDF as a monochrome image
                                    val image = renderer.renderImage(pageIndex, renderDpi / 72f, ImageType.BINARY)

                                    // Convert the image to a ZPL format string (contains ^XA ... ^XZ)
                                    val zplCommand = convertImageToZpl(image, printDirection)

                                    // Send to printer (same socket for whole job)
                                    os.write(zplCommand.toByteArray(Charsets.US_ASCII))

                                    // Small delay between pages to ensure printer can process each page
                                    Thread.sleep(100)
                                }
                            }

                            os.flush()
                            println("DEBUG: Print job sent to printer ($safeIp:$printerPort)")
                        }
                    }
                }
            } catch (e: Exception) {
                // Re-throw the exception with a more descriptive message
                throw Exception("Error processing print job for PDF: ${e.message}", e)
            }
        }

        /**
         * Rotates a BufferedImage by the specified angle.
         *
         * @param image The BufferedImage to rotate.
         * @param angleDegrees The rotation angle in degrees (clockwise).
         * @return The rotated BufferedImage.
         */
        private fun rotateImage(image: BufferedImage, angleDegrees: Double): BufferedImage {
            val rads = Math.toRadians(angleDegrees)
            val sin = Math.abs(Math.sin(rads))
            val cos = Math.abs(Math.cos(rads))
            val w = image.width
            val h = image.height
            val newWidth = (w * cos + h * sin).toInt()
            val newHeight = (w * sin + h * cos).toInt()

            val rotated = BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_BINARY)
            val g2d: Graphics2D = rotated.createGraphics()
            g2d.translate((newWidth - w) / 2, (newHeight - h) / 2)
            g2d.rotate(rads, w / 2.0, h / 2.0)
            g2d.drawImage(image, 0, 0, null)
            g2d.dispose()
            return rotated
        }

        /**
         * Converts a BufferedImage (monochrome) to a ZPL string using the ^GFA command.
         * This function handles the conversion of pixel data to a compressed hex string.
         *
         * @param image The BufferedImage to convert.
         * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270)
         * @return A ZPL-formatted string ready to be sent to the printer.
         */
        private fun convertImageToZpl(image: BufferedImage, printDirection: PrintDirection = PrintDirection.NORMAL): String {
            val rotatedImage = rotateImage(image, printDirection.degree)
            val zpl = StringBuilder()
            zpl.append("^XA")

            val width = rotatedImage.width
            val height = rotatedImage.height

            // ZPL format for a graphical image is ^GFA
            val bytesPerRow = (width + 7) / 8
            val totalBytes = bytesPerRow * height

            zpl.append("^FO0,0^GFA,").append(totalBytes).append(",").append(totalBytes).append(",").append(bytesPerRow).append(",")
            println(zpl)

            // Extract pixel data and convert to ZPL hex string
            for (y in 0 until height) {
                val rowBits = BitSet(width)
                for (x in 0 until width) {
                    // Check for a black pixel (0xFF000000 in ARGB)
                    if (rotatedImage.getRGB(x, y) == 0xFF000000.toInt()) {
                        rowBits.set(x)
                    }
                }

                for (i in 0 until bytesPerRow) {
                    var byteValue = 0
                    for (bit in 0 until 8) {
                        if (rowBits.get(i * 8 + bit)) {
                            byteValue = byteValue or (1 shl (7 - bit))
                        }
                    }
                    zpl.append(String.format("%02X", byteValue))
                }
            }

            zpl.append("^FS\n")
            zpl.append("^XZ\n")

            return zpl.toString()
        }
    }
}
