import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import serial
import serial.tools.list_ports
from PIL import Image, ImageTk
import threading
import time

# Printer constants
FONT_TYPE_NORMAL = 0x00
FONT_TYPE_ITALIC = 0x02
FONT_TYPE_BOLD = 0x08
FONT_TYPE_DOUBLE_HEIGHT = 0x10
FONT_TYPE_DOUBLE_WIDTH = 0x20
FONT_TYPE_REVERSE = 0x40
FONT_TYPE_UNDERLINE = 0x80

PRINTER_STATE_DEFAULT = 0x00
PRINTER_STATE_PAPEROUT = 0x04
PRINTER_STATE_OVERHEAT = 0x08

ALIGN_LEFT = 0
ALIGN_CENTER = 1
ALIGN_RIGHT = 2

FONT_SIZE_S_LARGE = 2
FONT_SIZE_S_SMALL = 1

PICTURE_TYPE_NORMAL = 0

BARCODE_SYS_UPCA = 0
BARCODE_SYS_UPCE = 1
BARCODE_SYS_EAN13 = 2
BARCODE_SYS_EAN8 = 3
BARCODE_SYS_CODE39 = 4
BARCODE_SYS_ITF25 = 5
BARCODE_SYS_CODABAR = 6
BARCODE_SYS_CODE93 = 7
BARCODE_SYS_CODE128 = 8

# Printer Head Types
MM58_FTP628MCL101 = 0
MM80_FTP638MCL101 = 1
MM58_LTP02_245_13 = 2
MM58_LTP02_245_C1 = 3
MM58_CUTTER_384 = 4
MM80_CUTTER_576 = 5
MM58_M23_DH_H69 = 6
MM58_M23_SX = 7
MM80_SEIKO_LTPV345 = 8
MM58_PT486HLV = 9

