package com.pingcap;

import SDK.SDKlib;
import SDK.SDKlib.*;
import SDK.SDKlib.Canvas;
import SDK.SDKlib.Config;

import com.pingcap.model.CardInfo;
import com.pingcap.fuction.PrinterUtils;
import com.pingcap.reader.HCReader;
import com.pingcap.model.DatabaseConnectionManager;

import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import com.sun.jna.ptr.IntByReference;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.client.j2se.MatrixToImageWriter;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.*;
import java.awt.*;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;


public class PrinterControllerApplication {
    private final SDKlib SDK;
    private HANDLE hSession;
    private String currentEventId;
    private static final String PRINTER_NAME = "Magicard K (V2)";
    private static final String Slogan = resolveSlogan();
    private static final String Website = "tidb.io";
    private static final String BACKGROUND1_LINK = "https://download.pingcap.com/printer/20240104-card1.png";
    private static final String BACKGROUND2_LINK = "https://download.pingcap.com/printer/20240104-card2.png";
    private static final String BACKGROUND3_LINK = "https://download.pingcap.com/printer/20240104-card3.png";

    private PrinterControllerApplication() {
        this.SDK = new SDKlib();
    }

    public static void main(String[] args) {
        PrinterControllerApplication printer = new PrinterControllerApplication();
        printer.processCards();
    }

    private static String resolveSlogan() {
        String user = System.getProperty("user.name", "");
        String slogan = "invic".equalsIgnoreCase(user)
                ? "Memory for AI Agents"
                : "Built to Scale with Your Ambitions";
        System.out.println("Detected user: " + user + " -> slogan: " + slogan);
        return slogan;
    }

    private void processCards() {
        try {
            // Step 1: Initialize the printer
            if (!initializePrinter()) {
                return;
            }

            if (!configurePrinterForDuplex()) {
                return;
            }

            while (true) {
                // Step 2: Connect to the DB and find all printing status Card Data
                Thread.sleep(1000); // sleep for 1 second
                System.out.println("Find 'printing' status card data from DB...");
                List<CardInfo> printingCards = retrieveAllPrintingCardInfos();
                if (printingCards.isEmpty()) {
                    System.out.println("No 'printing' status card data found. Waiting for new cards...");
                    continue;
                }

                for (CardInfo cardInfo : printingCards) {
                    System.out.println("Begin to print the card of : " + cardInfo.getShortName());

                    // Step 3: Feed Card and Write NFC Url
                    boolean feedSuccess = feedCard();
                    if (!feedSuccess) {
                        System.out.println("No card was obtained, please check if the printer is missing a card");
                        break;
                    }

                    System.out.println("write NDEF tag: " + cardInfo.getNfcUrl());
                    HCReader reader = new HCReader();
                    boolean result = reader.writeNFCUrl(cardInfo.getNfcUrl());
                    if (!result) {
                        // eject Card
                        ejectCard();
                        break;
                    }

                    // Step 4: init Canvas And Draw Content
                    System.out.println("Initialise Front Canvas... ");
                    initFrontCanvasAndDrawContent(cardInfo.getShortName(), cardInfo.getShortCompany(), cardInfo.getNfcUrl(), cardInfo.getCardType());

                    System.out.println("Initialise Rear Canvas... ");
                    initBackCanvasAndDrawContent();

                    // Step 5: Print Card
                    boolean printSuccess = printCard();
                    if (!printSuccess) {
                        // eject Card
                        ejectCard();
                        break;
                    }

                    // Step 6: Change Card Status
                    updateCardStatus(cardInfo.getId(), "printing_finished");
                    System.out.println("Finished print the card of : " + cardInfo.getShortName());
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("The card processing thread was interrupted.");
        } finally {
            closeSession();
        }
    }

    // Step 1: Initialize the printer
    private boolean initializePrinter() {
        System.out.println("Finding Printer...");
        PrintService targetPrinter = findTargetPrinter();
        if (targetPrinter == null) {
            System.out.println(PRINTER_NAME + " printer not found!");
            return false;
        }

        if (!openSession(targetPrinter)) {
            return false;
        }

        // get Serial Number
        String serialNumber = getSerialNumberFromPrinterInfo();
        if (serialNumber == null) {
            System.out.println("Failed to get printer serial number.");
            return false;
        }

        // get event_id
        String eventId = retrieveEventIdFromDB(serialNumber);
        if (eventId == null) {
            System.out.println("Failed to retrieve event ID for printer with serial: " + serialNumber);
            return false;
        }

        // save event_id
        this.currentEventId = eventId;
        System.out.println("Printer is associated with event ID: " + this.currentEventId);

        return true;
    }

    private PrintService findTargetPrinter() {
        PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
        for (PrintService printer : printServices) {
            if (PRINTER_NAME.equals(printer.getName())) {
                return printer;
            }
        }
        return null;
    }

    private boolean openSession(PrintService targetPrinter) {
        String printerNameForSession = targetPrinter.getName();
        System.out.println("Attempting to open session with printer: " + printerNameForSession);

        HANDLEByReference handleRef = new HANDLEByReference();
        byte sdkReturn = SDK.ID_OpenSession(printerNameForSession, handleRef, Config.NORMAL);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("Open session failed, code: " + sdkReturn);
            return false;
        }
        hSession = handleRef.getValue();
        System.out.println("Session opened with printer: " + printerNameForSession);
        return true;
    }

    private String getSerialNumberFromPrinterInfo() {
        PrinterInfo prnInfo = new PrinterInfo();
        byte sdkReturn = SDK.ID_PrinterInfo(hSession, prnInfo);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("Failed to get printer information, code: " + sdkReturn);
            return null;
        }

        byte[] serialBytes = prnInfo.printerSerial;
        StringBuilder serialNumberBuilder = new StringBuilder();
        for (byte b : serialBytes) {
            if (b == 0) break;
            serialNumberBuilder.append((char) b);
        }
        return serialNumberBuilder.toString();
    }

