package com.anggastudio.printama;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.RequiresPermission;

import com.anggastudio.printama.constants.PA;
import com.anggastudio.printama.constants.PW;
import com.anggastudio.printama.util.StrUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.UUID;

class PrinterUtil {
    private static final String TAG = "PRINTAMA";
    private static final int PRINTER_WIDTH_2_INCH = 384; // 2-inch (58mm) printer
    private static final int PRINTER_WIDTH_3_INCH = 576; // 3-inch (80mm) printer
    private static final int MAX_CHAR_2_INCH = 32;
    private static final int MAX_CHAR_3_INCH = 48;
    private static final int MAX_CHAR_MP5_INCH = 54;
    private static final int WIDTH_2_INCH = 48;  // 384/8 = 48 bytes per line
    private static final int WIDTH_3_INCH = 72;  // 576/8 = 72 bytes per line
    private static final int HEAD = 8;
    // printer commands
    private static final byte[] NEW_LINE = {10};
    private static final byte[] ESC_ALIGN_CENTER = {0x1b, 'a', 0x01};
    private static final byte[] ESC_ALIGN_RIGHT = {0x1b, 'a', 0x02};
    private static final byte[] ESC_ALIGN_LEFT = {0x1b, 'a', 0x00};
    private static final byte[] FEED_PAPER_AND_CUT = {0x1D, 0x56, 66, 0x00};

    private static final byte[] SMALL = new byte[]{0x1B, 0x21, 0x01};
    private static final byte[] NORMAL = new byte[]{0x1B, 0x21, 0x00};
    private static final byte[] NORMALIZED_FOR_ALL = new byte[]{0x1B, 0x4D, 0x00};
    private static final byte[] BOLD = new byte[]{0x1B, 0x21, 0x08};
    private static final byte[] WIDE = new byte[]{0x1B, 0x21, 0x20};
    private static final byte[] TALL = new byte[]{0x1B, 0x21, 0x10};
    private static final byte[] UNDERLINE = new byte[]{0x1B, 0x21, (byte) 0x80};
    private static final byte[] DELETE_LINE = new byte[]{0x1B, 0x21, (byte) 0x40};
    private static final byte[] WIDE_BOLD = new byte[]{0x1B, 0x21, 0x20 | 0x08};
    private static final byte[] TALL_BOLD = new byte[]{0x1B, 0x21, 0x10 | 0x08};
    private static final byte[] WIDE_TALL = new byte[]{0x1B, 0x21, 0x20 | 0x10};
    private static final byte[] WIDE_TALL_BOLD = new byte[]{0x1B, 0x21, 0x20 | 0x10 | 0x08};

    private final BluetoothDevice printer;
    private BluetoothSocket btSocket = null;
    private OutputStream btOutputStream = null;
    private InputStream btInputStream = null;
    private boolean is3InchPrinter;

    PrinterUtil(BluetoothDevice printer) {
        this.printer = printer;
    }

    void connectPrinter(final PrinterConnected successListener, PrinterConnectFailed failedListener) {
        new ConnectAsyncTask(new ConnectAsyncTask.ConnectionListener() {
            @Override
            public void onConnected(BluetoothSocket socket) {
                btSocket = socket;
                try {
                    btOutputStream = socket.getOutputStream();
                    btInputStream = socket.getInputStream();
                    successListener.onConnected();
                } catch (IOException e) {
                    failedListener.onFailed();
                }
            }

            @Override
            public void onFailed() {
                failedListener.onFailed();
            }
        }).execute(printer);
    }

     boolean isConnected() {
        if (btSocket == null) {
            return false;
        }

        try {
            // Tester la connexion en vérifiant le flux d'entrée
            if (btSocket.isConnected()) {
                // Vérifier si le flux est accessible
                InputStream inputStream = btSocket.getInputStream();
                // Tenter de lire avec un timeout très court
                if (inputStream.available() >= 0) {
                    return true;
                }
            }
            return false;
        } catch (IOException e) {
            // Exception signifie que la connexion est perdue
            Log.e("Bluetooth", "Socket non connecté: " + e.getMessage());
            return false;
        }
    }