class SerialSettingsDialog:
    def __init__(self, parent):
        self.parent = parent
        self.dialog = None
        self.result = None
        
    def setup_ui(self):
        self.dialog = tk.Toplevel(self.parent)
        self.dialog.title("Serial Port Settings")
        self.dialog.geometry("400x350")
        self.dialog.resizable(False, False)
        self.dialog.transient(self.parent)
        self.dialog.grab_set()
        
        # Make dialog modal and prevent interaction with main window
        self.dialog.focus_set()
        
        main_frame = ttk.Frame(self.dialog, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Port selection
        ttk.Label(main_frame, text="Serial Port:").grid(row=0, column=0, sticky=tk.W, pady=5)
        self.port_var = tk.StringVar()
        self.port_combo = ttk.Combobox(main_frame, textvariable=self.port_var, state="readonly", width=20)
        self.port_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
        self.refresh_ports()
        
        # Baud rate
        ttk.Label(main_frame, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.baud_var = tk.StringVar(value="115200")
        baud_combo = ttk.Combobox(main_frame, textvariable=self.baud_var, values=[
            "9600", "19200", "38400", "57600", "115200", "256000", "375000"
        ], state="readonly", width=20)
        baud_combo.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5)
        
        # Data bits
        ttk.Label(main_frame, text="Data Bits:").grid(row=2, column=0, sticky=tk.W, pady=5)
        self.data_bits_var = tk.StringVar(value="8")
        data_bits_combo = ttk.Combobox(main_frame, textvariable=self.data_bits_var, 
                                      values=["5", "6", "7", "8"], state="readonly", width=20)
        data_bits_combo.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5)
        
        # Stop bits
        ttk.Label(main_frame, text="Stop Bits:").grid(row=3, column=0, sticky=tk.W, pady=5)
        self.stop_bits_var = tk.StringVar(value="1")
        stop_bits_combo = ttk.Combobox(main_frame, textvariable=self.stop_bits_var, 
                                      values=["1", "1.5", "2"], state="readonly", width=20)
        stop_bits_combo.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5)
        
        # Parity
        ttk.Label(main_frame, text="Parity:").grid(row=4, column=0, sticky=tk.W, pady=5)
        self.parity_var = tk.StringVar(value="N")
        parity_combo = ttk.Combobox(main_frame, textvariable=self.parity_var, 
                                   values=["N", "E", "O", "M", "S"], state="readonly", width=20)
        parity_combo.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=5)
        
        # Flow control
        ttk.Label(main_frame, text="Flow Control:").grid(row=5, column=0, sticky=tk.W, pady=5)
        self.flow_control_var = tk.StringVar(value="None")
        flow_combo = ttk.Combobox(main_frame, textvariable=self.flow_control_var, 
                                 values=["None", "XON/XOFF", "RTS/CTS"], state="readonly", width=20)
        flow_combo.grid(row=5, column=1, sticky=(tk.W, tk.E), pady=5)
        
        # Buttons
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=6, column=0, columnspan=2, pady=20)
        
        ttk.Button(button_frame, text="Refresh Ports", command=self.refresh_ports).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="OK", command=self.ok_clicked).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Cancel", command=self.cancel_clicked).pack(side=tk.LEFT, padx=5)
        
        main_frame.columnconfigure(1, weight=1)
        
        # Center the dialog
        self.dialog.update_idletasks()
        x = (self.dialog.winfo_screenwidth() // 2) - (self.dialog.winfo_width() // 2)
        y = (self.dialog.winfo_screenheight() // 2) - (self.dialog.winfo_height() // 2)
        self.dialog.geometry(f"+{x}+{y}")
        
        # Bind Enter key to OK and Escape to Cancel
        self.dialog.bind('<Return>', lambda e: self.ok_clicked())
        self.dialog.bind('<Escape>', lambda e: self.cancel_clicked())
        
    def refresh_ports(self):
        try:
            ports = [port.device for port in serial.tools.list_ports.comports()]
            self.port_combo['values'] = ports
            if ports:
                self.port_var.set(ports[0])
            else:
                self.port_var.set("")
                messagebox.showwarning("No Ports", "No serial ports found!")
        except Exception as e:
            messagebox.showerror("Error", f"Failed to refresh ports: {str(e)}")
    
    def ok_clicked(self):
        if not self.port_var.get():
            messagebox.showwarning("Warning", "Please select a serial port!")
            return
        self.result = self.get_settings()
        self.dialog.destroy()
    
    def cancel_clicked(self):
        self.result = None
        self.dialog.destroy()
    
    def show(self):
        self.setup_ui()
        self.dialog.wait_window()
        return self.result
    
    def get_settings(self):
        return {
            'port': self.port_var.get(),
            'baudrate': int(self.baud_var.get()),
            'bytesize': int(self.data_bits_var.get()),
            'stopbits': float(self.stop_bits_var.get()),
            'parity': self.parity_var.get(),
            'flow_control': self.flow_control_var.get()
        }

class PrinterEVKApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Printer EVK - Python Tkinter")
        self.root.geometry("1000x800")  # Increased height to accommodate status pane
        
        self.serial_port = None
        self.current_image = None
        self.scale_factor = 1.0
        self.is_reading = False
        
        # Statistics counters
        self.bytes_sent = 0
        self.bytes_received = 0
        self.commands_sent = 0
        
        self.setup_ui()
        
    def setup_ui(self):
        # Main notebook (tabs)
        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # Create tabs
        self.setup_general_tab()
        self.setup_barcode_tab()
        self.setup_bitmap_tab()
        self.setup_cutter_tab()
        self.setup_controller_tab()
        
        # Status pane - NEW: Shows bytes sent/received in hex
        status_pane = ttk.LabelFrame(self.root, text="Communication Status", padding="5")
        status_pane.pack(fill=tk.X, padx=10, pady=5)
        
        # Create a frame for status information
        status_frame = ttk.Frame(status_pane)
        status_frame.pack(fill=tk.X)
        
        # Bytes sent
        sent_frame = ttk.Frame(status_frame)
        sent_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Label(sent_frame, text="Bytes Sent:", font=('TkDefaultFont', 9, 'bold')).pack(anchor=tk.W)
        self.sent_var = tk.StringVar(value="0 bytes")
        ttk.Label(sent_frame, textvariable=self.sent_var, font=('TkDefaultFont', 9)).pack(anchor=tk.W)
        
        # Bytes received
        received_frame = ttk.Frame(status_frame)
        received_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Label(received_frame, text="Bytes Received:", font=('TkDefaultFont', 9, 'bold')).pack(anchor=tk.W)
        self.received_var = tk.StringVar(value="0 bytes")
        ttk.Label(received_frame, textvariable=self.received_var, font=('TkDefaultFont', 9)).pack(anchor=tk.W)
        
        # Commands sent
        commands_frame = ttk.Frame(status_frame)
        commands_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Label(commands_frame, text="Commands Sent:", font=('TkDefaultFont', 9, 'bold')).pack(anchor=tk.W)
        self.commands_var = tk.StringVar(value="0 commands")
        ttk.Label(commands_frame, textvariable=self.commands_var, font=('TkDefaultFont', 9)).pack(anchor=tk.W)
        
        # Last command
        last_cmd_frame = ttk.Frame(status_frame)
        last_cmd_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        ttk.Label(last_cmd_frame, text="Last Command:", font=('TkDefaultFont', 9, 'bold')).pack(anchor=tk.W)
        self.last_cmd_var = tk.StringVar(value="None")
        ttk.Label(last_cmd_frame, textvariable=self.last_cmd_var, font=('TkDefaultFont', 8)).pack(anchor=tk.W)
        
        # Clear stats button
        ttk.Button(status_frame, text="Clear Stats", command=self.clear_stats).pack(side=tk.RIGHT, padx=5)
        
        # Output text area
        output_frame = ttk.LabelFrame(self.root, text="Output Console", padding="5")
        output_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        # Create a frame for output controls
        output_controls = ttk.Frame(output_frame)
        output_controls.pack(fill=tk.X, pady=(0, 5))
        
        ttk.Button(output_controls, text="Clear Console", command=self.clear_console).pack(side=tk.LEFT)
        ttk.Button(output_controls, text="Show Hex", command=self.toggle_hex_display).pack(side=tk.LEFT, padx=5)
        
        self.show_hex = tk.BooleanVar(value=False)
        ttk.Checkbutton(output_controls, text="Auto-scroll", variable=self.show_hex).pack(side=tk.LEFT, padx=5)
        
        self.output_text = scrolledtext.ScrolledText(output_frame, height=12, width=80, font=('Consolas', 9))
        self.output_text.pack(fill=tk.BOTH, expand=True)
        
        # Status bar
        self.status_var = tk.StringVar(value="Ready - Not Connected")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, side=tk.BOTTOM)
        
        # Menu bar
        self.setup_menubar()
        
    def setup_menubar(self):
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)
        
        # Serial menu
        serial_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Serial", menu=serial_menu)
        serial_menu.add_command(label="Connect", command=self.open_serial_port)
        serial_menu.add_command(label="Disconnect", command=self.close_serial_port)
        serial_menu.add_separator()
        serial_menu.add_command(label="Quit", command=self.root.quit)
        
        # Settings menu
        settings_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Settings", menu=settings_menu)
        settings_menu.add_command(label="Configure", command=self.show_settings)
        
        # View menu
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="View", menu=view_menu)
        view_menu.add_command(label="Clear Statistics", command=self.clear_stats)
        view_menu.add_command(label="Show Communication Log", command=self.show_comm_log)
        
        # Help menu
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Help", menu=help_menu)
        help_menu.add_command(label="About", command=self.about)
        
    def setup_general_tab(self):
        general_frame = ttk.Frame(self.notebook)
        self.notebook.add(general_frame, text="General")
        
        # Create buttons for general functions
        buttons = [
            ("Quick Test", self.on_quick_test),
            ("Attributes", self.on_attributes),
            ("Language", self.on_language),
            ("Density", self.on_density),
            ("Formatting", self.on_formatting),
            ("Alignment", self.on_alignment),
            ("Reset", self.on_reset),
            ("Clear Buffer", self.on_clear_buffer),
            ("Receipt", self.on_receipt),
            ("Status", self.on_status),
            ("Version", self.on_version),
            ("Power Management", self.on_power_management)
        ]
        
        for i, (text, command) in enumerate(buttons):
            row = i // 3
            col = i % 3
            ttk.Button(general_frame, text=text, command=command).grid(
                row=row, column=col, padx=5, pady=5, sticky="ew"
            )
            general_frame.columnconfigure(col, weight=1)
        
        for i in range((len(buttons) + 2) // 3):
            general_frame.rowconfigure(i, weight=1)
    
    def setup_barcode_tab(self):
        barcode_frame = ttk.Frame(self.notebook)
        self.notebook.add(barcode_frame, text="Bar Codes")
        
        barcodes = [
            ("UPC-A", self.on_barcode_upca),
            ("UPC-E", self.on_barcode_upce),
            ("EAN-13", self.on_barcode_ean13),
            ("EAN-8", self.on_barcode_ean8),
            ("CODE 39", self.on_barcode_code39),
            ("ITF 2 5", self.on_barcode_itf25),
            ("CODA BAR", self.on_barcode_codabar),
            ("CODE 93", self.on_barcode_code93),
            ("CODE 128 A", self.on_barcode_code128a)
        ]
        
        for i, (text, command) in enumerate(barcodes):
            row = i // 3
            col = i % 3
            ttk.Button(barcode_frame, text=text, command=command).grid(
                row=row, column=col, padx=5, pady=5, sticky="ew"
            )
            barcode_frame.columnconfigure(col, weight=1)
        
        # Barcode options
        options_frame = ttk.LabelFrame(barcode_frame, text="Bar Code Options")
        options_frame.grid(row=4, column=0, columnspan=3, sticky="ew", padx=5, pady=10)
        
        ttk.Button(options_frame, text="Bar Code with Text", 
                  command=lambda: self.barcodeTextYesNo(1)).pack(side=tk.LEFT, padx=5)
        ttk.Button(options_frame, text="Bar Code without Text", 
                  command=lambda: self.barcodeTextYesNo(0)).pack(side=tk.LEFT, padx=5)
        
        self.barcode_margin_var = tk.BooleanVar()
        ttk.Checkbutton(options_frame, text="Add Left Margin Formatting",
                       variable=self.barcode_margin_var).pack(side=tk.LEFT, padx=5)
    
    def setup_bitmap_tab(self):
        bitmap_frame = ttk.Frame(self.notebook)
        self.notebook.add(bitmap_frame, text="Bitmaps")
        
        # Image display area
        self.image_frame = ttk.Frame(bitmap_frame)
        self.image_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        self.canvas = tk.Canvas(self.image_frame, bg="white", width=400, height=300)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # Controls
        control_frame = ttk.Frame(bitmap_frame)
        control_frame.pack(fill=tk.X, padx=5, pady=5)
        
        ttk.Button(control_frame, text="Load Image", 
                  command=self.load_image).pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="Print Raster Image", 
                  command=self.print_raster_image).pack(side=tk.LEFT, padx=5)
        ttk.Button(control_frame, text="Line Feed", 
                  command=self.printLF).pack(side=tk.LEFT, padx=5)
    
    def setup_cutter_tab(self):
        cutter_frame = ttk.Frame(self.notebook)
        self.notebook.add(cutter_frame, text="Cutter")
        
        ttk.Button(cutter_frame, text="Full Cut", 
                  command=self.fullCut).pack(padx=5, pady=5, fill=tk.X)
        ttk.Button(cutter_frame, text="Partial Cut", 
                  command=self.partialCut).pack(padx=5, pady=5, fill=tk.X)
        ttk.Button(cutter_frame, text="Receipt with Full Cut", 
                  command=self.on_receipt_full_cut).pack(padx=5, pady=5, fill=tk.X)
        ttk.Button(cutter_frame, text="Receipt with Partial Cut", 
                  command=self.on_receipt_partial_cut).pack(padx=5, pady=5, fill=tk.X)
    
    def setup_controller_tab(self):
        controller_frame = ttk.Frame(self.notebook)
        self.notebook.add(controller_frame, text="Controller")
        
        # UART Settings
        uart_frame = ttk.LabelFrame(controller_frame, text="UART Serial Settings")
        uart_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # Speed settings
        speed_frame = ttk.LabelFrame(uart_frame, text="Serial Speed Setting")
        speed_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.speed_var = tk.StringVar(value="115200")
        speeds = [
            ("9600 bps", "9600"),
            ("19200 bps", "19200"),
            ("38400 bps", "38400"),
            ("57600 bps", "57600"),
            ("115200 bps (*Default)", "115200"),
            ("256000 bps (FW 1.22 and higher)", "256000"),
            ("375000 bps (FW 1.22 and higher)", "375000")
        ]
        
        for text, value in speeds:
            ttk.Radiobutton(speed_frame, text=text, variable=self.speed_var, 
                           value=value).pack(anchor=tk.W)
        
        # Flow control
        flow_frame = ttk.LabelFrame(uart_frame, text="Flow Control")
        flow_frame.pack(fill=tk.X, padx=5, pady=5)
        
        self.flow_var = tk.StringVar(value="XON/XOFF")
        ttk.Radiobutton(flow_frame, text="No Flow Control", 
                       variable=self.flow_var, value="None").pack(anchor=tk.W)
        ttk.Radiobutton(flow_frame, text="XON/XOFF Flow Control (*Default)", 
                       variable=self.flow_var, value="XON/XOFF").pack(anchor=tk.W)
        ttk.Radiobutton(flow_frame, text="Hardware Flow Control", 
                       variable=self.flow_var, value="RTS/CTS").pack(anchor=tk.W)
        
        ttk.Button(uart_frame, text="Apply", 
                  command=self.apply_com_settings).pack(padx=5, pady=10)
        
        # Print Head Selection
        head_frame = ttk.LabelFrame(controller_frame, text="Print Head Type Selection")
        head_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        heads = [
            ("58mm FTP628MCL101 Type", MM58_FTP628MCL101),
            ("80mm FTP638MCL101 Type", MM80_FTP638MCL101),
            ("58mm LTP02-245-13 Type", MM58_LTP02_245_13),
            ("58mm LTP02-245-C1 Type", MM58_LTP02_245_C1),
            ("58mm 384 dots Cutter Type", MM58_CUTTER_384),
            ("80mm 576 dots Cutter Type", MM80_CUTTER_576),
            ("58mm M23-DH-H69 Type", MM58_M23_DH_H69),
            ("58mm M23-SX Type", MM58_M23_SX),
            ("80mm Seiko LTPV345 Type", MM80_SEIKO_LTPV345),
            ("58mm PT486HLV Type", MM58_PT486HLV)
        ]
        
        for i, (text, head_type) in enumerate(heads):
            row = i // 2
            col = i % 2
            ttk.Button(head_frame, text=text, 
                      command=lambda ht=head_type: self.setPrinterHead(ht)).grid(
                row=row, column=col, padx=5, pady=2, sticky="ew"
            )
            head_frame.columnconfigure(col, weight=1)
    
    def show_settings(self):
        settings_dialog = SerialSettingsDialog(self.root)
        settings = settings_dialog.show()
        if settings:
            self.output_text.insert(tk.END, f"Settings configured: {settings}\n")
    
    def open_serial_port(self):
        settings_dialog = SerialSettingsDialog(self.root)
        settings = settings_dialog.show()
        
        if not settings or not settings['port']:
            return
        
        try:
            # Close existing connection if any
            if self.serial_port and self.serial_port.is_open:
                self.serial_port.close()
            
            # Create new serial connection
            self.serial_port = serial.Serial(
                port=settings['port'],
                baudrate=settings['baudrate'],
                bytesize=settings['bytesize'],
                stopbits=settings['stopbits'],
                parity=settings['parity'],
                timeout=1,
                write_timeout=1
            )
            
            self.status_var.set(f"Connected to {settings['port']}")
            self.output_text.insert(tk.END, f"Connected to {settings['port']} at {settings['baudrate']} baud\n")
            
            # Reset statistics when connecting
            self.clear_stats()
            
            # Start reading data
            self.is_reading = True
            self.start_serial_read()
            
        except Exception as e:
            messagebox.showerror("Error", f"Failed to open serial port: {str(e)}")
            self.status_var.set("Connection failed")
    
    def close_serial_port(self):
        self.is_reading = False
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.serial_port = None
        self.status_var.set("Disconnected")
        self.output_text.insert(tk.END, "Disconnected from serial port\n")
    
    def about(self):
        messagebox.showinfo("About", 
            "Printer EVK - Python Tkinter Version\n"
            "Norden Logic Printer Evaluation Kit\n"
            "Featuring NL02x printer controllers\n"
            "(c) Norden Logic Oy 2019-2025")
    
    # NEW: Statistics management methods
    def clear_stats(self):
        """Clear all communication statistics"""
        self.bytes_sent = 0
        self.bytes_received = 0
        self.commands_sent = 0
        self.update_status_display()
        self.output_text.insert(tk.END, "Communication statistics cleared\n")
    
    def update_status_display(self):
        """Update the status pane with current statistics"""
        self.sent_var.set(f"{self.bytes_sent} bytes")
        self.received_var.set(f"{self.bytes_received} bytes")
        self.commands_var.set(f"{self.commands_sent} commands")
    
    def clear_console(self):
        """Clear the output console"""
        self.output_text.delete(1.0, tk.END)
    
    def toggle_hex_display(self):
        """Toggle between hex and text display (placeholder)"""
        self.output_text.insert(tk.END, "Hex display toggle - feature coming soon\n")
    
    def show_comm_log(self):
        """Show detailed communication log"""
        log_window = tk.Toplevel(self.root)
        log_window.title("Communication Log")
        log_window.geometry("600x400")
        
        log_text = scrolledtext.ScrolledText(log_window, width=80, height=20, font=('Consolas', 9))
        log_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        log_text.insert(tk.END, "=== Communication Statistics ===\n")
        log_text.insert(tk.END, f"Total bytes sent: {self.bytes_sent}\n")
        log_text.insert(tk.END, f"Total bytes received: {self.bytes_received}\n")
        log_text.insert(tk.END, f"Total commands sent: {self.commands_sent}\n")
        log_text.insert(tk.END, "================================\n")
        
        log_text.config(state=tk.DISABLED)
    
    # Enhanced serial communication methods with statistics tracking
    def Write(self, data):
        if not self.serial_port or not self.serial_port.is_open:
            messagebox.showwarning("Warning", "Serial port not connected!")
            return False
        
        try:
            if isinstance(data, (bytes, bytearray)):
                written = self.serial_port.write(data)
                # Update statistics
                self.bytes_sent += len(data)
                self.commands_sent += 1
                
                # Display hex of sent data
                hex_data = ' '.join(f'{b:02X}' for b in data)
                self.output_text.insert(tk.END, f"Sent ({len(data)} bytes): {hex_data}\n")
                self.last_cmd_var.set(f"{len(data)} bytes: {hex_data[:50]}{'...' if len(hex_data) > 50 else ''}")
                
            else:
                encoded_data = data.encode('latin-1')
                written = self.serial_port.write(encoded_data)
                # Update statistics
                self.bytes_sent += len(encoded_data)
                self.commands_sent += 1
                
                # Display both text and hex
                hex_data = ' '.join(f'{b:02X}' for b in encoded_data)
                self.output_text.insert(tk.END, f"Sent ({len(encoded_data)} bytes): '{data}' | HEX: {hex_data}\n")
                self.last_cmd_var.set(f"Text: {data[:30]}{'...' if len(data) > 30 else ''}")
            
            self.serial_port.flush()  # Ensure data is sent immediately
            
            # Update status display
            self.update_status_display()
            self.output_text.see(tk.END)
            
            return True
        except Exception as e:
            messagebox.showerror("Error", f"Write failed: {str(e)}")
            return False
    
    def read_data(self):
        if not self.is_reading:
            return
            
        if self.serial_port and self.serial_port.is_open:
            try:
                if self.serial_port.in_waiting > 0:
                    data = self.serial_port.read(self.serial_port.in_waiting)
                    if data:
                        # Update statistics
                        self.bytes_received += len(data)
                        
                        # Display received data in both text and hex
                        try:
                            decoded_data = data.decode('latin-1', errors='replace')
                            hex_data = ' '.join(f'{b:02X}' for b in data)
                            self.output_text.insert(tk.END, f"Received ({len(data)} bytes): '{decoded_data}' | HEX: {hex_data}\n")
                        except:
                            hex_data = ' '.join(f'{b:02X}' for b in data)
                            self.output_text.insert(tk.END, f"Received ({len(data)} bytes) HEX: {hex_data}\n")
                        
                        # Update status display
                        self.update_status_display()
                        self.output_text.see(tk.END)
            except Exception as e:
                self.output_text.insert(tk.END, f"Read error: {e}\n")
        
        # Schedule next read if still connected
        if self.is_reading:
            self.root.after(100, self.read_data)
    
    def start_serial_read(self):
        self.read_data()
    
    # Printer command methods - FIXED
    def setComMode(self, ucComSet, ucFlowSet):
        buf = bytes([0x1D, 0x28, 0x00, ucComSet, ucFlowSet])
        return self.Write(buf)
    
    def setPrinterHead(self, ucHeadIndex):
        buf = bytes([0x1B, 0x5A, ucHeadIndex])
        success = self.Write(buf)
        if success:
            self.output_text.insert(tk.END, f"Setting print head to type {ucHeadIndex}\n")
        return success
    
    def printerSoftInit(self):
        buf = bytes([0x1B, 0x40])
        return self.Write(buf)
    
    def printLF(self):
        buf = bytes([0x0A])
        return self.Write(buf)
    
    def printCR(self):
        buf = bytes([0x0D])
        return self.Write(buf)
    
    def printStr(self, pStr):
        return self.Write(pStr + '\n')  # Add newline for better formatting
    
    def printAndFeedNDotLine(self, ucDotLineNum):
        buf = bytes([0x1B, 0x4A, ucDotLineNum])
        return self.Write(buf)
    
    def fontTypeSet(self, ucFontType):
        buf = bytes([0x1B, 0x21, ucFontType])
        return self.Write(buf)
    
    def horizontalAlign(self, align):
        buf = bytes([0x1B, 0x61, align])
        return self.Write(buf)
    
    def lineSpacingSet(self, ucDotLineNum):
        buf = bytes([0x1B, 0x33, ucDotLineNum])
        return self.Write(buf)
    
    def lineSpacingDefaults(self):
        buf = bytes([0x1B, 0x32])
        return self.Write(buf)
    
    def setLeftMargin(self, ucDots):
        buf = bytes([0x1B, 0x6C, ucDots])
        return self.Write(buf)
    
    def setRightMargin(self, ucDots):
        buf = bytes([0x1B, 0x51, ucDots])
        return self.Write(buf)
    
    def printerState(self):
        buf = bytes([0x10, 0x04])
        return self.Write(buf)
    
    def setFontGrayScale(self, ucScale):
        buf = bytes([0x1B, 0x6D, ucScale])
        return self.Write(buf)
    
    def setFontMode(self, ucFont):
        buf = bytes([0x1B, 0x4D, ucFont])
        return self.Write(buf)
    
    def barcodeHeightSet(self, ucHeight):
        buf = bytes([0x1D, 0x68, ucHeight])
        return self.Write(buf)
    
    def barcodeWidthSet(self, ucWidth):
        buf = bytes([0x1D, 0x77, ucWidth])
        return self.Write(buf)
    
    def barcodeTextYesNo(self, ucYesNo):
        buf = bytes([0x1D, 0x48, ucYesNo])
        return self.Write(buf)
    
    def barcodePrint(self, ucBarcodeSys, pucCodeBuf, ucCodeLen):
        buf = bytes([0x1D, 0x6B, ucBarcodeSys + 65, ucCodeLen])
        success1 = self.Write(buf)
        success2 = self.Write(pucCodeBuf)
        return success1 and success2
    
    def clearPrintBuffer(self):
        buf = bytes([0x10, 0x14])
        return self.Write(buf)
    
    def getFirmwareInfo(self):
        buf = bytes([0x10, 0x05])
        return self.Write(buf)
    
    def fullCut(self):
        buf = bytes([0x1B, 0x69])
        return self.Write(buf)
    
    def partialCut(self):
        buf = bytes([0x1B, 0x6E])
        return self.Write(buf)
    
    def enterSleepMode(self):
        buf = bytes([0x10, 0x16])
        return self.Write(buf)
    
    def picturePrintH(self, ucType, usHSize, usVSize, pucDataBuf):
        buf = bytes([0x1D, 0x76, 0x30, ucType, 
                    usHSize & 0xFF, (usHSize >> 8) & 0xFF,
                    usVSize & 0xFF, (usVSize >> 8) & 0xFF])
        success1 = self.Write(buf)
        success2 = True
        if pucDataBuf:
            success2 = self.Write(pucDataBuf)
        return success1 and success2
    
    # Combined functions
    def print_with_font(self, text, font):
        self.fontTypeSet(font)
        self.printStr(text)
    
    # Button handlers - IMPROVED
    def on_quick_test(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Starting Quick Test...\n")
        
        # Reset printer first
        self.printerSoftInit()
        time.sleep(0.1)
        
        # Test basic printing
        self.printStr("=== PRINTER QUICK TEST ===")
        self.printLF()
        self.printStr("Basic text printing test")
        self.printLF()
        self.printLF()
        
        # Test different alignments
        self.horizontalAlign(ALIGN_LEFT)
        self.printStr("Left aligned text")
        self.printLF()
        
        self.horizontalAlign(ALIGN_CENTER)
        self.printStr("Center aligned text")
        self.printLF()
        
        self.horizontalAlign(ALIGN_RIGHT)
        self.printStr("Right aligned text")
        self.printLF()
        
        self.horizontalAlign(ALIGN_LEFT)
        self.printLF()
        
        # Test font attributes
        self.printStr("Font attributes test:")
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_NORMAL)
        self.printStr("Normal text")
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_BOLD)
        self.printStr("Bold text")
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_UNDERLINE)
        self.printStr("Underlined text")
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_NORMAL)
        self.printLF()
        self.printStr("Quick test completed!")
        self.printLF()
        self.printLF()  # Feed some paper
        
        self.output_text.insert(tk.END, "Quick Test completed\n")
    
    def on_attributes(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing text attributes...\n")
        self.printerSoftInit()
        
        attributes = [
            ("Normal", FONT_TYPE_NORMAL),
            ("Bold", FONT_TYPE_BOLD),
            ("Italic", FONT_TYPE_ITALIC),
            ("Underline", FONT_TYPE_UNDERLINE),
            ("Double Width", FONT_TYPE_DOUBLE_WIDTH),
            ("Double Height", FONT_TYPE_DOUBLE_HEIGHT),
            ("Reverse", FONT_TYPE_REVERSE)
        ]
        
        for name, attr in attributes:
            self.fontTypeSet(attr)
            self.printStr(f"{name}: Sample text 123 ABC")
            self.printLF()
        
        self.fontTypeSet(FONT_TYPE_NORMAL)
        self.printLF()
    
    def on_language(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing language samples...\n")
        self.printerSoftInit()
        
        self.printStr("Language Support Test")
        self.printLF()
        self.printStr("Basic Latin: ABCDEFG abcdefg 123456")
        self.printLF()
        self.printLF()
        
        # Test small font
        self.setFontMode(FONT_SIZE_S_SMALL)
        self.printStr("Small Font: Small text sample")
        self.printLF()
        
        # Back to normal font
        self.setFontMode(FONT_SIZE_S_LARGE)
        self.printStr("Normal Font: Normal text sample")
        self.printLF()
        self.printLF()
    
    def on_density(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing density test...\n")
        self.printerSoftInit()
        
        self.printStr("Print Density Test")
        self.printLF()
        
        for i in range(1, 9):
            self.setFontGrayScale(i)
            # Use ASCII characters instead of Unicode block characters
            self.printStr(f"Density level {i}: ==========")
            self.printLF()
        
        self.setFontGrayScale(4)  # Reset to default
        self.printLF()
    
    def on_formatting(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing formatting test...\n")
        self.printerSoftInit()
        
        self.printStr("Formatting Test")
        self.printLF()
        
        # Line spacing
        self.lineSpacingSet(20)
        self.printStr("Tight spacing (20 dots)")
        self.printLF()
        
        self.lineSpacingSet(66)
        self.printStr("Wide spacing (66 dots)")
        self.printLF()
        
        self.lineSpacingDefaults()
        self.printStr("Default spacing")
        self.printLF()
        self.printLF()
    
    def on_alignment(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing alignment test...\n")
        self.printerSoftInit()
        
        alignments = [
            ("Left Alignment", ALIGN_LEFT),
            ("Center Alignment", ALIGN_CENTER), 
            ("Right Alignment", ALIGN_RIGHT)
        ]
        
        for name, align in alignments:
            self.horizontalAlign(align)
            self.printStr(name)
            self.printLF()
        
        self.horizontalAlign(ALIGN_LEFT)  # Reset to left
        self.printLF()
    
    def on_reset(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Resetting printer...\n")
        self.printerSoftInit()
        self.output_text.insert(tk.END, "Printer reset completed\n")
    
    def on_clear_buffer(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Clearing print buffer...\n")
        self.clearPrintBuffer()
        self.printLF()
        self.output_text.insert(tk.END, "Buffer cleared\n")
    
    def on_receipt(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Printing sample receipt...\n")
        self.printerSoftInit()
        
        # Print store logo bitmap
        self.print_store_logo()
        
        self.horizontalAlign(ALIGN_CENTER)
        self.fontTypeSet(FONT_TYPE_BOLD | FONT_TYPE_DOUBLE_HEIGHT)
        self.printStr("SAMPLE STORE")
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_NORMAL)
        self.printStr("123 Main Street")
        self.printLF()
        self.printStr("City, State 12345")
        self.printLF()
        self.printLF()
        
        self.horizontalAlign(ALIGN_LEFT)
        self.printStr("Receipt #: 001234")
        self.printLF()
        self.printStr("Date: 2024-01-01")
        self.printLF()
        self.printStr("Time: 14:30:00")
        self.printLF()
        self.printLF()
        
        self.printStr("Item".ljust(20) + "Price".rjust(10))
        self.printLF()
        self.printStr("-" * 30)
        self.printLF()
        
        items = [
            ("Product A", "12.99"),
            ("Product B", "5.50"), 
            ("Service Fee", "2.00")
        ]
        
        for item, price in items:
            self.printStr(item.ljust(20) + price.rjust(10))
            self.printLF()
        
        self.printStr("-" * 30)
        self.printLF()
        self.fontTypeSet(FONT_TYPE_BOLD)
        self.printStr("TOTAL:".ljust(20) + "20.49".rjust(10))
        self.printLF()
        self.printLF()
        
        self.fontTypeSet(FONT_TYPE_NORMAL)
        self.horizontalAlign(ALIGN_CENTER)
        self.printStr("Thank you for your business!")
        self.printLF()
        self.printLF()
        self.printLF()
        
    def print_store_logo(self):
        """Print a simple store logo as a bitmap"""
        # Create a 48x48 pixel store logo - using a simple geometric pattern
        width = 48
        height = 48
        bytes_per_line = (width + 7) // 8  # 6 bytes per line for 48 pixels

        # Create a simple geometric pattern (a diamond inside a box)
        logo_data = []

        for y in range(height):
            row = [0x00] * bytes_per_line  # Start with all white pixels

            for x in range(width):
                # Create a simple geometric pattern
                # Center of the image
                center_x = width // 2
                center_y = height // 2
                
                # Create a diamond shape
                if (abs(x - center_x) + abs(y - center_y)) < 20:
                    # Set the pixel to black
                    byte_index = x // 8
                    bit_index = 7 - (x % 8)
                    row[byte_index] |= (1 << bit_index)

            logo_data.append(row)

        # Convert the 2D list to a flat bytearray
        img_data = bytearray()
        for row in logo_data:
            img_data.extend(row)

        # Center the logo
        self.horizontalAlign(ALIGN_CENTER)

        # Print the logo bitmap
        success = self.picturePrintH(PICTURE_TYPE_NORMAL, bytes_per_line, height, img_data)

        # Reset alignment
        self.horizontalAlign(ALIGN_LEFT)

        if success:
            self.output_text.insert(tk.END, "Store logo printed successfully\n")
        else:
            self.output_text.insert(tk.END, "Failed to print store logo\n")

        # Add some space after the logo
        self.printLF()
        self.printLF()
    
    def on_status(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Getting printer status...\n")
        self.printerState()
    
    def on_version(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Getting firmware info...\n")
        self.getFirmwareInfo()
    
    def on_power_management(self):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, "Entering sleep mode...\n")
        self.enterSleepMode()
    
    # Barcode handlers
    def on_barcode_upca(self):
        self.print_barcode(BARCODE_SYS_UPCA, "01234567890", "UPC-A")
    
    def on_barcode_upce(self):
        self.print_barcode(BARCODE_SYS_UPCE, "012345", "UPC-E")
    
    def on_barcode_ean13(self):
        self.print_barcode(BARCODE_SYS_EAN13, "012345678901", "EAN-13")
    
    def on_barcode_ean8(self):
        self.print_barcode(BARCODE_SYS_EAN8, "0123456", "EAN-8")
    
    def on_barcode_code39(self):
        self.print_barcode(BARCODE_SYS_CODE39, "123456NL022", "CODE 39")
    
    def on_barcode_itf25(self):
        self.print_barcode(BARCODE_SYS_ITF25, "12345678", "ITF 2 5")
    
    def on_barcode_codabar(self):
        self.print_barcode(BARCODE_SYS_CODABAR, "A123456A", "CODA BAR")
    
    def on_barcode_code93(self):
        self.print_barcode(BARCODE_SYS_CODE93, "12345678", "CODE 93")
    
    def on_barcode_code128a(self):
        self.print_barcode(BARCODE_SYS_CODE128, "12345678", "CODE 128 A")
    
    def print_barcode(self, barcode_type, data, name):
        if not self.check_serial():
            return
        
        self.output_text.insert(tk.END, f"Printing {name} barcode: {data}\n")
        self.printerSoftInit()
        
        self.printStr(f"{name} Barcode Test")
        self.printLF()
        
        if self.barcode_margin_var.get():
            self.setLeftMargin(4)
        
        self.barcodeWidthSet(2)
        self.barcodeHeightSet(64)
        self.barcodeTextYesNo(1)  # Show text
        
        success = self.barcodePrint(barcode_type, data.encode('latin-1'), len(data))
        
        self.setLeftMargin(0)
        self.printLF()
        self.printLF()
        
        if success:
            self.output_text.insert(tk.END, f"{name} barcode sent successfully\n")
        else:
            self.output_text.insert(tk.END, f"Failed to send {name} barcode\n")
    
    # Bitmap handlers
    def load_image(self):
        filename = filedialog.askopenfilename(
            title="Open Image",
            filetypes=[("Image files", "*.bmp *.png *.jpg *.jpeg"), ("All files", "*.*")]
        )
        
        if filename:
            try:
                self.current_image = Image.open(filename)
                self.display_image()
                self.output_text.insert(tk.END, f"Loaded image: {filename}\n")
                self.output_text.insert(tk.END, f"Image size: {self.current_image.size}\n")
            except Exception as e:
                messagebox.showerror("Error", f"Failed to load image: {str(e)}")
    
    def display_image(self):
        if not self.current_image:
            return
        
        # Get canvas dimensions
        self.canvas.update()
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        
        if canvas_width <= 1:
            canvas_width = 400
        if canvas_height <= 1:
            canvas_height = 300
        
        # Resize image to fit canvas while maintaining aspect ratio
        img = self.current_image.copy()
        img.thumbnail((canvas_width - 20, canvas_height - 20), Image.Resampling.LANCZOS)
        
        self.photo = ImageTk.PhotoImage(img)
        self.canvas.delete("all")
        self.canvas.create_image(canvas_width//2, canvas_height//2, 
                                image=self.photo, anchor=tk.CENTER)
    
    def print_raster_image(self):
        if not self.check_serial():
            return
        
        if not self.current_image:
            messagebox.showwarning("Warning", "No image loaded!")
            return
        
        self.output_text.insert(tk.END, "Printing raster image...\n")
        self.printerSoftInit()
        
        # Convert image to 1bpp if needed
        if self.current_image.mode != '1':
            img = self.current_image.convert('1')
        else:
            img = self.current_image
        
        width, height = img.size
        bytes_per_line = (width + 7) // 8
        
        # Convert image data to bytes
        img_data = bytearray()
        for y in range(height):
            for x in range(0, width, 8):
                byte_val = 0
                for bit in range(8):
                    if x + bit < width:
                        pixel = img.getpixel((x + bit, y))
                        if pixel == 0:  # Black pixel
                            byte_val |= (1 << (7 - bit))
                img_data.append(byte_val)
        
        success = self.picturePrintH(PICTURE_TYPE_NORMAL, bytes_per_line, height, img_data)
        
        if success:
            self.output_text.insert(tk.END, f"Image sent successfully ({width}x{height})\n")
        else:
            self.output_text.insert(tk.END, "Failed to send image\n")
    
    def on_receipt_full_cut(self):
        self.on_receipt()
        self.fullCut()
    
    def on_receipt_partial_cut(self):
        self.on_receipt()
        self.partialCut()
    
    def apply_com_settings(self):
        if not self.check_serial():
            return
        
        # Map speed to com mode
        speed_map = {
            "9600": 0, "19200": 1, "38400": 2, "57600": 3,
            "115200": 4, "256000": 5, "375000": 6
        }
        
        # Map flow control
        flow_map = {"None": 0, "XON/XOFF": 1, "RTS/CTS": 2}
        
        com_mode = speed_map.get(self.speed_var.get(), 4)
        flow_mode = flow_map.get(self.flow_var.get(), 1)
        
        self.output_text.insert(tk.END, 
            f"Setting COM mode: speed={self.speed_var.get()}, flow={self.flow_var.get()}\n")
        
        success = self.setComMode(com_mode, flow_mode)
        if success:
            self.output_text.insert(tk.END, "COM settings applied successfully\n")
        else:
            self.output_text.insert(tk.END, "Failed to apply COM settings\n")
    
    def check_serial(self):
        if not self.serial_port or not self.serial_port.is_open:
            messagebox.showwarning("Warning", "Please connect to serial port first!")
            return False
        return True

def main():
    root = tk.Tk()
    app = PrinterEVKApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()