#!/usr/bin/env python3
"""
ESC/POS Printer Font Tester GUI
Allows testing customer fonts with hex command 1B 74 n where n can be 0,1 or 0x11-0x18
Supports various text styles using 1B 21 command with bitwise OR combinations
"""

import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
from datetime import datetime

class ESCPOSFontTester:
    def __init__(self, root):
        self.root = root
        self.root.title("ESC/POS Printer Font Tester")
        self.root.geometry("600x900")
        
        self.ser = None
        self.is_connected = False
        
        # Serial configuration variables
        self.port_var = tk.StringVar()
        self.baudrate_var = tk.StringVar(value="115200")
        
        # Font selection variables
        self.font_var = tk.StringVar(value="0")
        
        # Style selection variable (radio buttons - single selection)
        self.style_var = tk.StringVar(value="normal")
        
        # Text case variable
        self.case_var = tk.StringVar(value="normal")
        
        # Test text
        self.test_text_var = tk.StringVar(value="The quick brown fox jumps over the lazy dog. 1234567890")
        
        # Define style bit masks
        self.style_bits = {
            "normal": 0x00,          # Normal - bit 0
            "italic": 0x02,          # Italic - bit 1
            "bold": 0x08,            # Bold - bit 3
            "double_height": 0x10,   # Double height - bit 4
            "double_width": 0x20,    # Double width - bit 5
            "underline": 0x80,       # Underline - bit 7
            "double_both": 0x30,     # Double width + height (0x20 | 0x10)
            "bold_italic": 0x0A,     # Bold + italic (0x08 | 0x02)
            "bold_underline": 0x88,  # Bold + underline (0x08 | 0x80)
            "italic_underline": 0x82 # Italic + underline (0x02 | 0x80)
        }
        
        self.setup_gui()
        self.refresh_ports()
    
    def setup_gui(self):
        # Configure main grid
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=1)
        
        # Connection Frame
        conn_frame = ttk.LabelFrame(self.root, text="Serial Connection", padding="10")
        conn_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=10, pady=5)
        conn_frame.columnconfigure(1, weight=1)
        
        # Port selection
        ttk.Label(conn_frame, text="Port:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
        self.port_combo = ttk.Combobox(conn_frame, textvariable=self.port_var, state="readonly")
        self.port_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        
        ttk.Button(conn_frame, text="Refresh", command=self.refresh_ports).grid(row=0, column=2, padx=(0, 10))
        
        # Baud rate
        ttk.Label(conn_frame, text="Baud Rate:").grid(row=1, column=0, sticky=tk.W, padx=(0, 5))
        baud_combo = ttk.Combobox(conn_frame, textvariable=self.baudrate_var, 
                                 values=["9600", "19200", "38400", "57600", "115200"], 
                                 state="readonly")
        baud_combo.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
        
        self.conn_button = ttk.Button(conn_frame, text="Connect", command=self.toggle_connection)
        self.conn_button.grid(row=1, column=2)
        
        # Main content frame
        main_frame = ttk.Frame(self.root)
        main_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=10, pady=5)
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(1, weight=1)
        
        # Font Selection Frame
        font_frame = ttk.LabelFrame(main_frame, text="Font Selection (ESC t n)", padding="10")
        font_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Font codes with descriptions
        font_options = [
            ("0 (Default Font)", "0"),
            ("1 (Font B)", "1"),
            ("17 (0x11 custom)", "17"),
            ("18 (0x12 custom)", "18"),
            ("19 (0x13 custom)", "19"),
            ("20 (0x14 custom)", "20"),
            ("21 (0x15 custom)", "21"),
            ("22 (0x16 custom)", "22"),
            ("23 (0x17 custom)", "23"),
            ("24 (0x18 custom)", "24"),
        ]
        
        for i, (text, value) in enumerate(font_options):
            rb = ttk.Radiobutton(font_frame, text=text, variable=self.font_var, value=value)
            rb.grid(row=i//3, column=i%3, sticky=tk.W, padx=5, pady=2)
        
        # Text Styles Frame
        style_frame = ttk.LabelFrame(main_frame, text="Text Styles (ESC ! n) - Single Selection", padding="10")
        style_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Style radio buttons in two columns
        style_col1 = ttk.Frame(style_frame)
        style_col1.grid(row=0, column=0, sticky=tk.W, padx=(0, 20))
        
        style_col2 = ttk.Frame(style_frame)
        style_col2.grid(row=0, column=1, sticky=tk.W)
        
        # Column 1 styles
        styles_col1 = [
            ("Normal (0x00)", "normal"),
            ("Bold (0x08)", "bold"),
            ("Italic (0x02)", "italic"),
            ("Underline (0x80)", "underline"),
            ("Double Height (0x10)", "double_height"),
        ]
        
        for text, value in styles_col1:
            rb = ttk.Radiobutton(style_col1, text=text, variable=self.style_var, value=value)
            rb.pack(anchor=tk.W, pady=2)
        
        # Column 2 styles
        styles_col2 = [
            ("Double Width (0x20)", "double_width"),
            ("Double Both (0x30)", "double_both"),
            ("Bold + Italic (0x0A)", "bold_italic"),
            ("Bold + Underline (0x88)", "bold_underline"),
            ("Italic + Underline (0x82)", "italic_underline"),
        ]
        
        for text, value in styles_col2:
            rb = ttk.Radiobutton(style_col2, text=text, variable=self.style_var, value=value)
            rb.pack(anchor=tk.W, pady=2)
        
        # Text Case Frame
        case_frame = ttk.LabelFrame(main_frame, text="Text Case", padding="10")
        case_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        ttk.Radiobutton(case_frame, text="Normal Case", variable=self.case_var, value="normal").pack(side=tk.LEFT, padx=(0, 20))
        ttk.Radiobutton(case_frame, text="ALL CAPS", variable=self.case_var, value="uppercase").pack(side=tk.LEFT, padx=(0, 20))
        ttk.Radiobutton(case_frame, text="all small", variable=self.case_var, value="lowercase").pack(side=tk.LEFT)
        
        # Test Text Frame
        text_frame = ttk.LabelFrame(main_frame, text="Test Text", padding="10")
        text_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        text_frame.columnconfigure(0, weight=1)
        
        ttk.Label(text_frame, text="Text to print:").grid(row=0, column=0, sticky=tk.W)
        self.text_entry = ttk.Entry(text_frame, textvariable=self.test_text_var, width=80)
        self.text_entry.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(5, 10))
        
        # Predefined text buttons
        text_buttons_frame = ttk.Frame(text_frame)
        text_buttons_frame.grid(row=2, column=0, sticky=(tk.W, tk.E))
        
        sample_texts = [
            ("English", "The quick brown fox jumps over the lazy dog"),
            ("Numbers", "1234567890 !@#$%^&*()"),
            ("Latin", "Lorem ipsum dolor sit amet"),
            ("Special Chars", "Hello! ¿Cómo estás? こんにちは"),
            ("Alphabet", "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz")
        ]
        
        for i, (label, text) in enumerate(sample_texts):
            ttk.Button(text_buttons_frame, text=label, 
                      command=lambda t=text: self.test_text_var.set(t)).grid(row=0, column=i, padx=2)
        
        # Control Buttons Frame
        control_frame = ttk.Frame(main_frame)
        control_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        ttk.Button(control_frame, text="Test Print", command=self.test_print).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(control_frame, text="Print All Styles", command=self.print_all_styles).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(control_frame, text="Initialize Printer", command=self.initialize_printer).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(control_frame, text="Cut Paper", command=self.cut_paper).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(control_frame, text="Clear Output", command=self.clear_output).pack(side=tk.LEFT)
        
        # Output Frame
        output_frame = ttk.LabelFrame(main_frame, text="Output Log", padding="10")
        output_frame.grid(row=5, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        output_frame.columnconfigure(0, weight=1)
        output_frame.rowconfigure(0, weight=1)
        
        self.output_text = scrolledtext.ScrolledText(output_frame, height=12, wrap=tk.WORD)
        self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.output_text.config(state=tk.DISABLED)
    
    def refresh_ports(self):
        """Refresh available serial ports"""
        ports = [port.device for port in serial.tools.list_ports.comports()]
        self.port_combo['values'] = ports
        if ports and not self.port_var.get():
            self.port_var.set(ports[0])
        self.log_output("Available ports refreshed")
    
    def toggle_connection(self):
        """Toggle serial connection"""
        if not self.is_connected:
            self.connect()
        else:
            self.disconnect()
    
    def connect(self):
        """Connect to serial port"""
        port = self.port_var.get()
        baudrate = self.baudrate_var.get()
        
        if not port:
            messagebox.showerror("Error", "Please select a serial port")
            return
        
        try:
            self.ser = serial.Serial(
                port=port,
                baudrate=int(baudrate),
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=1
            )
            self.is_connected = True
            self.conn_button.config(text="Disconnect")
            self.port_combo.config(state="disabled")
            self.log_output(f"✓ Connected to {port} at {baudrate} baud")
            
            # Initialize printer
            self.initialize_printer()
            
        except serial.SerialException as e:
            messagebox.showerror("Connection Error", f"Failed to connect: {e}")
            self.log_output(f"✗ Connection failed: {e}")
    
    def disconnect(self):
        """Disconnect from serial port"""
        if self.ser and self.ser.is_open:
            self.ser.close()
        self.is_connected = False
        self.conn_button.config(text="Connect")
        self.port_combo.config(state="readonly")
        self.log_output("✗ Disconnected")
    
    def send_hex_data(self, hex_data):
        """Send hex data to printer"""
        if not self.is_connected:
            messagebox.showwarning("Not Connected", "Please connect to a serial port first")
            return False
        
        try:
            # Clean hex data
            hex_clean = ''.join(c for c in hex_data if c in '0123456789ABCDEFabcdef')
            
            if len(hex_clean) % 2 != 0:
                self.log_output("✗ Error: Hex data must have even number of characters")
                return False
            
            data = bytes.fromhex(hex_clean)
            bytes_written = self.ser.write(data)
            self.ser.flush()
            
            timestamp = datetime.now().strftime("%H:%M:%S")
            hex_display = ' '.join(hex_clean[i:i+2].upper() for i in range(0, len(hex_clean), 2))
            self.log_output(f"[{timestamp}] ✓ Sent {bytes_written} bytes: {hex_display}")
            
            return True
            
        except Exception as e:
            self.log_output(f"✗ Error sending data: {e}")
            return False
    
    def send_text(self, text):
        """Send text to printer"""
        if not self.is_connected:
            return False
        
        try:
            data = text.encode('utf-8')
            self.ser.write(data)
            self.ser.flush()
            return True
        except Exception as e:
            self.log_output(f"✗ Error sending text: {e}")
            return False
    
    def initialize_printer(self):
        """Initialize printer (ESC @)"""
        self.send_hex_data("1B40")
        self.log_output("Printer initialized")
    
    def set_font(self, font_code):
        """Set customer font (ESC t n)"""
        hex_command = f"1B74{int(font_code):02X}"
        self.send_hex_data(hex_command)
        self.log_output(f"Font set to: {font_code}")
    
    def set_text_style(self):
        """Set text style using ESC ! n command with selected style bit"""
        style_bit = self.style_bits.get(self.style_var.get(), 0x00)
        hex_command = f"1B21{style_bit:02X}"
        self.send_hex_data(hex_command)
        self.log_output(f"Style set to: {self.style_var.get()} (0x{style_bit:02X})")
    
    def apply_text_case(self, text):
        """Apply selected text case"""
        case = self.case_var.get()
        if case == "uppercase":
            return text.upper()
        elif case == "lowercase":
            return text.lower()
        else:
            return text
    
    def test_print(self):
        """Perform test print with current settings"""
        if not self.is_connected:
            messagebox.showwarning("Not Connected", "Please connect to a serial port first")
            return
        
        # Start in separate thread
        thread = threading.Thread(target=self._test_print_thread)
        thread.daemon = True
        thread.start()
    
    def _test_print_thread(self):
        """Thread function for test printing"""
        try:
            self.log_output("Starting test print...")
            
            # Initialize printer
            self.initialize_printer()
            
            # Set selected font
            font_code = self.font_var.get()
            self.set_font(font_code)
            
            # Print separator
            self.send_text("=" * 40)
            self.send_hex_data("0A")
            
            # Print configuration info
            self.send_text(f"Font: {self.font_var.get()}")
            self.send_hex_data("0A")
            self.send_text(f"Style: {self.style_var.get()}")
            self.send_hex_data("0A")
            self.send_text(f"Case: {self.case_var.get()}")
            self.send_hex_data("0A0A")
            
            # Set text style
            self.set_text_style()
            
            # Print test text with applied case
            test_text = self.test_text_var.get()
            processed_text = self.apply_text_case(test_text)
            self.send_text(processed_text)
            
            # Extra line feeds
            self.send_hex_data("0A0A")
            
            self.log_output("✓ Test print completed")
            
        except Exception as e:
            self.log_output(f"✗ Test print failed: {e}")
    
    def print_all_styles(self):
        """Print all style combinations for testing"""
        if not self.is_connected:
            messagebox.showwarning("Not Connected", "Please connect to a serial port first")
            return
        
        thread = threading.Thread(target=self._print_all_styles_thread)
        thread.daemon = True
        thread.start()
    
    def _print_all_styles_thread(self):
        """Thread function for printing all styles"""
        try:
            self.log_output("Printing all style combinations...")
            
            # Initialize printer
            self.initialize_printer()
            
            test_text = self.test_text_var.get()
            
            # Test different fonts
            fonts_to_test = ["0", "1", "17", "18", "19"]
            
            for font in fonts_to_test:
                self.set_font(font)
                self.send_text(f"Font {font}:")
                self.send_hex_data("0A")
                
                # Test with normal style
                self.set_text_style()
                self.send_text(test_text)
                self.send_hex_data("0A0A")
            
            # Test all styles with default font
            self.set_font("0")
            self.send_text("Style Tests:")
            self.send_hex_data("0A")
            
            for style_name, style_bit in self.style_bits.items():
                # Set the style
                hex_command = f"1B21{style_bit:02X}"
                self.send_hex_data(hex_command)
                
                # Print style name and test text
                self.send_text(f"{style_name} (0x{style_bit:02X}): {test_text}")
                self.send_hex_data("0A")
            
            self.send_hex_data("0A")
            self.log_output("✓ All styles printed")
            
        except Exception as e:
            self.log_output(f"✗ Printing all styles failed: {e}")
    
    def cut_paper(self):
        """Cut paper (GS V m)"""
        self.send_hex_data("1D5600")  # Partial cut
        self.log_output("Paper cut command sent")
    
    def clear_output(self):
        """Clear output log"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete(1.0, tk.END)
        self.output_text.config(state=tk.DISABLED)
    
    def log_output(self, message):
        """Add message to output log"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.insert(tk.END, message + "\n")
        self.output_text.see(tk.END)
        self.output_text.config(state=tk.DISABLED)
    
    def on_closing(self):
        """Handle application closing"""
        if self.is_connected:
            self.disconnect()
        self.root.destroy()

def main():
    root = tk.Tk()
    app = ESCPOSFontTester(root)
    
    # Handle window closing
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    
    root.mainloop()

if __name__ == "__main__":
    main()