    void finish() {
        if (btSocket != null) {
            try {
                if (btOutputStream != null) {
                    btOutputStream.flush(); // ensure buffer is drained
                }
                btOutputStream.close();
                btSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            btSocket = null;
        }
    }

    void resetPrinter() {
        printUnicode(new byte[]{0x1B, 0x40}); // ESC @
    }

    private boolean printUnicode(byte[] data) {
        try {
            btOutputStream.write(data);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    //----------------------------------------------------------------------------------------------
    // PRINT TEXT
    //----------------------------------------------------------------------------------------------

    boolean printText(String text) {
        try {
            String s = StrUtil.encodeNonAscii(text);
            btOutputStream.write(s.getBytes());
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    String textTest(String text){
        try{
            String s = StrUtil.encodeNonAscii(text);
            btOutputStream.write(s.getBytes());
            btOutputStream.flush();
            int available = btInputStream.available();
            return String.valueOf(available);
        }catch (Exception e){
            e.printStackTrace();
            return "Erreur: "+e.getMessage();
        }
    }

    boolean printTextBuilder(StringBuilder text){
        try {
            String tspl = ""+ text + "\r\n";
            String s = StrUtil.encodeNonAscii(tspl);
            btOutputStream.write(s.getBytes());
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * Vérifie si l'imprimante a une opération en attente ou en cours
     *
     * @return
     *   0 = Aucune opération (imprimante prête)
     *   1 = Opération en cours d'impression
     *   2 = Opération en attente (buffer plein)
     *   -1 = Erreur / impossible de déterminer
     */
    public int isQueued() {
        try {
            // Vider le buffer d'entrée avant d'envoyer la commande
            while (btInputStream != null && btInputStream.available() > 0) {
                btInputStream.read();
            }
            // Commande DLE EOT 1 - Statut en temps réel
            byte[] command = new byte[]{0x10, 0x04, 0x01};
            btOutputStream.write(command);
            btOutputStream.flush();

            // Attendre 100ms pour la réponse
            Thread.sleep(100);

            // Lire la réponse (1 octet)
            if (btInputStream != null && btInputStream.available() > 0) {
                byte[] response = new byte[1];
                int bytesRead = btInputStream.read(response);

                if (bytesRead > 0) {
                    byte status = response[0];

                    // Bit 1 (0x02) = Imprimante en cours d'impression
                    if ((status & 0x02) != 0) {
                        return 1;  // Opération en cours
                    }

                    // Bit 3 (0x08) = Buffer plein / attente
                    if ((status & 0x08) != 0) {
                        return 2;  // Opération en attente
                    }

                    return 0;  // Aucune opération
                }
            }

            // Pas de réponse
            return -1;

        } catch (Exception e) {
            Log.e(TAG, "Erreur isQueued: " + e.getMessage());
            return -1;
        }
    }

    void setNormalizedForAll(){
        printUnicode(NORMALIZED_FOR_ALL);
    }

    void setNormalText() {
        printUnicode(NORMAL);
    }

    void setSmallText() {
        printUnicode(SMALL);
    }

    void setBold() {
        printUnicode(BOLD);
    }

    void setUnderline() {
        printUnicode(UNDERLINE);
    }

    void setDeleteLine() {
        printUnicode(DELETE_LINE);
    }

    void setTall() {
        printUnicode(TALL);
    }

    void setWide() {
        printUnicode(WIDE);
    }

    void setWideBold() {
        printUnicode(WIDE_BOLD);
    }

    void setTallBold() {
        printUnicode(TALL_BOLD);
    }

    void setWideTall() {
        printUnicode(WIDE_TALL);
    }

    void setWideTallBold() {
        printUnicode(WIDE_TALL_BOLD);
    }

    void printEndPaper() {
        printUnicode(FEED_PAPER_AND_CUT);
    }

    boolean addNewLine() {
        return printUnicode(NEW_LINE);
    }

    int addNewLine(int count) {
        int success = 0;
        for (int i = 0; i < count; i++) {
            if (addNewLine()) success++;
        }
        return success;
    }

    void setAlign(int alignType) {
        byte[] d;
        switch (alignType) {
            case PA.CENTER:
                d = ESC_ALIGN_CENTER;
                break;
            case PA.RIGHT:
                d = ESC_ALIGN_RIGHT;
                break;
            default:
                d = ESC_ALIGN_LEFT;
                break;
        }
        try {
            btOutputStream.write(d);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void setLineSpacing(int lineSpacing) {
        byte[] cmd = new byte[]{0x1B, 0x33, (byte) lineSpacing};
        printUnicode(cmd);
    }

    //----------------------------------------------------------------------------------------------
    // PRINT IMAGE
    //----------------------------------------------------------------------------------------------

    boolean printImage(Bitmap bitmap) {
        try {
            int width = bitmap.getWidth() > getPrinterWidth() ? PW.FULL_WIDTH : PW.ORIGINAL_WIDTH;
            return printImage(PA.CENTER, bitmap, width);
        } catch (NullPointerException e) {
            Log.e(TAG, e.getMessage());
            return false;
        }
    }

    public void testAllPaperStatusCommands() {
        // Liste des commandes à tester
        int[][] commands = {
                {0x1D, 0x72, 0x01},  // GS r 1 - Standard paper status
                {0x10, 0x04, 0x01},  // DLE EOT 1 - Real-time status
                {0x10, 0x04, 0x04},  // DLE EOT 4 - Paper sensor status
                {0x1B, 0x76, 0x00},  // ESC v 0 - Paper sensor
                {0x1D, 0x61, 0x01},  // GS a 1 - Enable real-time mode
                {0x1D, 0x49, 0x01},  // GS I 1 - Printer ID
                {0x1B, 0x69, 0x00}   // ESC i 0 - Printer status
        };

        for (int i = 0; i < commands.length; i++) {
            try {
                Log.e("TEST", "========================================");
                Log.e("TEST", "Test commande " + i + ": " + bytesToHex(commands[i]));

                // Nettoyer buffer
                while (btInputStream.available() > 0) {
                    btInputStream.read();
                }

                // Envoyer commande
                byte[] cmd = new byte[commands[i].length];
                for (int j = 0; j < commands[i].length; j++) {
                    cmd[j] = (byte) commands[i][j];
                }
                btOutputStream.write(cmd);
                btOutputStream.flush();

                // Attendre réponse
                Thread.sleep(300);

                // Lire réponse
                byte[] response = new byte[16];
                int totalRead = 0;
                long startTime = System.currentTimeMillis();

                while (totalRead < response.length && System.currentTimeMillis() - startTime < 1000) {
                    if (btInputStream.available() > 0) {
                        int read = btInputStream.read(response, totalRead, response.length - totalRead);
                        if (read > 0) {
                            totalRead += read;
                        }
                    }
                    Thread.sleep(20);
                }

                if (totalRead > 0) {
                    Log.e("TEST", "✓ RÉPONSE REÇUE ! " + totalRead + " octets");
                    StringBuilder hex = new StringBuilder();
                    for (int j = 0; j < totalRead; j++) {
                        hex.append(String.format("%02X ", response[j]));
                    }
                    Log.e("TEST", "  Hex: " + hex.toString());

                    // Interpréter la réponse selon la commande
                    if (i == 0 && totalRead >= 1) { // GS r 1
                        interpretGS_r1(response[0]);
                    } else if (i == 1 && totalRead >= 1) { // DLE EOT 1
                        interpretDLE_EOT_1(response[0]);
                    } else if (i == 2 && totalRead >= 1) { // DLE EOT 4
                        interpretDLE_EOT_4(response[0]);
                    } else if (i == 5 && totalRead >= 1) { // GS I 1
                        Log.e("TEST", "  ID Imprimante: " + new String(response, 0, totalRead));
                    }
                } else {
                    Log.e("TEST", "✗ AUCUNE RÉPONSE");
                }

            } catch (Exception e) {
                Log.e("TEST", "Erreur commande " + i, e);
            }
        }
    }

    private void interpretGS_r1(byte status) {
        Log.e("TEST", "Interprétation GS r 1:");
        Log.e("TEST", "  Bit 0 (couvercle): " + ((status & 0x01) != 0 ? "OUVERT" : "FERMÉ"));
        Log.e("TEST", "  Bit 2 (papier): " + ((status & 0x04) != 0 ? "FAIBLE" : "OK"));
        Log.e("TEST", "  Bit 5 (papier): " + ((status & 0x20) != 0 ? "VIDE" : "OK"));
    }

    private void interpretDLE_EOT_1(byte status) {
        Log.e("TEST", "Interprétation DLE EOT 1:");
        Log.e("TEST", "  Bit 2 (papier): " + ((status & 0x04) != 0 ? "VIDE" : "OK"));
    }

    private void interpretDLE_EOT_4(byte status) {
        Log.e("TEST", "Interprétation DLE EOT 4:");
        Log.e("TEST", "  Bit 0 (couvercle): " + ((status & 0x01) != 0 ? "OUVERT" : "FERMÉ"));
        Log.e("TEST", "  Bit 2 (papier): " + ((status & 0x04) != 0 ? "FAIBLE" : "OK"));
        Log.e("TEST", "  Bit 5 (papier): " + ((status & 0x20) != 0 ? "VIDE" : "OK"));
    }

    private String bytesToHex(int[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }

    public String getPrinterPaperStatus() {
        try{
            byte[] command = new byte[]{0x10, 0x04, 0x04};
            btOutputStream.write(command);
            btOutputStream.flush();
            Thread.sleep(300);
            int status = btInputStream.read();
            return String.valueOf(status);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    public String checkPaperStatus() {
        try {
            // 1. Envoyer la commande d'activation ASB
            byte[] cmd = {0x1D, 0x61, (byte) 0xFF};
            btOutputStream.write(cmd);
            btOutputStream.flush();

            // 2. Attendre un peu plus longtemps pour la réponse
            Thread.sleep(500);

            // 3. Lire tout ce qui est disponible dans le buffer
            int available = btInputStream.available();
            if (available > 0) {
                byte[] buffer = new byte[available];
                btInputStream.read(buffer);

                // On convertit le buffer en une chaîne lisible (Hex)
                StringBuilder sb = new StringBuilder();
                for (byte b : buffer) {
                    sb.append(String.format("%02X ", b));
                }
                return "Status: " + sb.toString();
            } else {
                return "0 (Aucune réponse)";
            }
        } catch (Exception e) {
            return "Erreur: " + e.getMessage();
        }
    }


    private int readStatusByte(int timeoutMs) throws IOException, InterruptedException {
        long start = System.currentTimeMillis();

        // petite attente initiale (important en Bluetooth)
        Thread.sleep(50);

        while (System.currentTimeMillis() - start < timeoutMs) {

            int available = btInputStream.available();
            Log.e("AVAILABLE", "STATUS: "+String.valueOf(available) );
            if (available > 0) {
                byte[] buffer = new byte[available];
                int read = btInputStream.read(buffer);
                if (read > 0) {
                    int value = buffer[0] & 0xFF;

                    Log.d("Printama", "Raw bytes: " + bytesToHex(buffer, read));

                    return value; // on prend le premier byte
                }
            }

            Thread.sleep(20);
        }

        return -1;
    }

    private String bytesToHex(byte[] bytes, int length) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            sb.append(String.format("%02X ", bytes[i]));
        }
        return sb.toString();
    }


    boolean printImage(Bitmap bitmap, int width) {
        return printImage(PA.CENTER, bitmap, width);
    }

    boolean printImage(int alignment, Bitmap bitmap, int width) {
        Bitmap workBitmap = bitmap;

        // Auto-trim borders for full-width prints to remove internal whitespace
        if (width == PW.FULL_WIDTH && workBitmap != null) {
            Bitmap trimmed = trimWhitespace(workBitmap, 245); // higher threshold trims more
            if (trimmed != null) {
                workBitmap = trimmed;
            }
        }

        Bitmap scaledBitmap = scaledBitmap(workBitmap, width);

        if (scaledBitmap != null) {
            int marginLeft = 0;
            if (width == PW.FULL_WIDTH || alignment == PA.LEFT) {
                marginLeft = 0;
            } else if (alignment == PA.CENTER) {
                marginLeft = (getPrinterWidth() - scaledBitmap.getWidth()) / 2;
            } else if (alignment == PA.RIGHT) {
                marginLeft = getPrinterWidth() - scaledBitmap.getWidth();
            }

            // Reset hardware margins and set printable area for full-width
            if (width == PW.FULL_WIDTH) {
                int printerWidthDots = getPrinterWidth();
                printUnicode(new byte[]{0x1B, 0x24, 0x00, 0x00}); // ESC $: absolute position = 0
                printUnicode(new byte[]{0x1D, 0x4C, 0x00, 0x00}); // GS L: left margin = 0
                printUnicode(new byte[]{0x1D, 0x57, (byte) (printerWidthDots & 0xFF), (byte) ((printerWidthDots >> 8) & 0xFF)}); // GS W
            }

            // Calculate correct width in bytes for printer command
            int widthBytes = getLineWidth();
            int lines = scaledBitmap.getHeight();

            // Remove top margin for better space utilization
            byte[] command = autoGrayScale(scaledBitmap, marginLeft, 0);

            // Fix: Correct printer command header for both printer sizes
            System.arraycopy(new byte[]{
                    0x1D, 0x76, 0x30, 0x00,
                    (byte) (widthBytes & 0xff),  // Width in bytes (low byte)
                    (byte) ((widthBytes >> 8) & 0xff),  // Width in bytes (high byte)
                    (byte) (lines & 0xff),  // Height (low byte)
                    (byte) ((lines >> 8) & 0xff)  // Height (high byte)
            }, 0, command, 0, HEAD);

            // Convert GS v 0 command to ESC * commands for better compatibility
            byte[][] commandLines = convertGSv0ToEscAsterisk(command);

            // Print each line of the image
            boolean success = true;
            for (byte[] line : commandLines) {
                if (!printUnicode(line)) {
                    success = false;
                    break;
                }
            }

            // Add a command to reset the printer state after image printing
            if (success) {
                printUnicode(new byte[]{0x1B, 0x40}); // ESC @ command to initialize printer
            }

            return success;
        } else {
            return false;
        }
    }

    private byte[] autoGrayScale(Bitmap bm, int bitMarginLeft, int bitMarginTop) {
        byte[] result;
        int n = bm.getHeight() + bitMarginTop;
        int offset = HEAD;
        int lineWidth = getLineWidth();
        result = new byte[n * lineWidth + offset];
        Arrays.fill(result, (byte) 0x00); // Initialize all bytes to zero

        // Create a temporary array to hold grayscale values
        int[][] grayPixels = new int[bm.getHeight()][bm.getWidth()];

        // First pass: Convert to grayscale and store values
        for (int y = 0; y < bm.getHeight(); y++) {
            for (int x = 0; x < bm.getWidth(); x++) {
                int color = bm.getPixel(x, y);
                int alpha = Color.alpha(color);
                if (alpha < 128) {
                    grayPixels[y][x] = 255; // Treat transparent as white
                    continue;
                }

                // Convert to grayscale using luminance formula
                int red = Color.red(color);
                int green = Color.green(color);
                int blue = Color.blue(color);
                int gray = (int) (red * 0.299 + green * 0.587 + blue * 0.114);
                gray = Math.max(0, Math.min(255, gray));
                grayPixels[y][x] = gray;
            }
        }

        // Second pass: Apply dithering and convert to binary
        for (int y = 0; y < bm.getHeight(); y++) {
            for (int x = 0; x < bm.getWidth(); x++) {
                // compute target bit position once
                int bitX = bitMarginLeft + x;

                // Skip pixels that would be outside the printer width, including negative shift
                if (bitX < 0 || bitX >= getPrinterWidth()) {
                    continue;
                }

                int oldPixel = Math.max(0, Math.min(255, grayPixels[y][x]));
                int newPixel = oldPixel > 128 ? 255 : 0;  // 128 threshold

                if (newPixel == 0) {
                    int byteX = bitX / 8;
                    int byteY = y + bitMarginTop;
                    int bitOffset = 7 - (bitX % 8);

                    // Ensure we don't exceed the line width
                    if (byteX < lineWidth && byteX >= 0) {
                        int byteIndex = offset + byteY * lineWidth + byteX;
                        if (byteIndex < result.length && byteIndex >= 0) {
                            result[byteIndex] |= (1 << bitOffset);
                        }
                    }
                }

                // Apply error diffusion with proper bounds checking
                int error = oldPixel - newPixel;

                // Distribute error to neighboring pixels using Floyd-Steinberg dithering
                if (x + 1 < bm.getWidth()) {
                    grayPixels[y][x + 1] = Math.max(0, Math.min(255,
                            grayPixels[y][x + 1] + (error * 7 / 16)));
                }

                if (y + 1 < bm.getHeight()) {
                    if (x > 0) {
                        grayPixels[y + 1][x - 1] = Math.max(0, Math.min(255,
                                grayPixels[y + 1][x - 1] + (error * 3 / 16)));
                    }

                    grayPixels[y + 1][x] = Math.max(0, Math.min(255,
                            grayPixels[y + 1][x] + (error * 5 / 16)));

                    if (x + 1 < bm.getWidth()) {
                        grayPixels[y + 1][x + 1] = Math.max(0, Math.min(255,
                                grayPixels[y + 1][x + 1] + (error * 1 / 16)));
                    }
                }
            }
        }

        // Ensure the end of the image data is properly terminated
        // Add padding zeros at the end to ensure clean termination
        for (int i = offset + n * lineWidth - lineWidth; i < result.length; i++) {
            result[i] = 0x00;
        }

        return result;
    }

    private Bitmap scaledBitmap(Bitmap bitmap, int width) {
        try {
            int printerWidth = getPrinterWidth();
            int desiredWidth = getDesiredWidth(bitmap, width, printerWidth);

            float scale = (float) desiredWidth / (float) bitmap.getWidth();
            int height = (int) (bitmap.getHeight() * scale);
            return Bitmap.createScaledBitmap(bitmap, desiredWidth, height, true);
        } catch (NullPointerException e) {
            Log.e(TAG, "Maybe resource is vector or mipmap?");
            return null;
        }
    }

    private static int getDesiredWidth(Bitmap bitmap, int width, int printerWidth) {
        int desiredWidth;

        if (width == PW.FULL_WIDTH || width >= printerWidth) {
            // Always use full printer width when requested or when width exceeds printer capacity
            desiredWidth = printerWidth;
        } else if (width > 0) {
            // Use specified width if it's positive and within printer limits
            desiredWidth = width;
        } else {
            desiredWidth = switch (width) {
                case PW.QUARTER_WIDTH -> (int) (printerWidth * 0.25); // 1/4 width
                case PW.HALF_WIDTH -> (int) (printerWidth * 0.5); // 1/2 width
                case PW.THREE_QUARTERS_WIDTH -> (int) (printerWidth * 0.75); // 3/4 width
                case PW.ONE_THIRD_WIDTH -> (int) (printerWidth * 0.333); // 1/3 width
                case PW.TWO_THIRD_WIDTH -> (int) (printerWidth * 0.667); // 2/3 width
                default -> Math.min(bitmap.getWidth(), printerWidth); // original width
            };
        }
        return desiredWidth;
    }

    void feedPaper() {
        addNewLine();
        addNewLine();
        addNewLine();
        addNewLine();
    }

    public int getMaxChar() {
        return is3InchPrinter ? MAX_CHAR_3_INCH : MAX_CHAR_2_INCH;
    }

    public int getMaxMP5Printer(){
        return _isMP5Printer()?MAX_CHAR_MP5_INCH:MAX_CHAR_2_INCH;
    }

    boolean _isMP5Printer(){
        @SuppressLint("MissingPermission") String name = printer.getName();
        return name.equals("MP5 Printer");

    }

    private static class ConnectAsyncTask extends AsyncTask<BluetoothDevice, Void, BluetoothSocket> {
        private final ConnectionListener listener;

        private ConnectAsyncTask(ConnectionListener listener) {
            this.listener = listener;
        }

        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
        @Override
        protected BluetoothSocket doInBackground(BluetoothDevice... bluetoothDevices) {
            BluetoothDevice device = bluetoothDevices[0];
            UUID uuid;
            if (device != null) {
                ParcelUuid[] uuids = device.getUuids();
                uuid = (uuids != null && uuids.length > 0) ? uuids[0].getUuid() : UUID.randomUUID();
            } else {
                return null;
            }
            BluetoothSocket socket = null;
            boolean connected = true;
            try {
                socket = device.createRfcommSocketToServiceRecord(uuid);
                socket.connect();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e2) {
                connected = false;
            }
            return connected ? socket : null;
        }

        @Override
        protected void onPostExecute(BluetoothSocket bluetoothSocket) {
            if (listener != null) {
                if (bluetoothSocket != null) listener.onConnected(bluetoothSocket);
                else listener.onFailed();
            }
        }

        private interface ConnectionListener {
            void onConnected(BluetoothSocket socket);

            void onFailed();
        }
    }

    public interface PrinterConnected {
        void onConnected();
    }

    public interface PrinterConnectFailed {
        void onFailed();
    }

    public boolean isIs3InchPrinter() {
        return is3InchPrinter;
    }

    public void isIs3InchPrinter(boolean is3inches) {
        is3InchPrinter = is3inches;
    }

    private int getLineWidth() {
        return is3InchPrinter ? WIDTH_3_INCH : WIDTH_2_INCH;
    }

    private int getPrinterWidth() {
        return is3InchPrinter ? PRINTER_WIDTH_3_INCH : PRINTER_WIDTH_2_INCH;
    }

    // escpos lib

    public static final byte LF = 0x0A;
    public static final byte[] LINE_SPACING_24 = {0x1b, 0x33, 0x18};
    public static final byte[] LINE_SPACING_30 = {0x1b, 0x33, 0x1e};

    public static byte[][] convertGSv0ToEscAsterisk(byte[] bytes) {
        int
                xL = bytes[4] & 0xFF,
                xH = bytes[5] & 0xFF,
                yL = bytes[6] & 0xFF,
                yH = bytes[7] & 0xFF,
                bytesByLine = xH * 256 + xL,
                dotsByLine = bytesByLine * 8,
                nH = dotsByLine / 256,
                nL = dotsByLine % 256,
                imageHeight = yH * 256 + yL,
                imageLineHeightCount = (int) Math.ceil((double) imageHeight / 24.0),
                imageBytesSize = 6 + bytesByLine * 24;

        byte[][] returnedBytes = new byte[imageLineHeightCount + 2][];
        returnedBytes[0] = LINE_SPACING_24;
        for (int i = 0; i < imageLineHeightCount; ++i) {
            int pxBaseRow = i * 24;
            byte[] imageBytes = new byte[imageBytesSize];
            imageBytes[0] = 0x1B;
            imageBytes[1] = 0x2A;
            imageBytes[2] = 0x21;
            imageBytes[3] = (byte) nL;
            imageBytes[4] = (byte) nH;
            for (int j = 5; j < imageBytes.length - 1; ++j) { // Fixed: -1 to avoid overwriting LF
                int
                        imgByte = j - 5,
                        byteRow = imgByte % 3,
                        pxColumn = imgByte / 3,
                        bitColumn = 1 << (7 - pxColumn % 8),
                        pxRow = pxBaseRow + byteRow * 8;
                for (int k = 0; k < 8; ++k) {
                    int indexBytes = bytesByLine * (pxRow + k) + pxColumn / 8 + 8;

                    if (indexBytes >= bytes.length) {
                        break;
                    }

                    boolean isBlack = (bytes[indexBytes] & bitColumn) == bitColumn;
                    if (isBlack) {
                        imageBytes[j] |= 1 << (7 - k);
                    }
                }
            }
            imageBytes[imageBytes.length - 1] = LF;
            returnedBytes[i + 1] = imageBytes;
        }
        returnedBytes[returnedBytes.length - 1] = LINE_SPACING_30;
        return returnedBytes;
    }


/**
 * Crops near-white (and transparent) borders from a bitmap.
 * threshold: 0..255. Pixels with grayscale > threshold are treated as background.
 */
private Bitmap trimWhitespace(Bitmap src, int threshold) {
    try {
        if (src == null) return null;
        int w = src.getWidth();
        int h = src.getHeight();
        if (w <= 2 || h <= 2) return src;

        int left = 0, right = w - 1, top = 0, bottom = h - 1;

        // scan top
        topLoop:
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                int c = src.getPixel(x, y);
                int a = android.graphics.Color.alpha(c);
                if (a < 128) continue; // transparent = background
                int gray = (int) (android.graphics.Color.red(c) * 0.299
                        + android.graphics.Color.green(c) * 0.587
                        + android.graphics.Color.blue(c) * 0.114);
                if (gray <= threshold) { top = y; break topLoop; }
            }
        }

        // scan bottom
        bottomLoop:
        for (int y = h - 1; y >= top; y--) {
            for (int x = 0; x < w; x++) {
                int c = src.getPixel(x, y);
                int a = android.graphics.Color.alpha(c);
                if (a < 128) continue;
                int gray = (int) (android.graphics.Color.red(c) * 0.299
                        + android.graphics.Color.green(c) * 0.587
                        + android.graphics.Color.blue(c) * 0.114);
                if (gray <= threshold) { bottom = y; break bottomLoop; }
            }
        }

        // scan left
        leftLoop:
        for (int x = 0; x < w; x++) {
            for (int y = top; y <= bottom; y++) {
                int c = src.getPixel(x, y);
                int a = android.graphics.Color.alpha(c);
                if (a < 128) continue;
                int gray = (int) (android.graphics.Color.red(c) * 0.299
                        + android.graphics.Color.green(c) * 0.587
                        + android.graphics.Color.blue(c) * 0.114);
                if (gray <= threshold) { left = x; break leftLoop; }
            }
        }

        // scan right
        rightLoop:
        for (int x = w - 1; x >= left; x--) {
            for (int y = top; y <= bottom; y++) {
                int c = src.getPixel(x, y);
                int a = android.graphics.Color.alpha(c);
                if (a < 128) continue;
                int gray = (int) (android.graphics.Color.red(c) * 0.299
                        + android.graphics.Color.green(c) * 0.587
                        + android.graphics.Color.blue(c) * 0.114);
                if (gray <= threshold) { right = x; break rightLoop; }
            }
        }

        int newW = Math.max(1, right - left + 1);
        int newH = Math.max(1, bottom - top + 1);
        if (newW <= 0 || newH <= 0 || (newW == w && newH == h)) {
            return src; // nothing to trim or degenerate
        }
        return Bitmap.createBitmap(src, left, top, newW, newH);
    } catch (Throwable t) {
        return src; // be safe on any error
    }
}

}