    private String retrieveEventIdFromDB(String serialNumber) {
        String sql = "SELECT event_id FROM printers WHERE sn_code = ?;";
        try (Connection connection = DatabaseConnectionManager.getConnection();
             PreparedStatement pst = connection.prepareStatement(sql)) {
            pst.setString(1, serialNumber);
            try (ResultSet rs = pst.executeQuery()) {
                if (rs.next()) {
                    return rs.getString("event_id");
                }
            }
        } catch (SQLException e) {
            System.out.println("Database connection or query failed: " + e.getMessage());
        }
        return null;
    }

    private boolean configurePrinterForDuplex() {
        PrintSettings OldPS = new PrintSettings();
        byte sdkReturn = SDK.ID_PrintSettings(hSession, Action.READ, OldPS);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("OldPrintSettings" + sdkReturn);
            return false;
        }
        OldPS.duplex = Duplex.BOTH_SIDES;
        sdkReturn = SDK.ID_PrintSettings(hSession, Action.WRITE, OldPS);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println(" WritePrintSettings Failed " + sdkReturn);
            return false;
        }
        return true;
    }

    // Step 2: Connect to the DB and find all printing status Card Data
    private List<CardInfo> retrieveAllPrintingCardInfos() {
        List<CardInfo> printingCards = new ArrayList<>();
        String sql = "SELECT c.id, u.short_name, u.short_company, c.nfc_url, c.card_type, c.status FROM card_info c JOIN user_info u ON c.user_id = u.id WHERE c.status = 'printing' AND c.event_id = ? ORDER BY c.created_at ASC;";

        try (Connection connection = DatabaseConnectionManager.getConnection();
             PreparedStatement pst = connection.prepareStatement(sql)) {
            pst.setString(1, this.currentEventId);
            try (ResultSet rs = pst.executeQuery()) {
                while (rs.next()) {
                    printingCards.add(new CardInfo(
                            rs.getInt("id"),
                            rs.getString("short_name"),
                            rs.getString("short_company"),
                            rs.getString("nfc_url"),
                            rs.getInt("card_type"),
                            rs.getString("status")
                    ));
                }
            }
        } catch (SQLException e) {
            System.out.println("Database connection or query failed: " + e.getMessage());
        }
        return printingCards;
    }

    // Step 3: Feed Card
    private boolean feedCard() {
        byte sdkReturn = SDK.ID_FeedCard(hSession, FeedMode.CONTACTLESS, 0);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("FeedCard failed: " + sdkReturn);
            return false;
        }
        //Wait for the card to be feed
        sdkReturn = PrinterUtils.WaitForPrinterToFinish(hSession, SDK);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("FeedCardWait failed: " + sdkReturn);
            return false;
        }
        return true;
    }

    private void ejectCard() {
        byte sdkReturn = SDK.ID_EjectCard(hSession);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("ejectCard failed: " + sdkReturn);
        }
        //Wait for the card to be ejected
        sdkReturn = PrinterUtils.WaitForPrinterToFinish(hSession, SDK);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("EjectCardWait failed: " + sdkReturn);
        }
    }

    // Step 4: init Canvas And Draw Content
    private void initFrontCanvasAndDrawContent(String name, String company, String nfcUrl, int cardType) {
        //Initialise Front Canvas
        IntByReference FrontDC = new IntByReference();
        byte sdkReturn = SDK.ID_CanvasInit(hSession, FrontDC, Canvas.FRONT);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("Canvas Init Front Failed" + sdkReturn);
            return;
        }

        // 1 Generate QR Code
        String qrCodePath;
        try {
            File tempQRCode = File.createTempFile("tempQRCode", ".png");
            qrCodePath = tempQRCode.getAbsolutePath();
            generateQRCodeImage(nfcUrl, 160, 160, qrCodePath);
        } catch (Exception e) {
            System.out.println("generate QRCode Image Failed : " + e.getMessage());
            return;
        }

        // 2 Choose Card Background based on Card_Type
        switch (cardType) {
            case 1:
                // BackGround 1
                drawImage(hSession, 0, 0, 1013, 642, BACKGROUND1_LINK, Canvas.FRONT);
                // QR code
                drawImage(hSession, 826, 456, 160, 160, qrCodePath, Canvas.FRONT);
                // User Name
                drawText(hSession, "Poppins", 14, 120, 246, name, new Color(0, 0, 0), FontStyle.BOLD, Canvas.FRONT);
                // Company
                drawText(hSession, "Poppins", 11, 120, 323, company, new Color(0, 0, 0), (byte) 0, Canvas.FRONT);
                break;

            case 2:
                // BackGround 2
                drawImage(hSession, 0, 0, 1013, 642, BACKGROUND2_LINK, Canvas.FRONT);
                // QR code
                drawImage(hSession, 755, 226, 160, 160, qrCodePath, Canvas.FRONT);
                // User Name
                drawText(hSession, "Poppins", 14, 115, 270, name, new Color(0, 0, 0), FontStyle.BOLD, Canvas.FRONT);
                // Company
                drawText(hSession, "Poppins", 11, 115, 340, company, new Color(0, 0, 0), (byte) 0, Canvas.FRONT);
                break;

            default:
                // BackGround 3
                drawImage(hSession, 0, 0, 1013, 642, BACKGROUND3_LINK, Canvas.FRONT);
                // QR code
                drawImage(hSession, 788, 440, 160, 160, qrCodePath, Canvas.FRONT);
                // User Name
                int xForUsername = calculateCenteredX(name, "Poppins", 14, 1013, Font.BOLD);
                drawText(hSession, "Poppins", 14, xForUsername, 246, name, new Color(0, 0, 0), FontStyle.BOLD, Canvas.FRONT);
                // Company
                int xForCompany = calculateCenteredX(company, "Poppins", 11, 1013, Font.PLAIN);
                drawText(hSession, "Poppins", 11, xForCompany, 317, company, new Color(0, 0, 0), (byte) 0, Canvas.FRONT);
        }
    }

    private void initBackCanvasAndDrawContent() {
        //Initialise Rear Canvas
        IntByReference BackResinDC = new IntByReference();
        byte sdkReturn = SDK.ID_CanvasInit(hSession, BackResinDC, Canvas.BACK);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("Canvas Init Back Failed" + sdkReturn);
        }

        //  draw slogan
        byte combinedStyle = (byte) (FontStyle.BOLD | FontStyle.ITALIC);
        int xForSlogan = calculateCenteredX(Slogan, "Poppins", 9, 1013, combinedStyle);
        drawText(hSession, "Poppins", 9, xForSlogan, 268, Slogan, new Color(0, 0, 0), combinedStyle, Canvas.BACK);

        // draw website
        int xForWebsite = calculateCenteredX(Website, "Poppins", 9, 1013, Font.PLAIN);
        drawText(hSession, "Poppins", 9, xForWebsite, 339, Website, new Color(0, 0, 0), (byte) 0, Canvas.BACK);
    }

    private void drawImage(HANDLE hSession, int x, int y, int p1, int p2, String imageUrl, byte canvasID) {
        // Check if the file exists at the given path, If it's a local file path, use it directly, Otherwise, download the image.
        String localPath = new File(imageUrl).exists() ? imageUrl : downloadImage(imageUrl);

        if (localPath != null && !localPath.isEmpty()) {
            ImageDef imageDef = new ImageDef();
            imageDef.x = x;
            imageDef.y = y;
            imageDef.p1 = p1;
            imageDef.p2 = p2;
            imageDef.filename = localPath;
            byte sdkReturn = SDK.ID_DrawImage(hSession, canvasID, imageDef);
            if (sdkReturn != Return.SUCCESS) {
                System.out.println("draw Image Failed: " + sdkReturn);
            }
        }
    }

    private String downloadImage(String imageUrl) {
        try {
            URL url = new URL(imageUrl);
            String fileName = url.getFile();
            File tempFile = File.createTempFile("tempImage", fileName.substring(fileName.lastIndexOf(".")));
            try (InputStream in = url.openStream()) {
                Files.copy(in, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }
            System.out.println("Image download successfully, saved to: " + tempFile.getAbsolutePath());
            return tempFile.getAbsolutePath();
        } catch (Exception e) {
            System.out.println("Error downloading image: " + e.getMessage());
            return null;
        }
    }

    private static void generateQRCodeImage(String text, int width, int height, String filePath) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();
        Map<EncodeHintType, ErrorCorrectionLevel> hints = new HashMap<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
        Path path = FileSystems.getDefault().getPath(filePath);
        MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
    }

    private int calculateCenteredX(String text, String fontName, int fontSizePoints, int cardWidthPixels, int fontStyle) {
        // DPI of the printer
        final int printerDPI = 300;
        // Convert points to pixels for the printer DPI (1 point = 1/72 of an inch)
        int fontSizePixels = (int) ((fontSizePoints / 72.0) * printerDPI);

        // Create a temporary Graphics2D object to get text width
        BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        // Apply rendering hints for text antialiasing, if necessary
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        try {
            // Use correct Java font style constants
            Font font = new Font(fontName, fontStyle, fontSizePixels);
            g2d.setFont(font);
            FontMetrics fm = g2d.getFontMetrics();
            int textWidth = fm.stringWidth(text);
            // Calculate the starting x-coordinate to center the text horizontally
            return (cardWidthPixels - textWidth) / 2;
        } finally {
            g2d.dispose();
        }
    }

    private void drawText(HANDLE hSession, String fontName, int fontSize, int x, int y, String text, Color color, byte fontStyle, byte canvasID) {
        System.out.println("Starting text drawing: " + text);
        TextDef textDef = SDK.new TextDef();
        textDef.fontName = fontName;
        textDef.fontSize = fontSize;
        textDef.x = x;
        textDef.y = y;
        textDef.text = text;
        textDef.fontColour = color;
        textDef.fontStyle = fontStyle;

        int result = SDK.ID_DrawText(hSession, canvasID, textDef);
        if (result != 0) {
            System.out.println("Text drawing failed, error code: " + result);
        } else {
            System.out.println("Text drawn successfully! ");
        }
    }

    // Step 5: Print Card
    private boolean printCard() {
        System.out.println("Attempting to print card...");
        byte sdkReturn = SDK.ID_PrintCard(hSession);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("PrintCard failed: " + sdkReturn);
            return false;
        }

        // Wait for the printing to complete
        sdkReturn = PrinterUtils.WaitForPrinterToFinish(hSession, SDK);
        if (sdkReturn != Return.SUCCESS) {
            System.out.println("PrintCardWait failed: " + sdkReturn);
            return false;
        }
        return true;
    }

    // Step 6: Change Card Status
    private void updateCardStatus(int id, String newStatus) {
        String sql = "UPDATE card_info SET status = ? WHERE id = ?;";
        try (Connection connection = DatabaseConnectionManager.getConnection();
             PreparedStatement pst = connection.prepareStatement(sql)) {
            pst.setString(1, newStatus);
            pst.setInt(2, id);
            pst.executeUpdate();

        } catch (SQLException e) {
            e.printStackTrace();
            System.out.println("Failed to update the card status in the database");
        }
    }

    private void closeSession() {
        if (hSession != null) {
            SDK.ID_CloseSession(hSession);
            hSession = null;
            System.out.println("Session with printer closed.");
        }
    }
}