
from PyQt6.QtWidgets import (
    QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
    QListWidget, QListWidgetItem, QComboBox, QLabel, QLineEdit, QPushButton, 
    QFileDialog, QMenuBar, QMenu, QMessageBox, QStatusBar, QProgressBar, QApplication,
    QCheckBox, QDialog, QGridLayout, QRadioButton, QFrame
)
from PyQt6.QtGui import QAction, QKeyEvent, QPainter, QBrush, QColor
from PyQt6.QtCore import Qt, QRect, QTimer, pyqtSignal

from .io import load_t42, save_t42, save_tti, decode_hamming_8_4, encode_hamming_8_4
from .renderer import TeletextCanvas, create_blank_packet
from .charsets import get_char
import copy
import sys
import os
from .models import TeletextService, Page, Packet

class MosaicButton(QPushButton):
    def __init__(self, code, main_window):
        super().__init__()
        self.code = code
        self.main_window = main_window
        self.setFixedSize(32, 32)
        self.setToolTip(f"Hex: {code:02X}")
        self.clicked.connect(self.on_click)

    def on_click(self):
        self.main_window.insert_char(self.code)

    def paintEvent(self, event):
        super().paintEvent(event)
        painter = QPainter(self)
        
        # Draw content area (centered, smaller than button)
        w = self.width()
        h = self.height()
        m = 4
        rect = QRect(m, m, w - 2*m, h - 2*m)
        
        # Background (Black)
        painter.fillRect(rect, Qt.GlobalColor.black)
        
        # Foreground (White)
        painter.setBrush(QBrush(Qt.GlobalColor.white))
        painter.setPen(Qt.PenStyle.NoPen)
        
        # Mosaic Logic
        val = self.code & 0x7F
        bits = 0
        if val >= 0x20:
            bits = val - 0x20
            
        # 2x3 grid
        cw = rect.width()
        ch = rect.height()
        
        x_splits = [0, cw // 2, cw]
        y_splits = [0, ch // 3, (2 * ch) // 3, ch]
        
        # bit 0: TL, 1: TR, 2: ML, 3: MR, 4: BL, 6: BR
        block_indices = [
            (0, 0), (1, 0), # Top
            (0, 1), (1, 1), # Mid
            (0, 2), (1, 2)  # Bot
        ]
        bit_mask = [1, 2, 4, 8, 16, 64]
        
        for i in range(6):
            if bits & bit_mask[i]:
                c, r = block_indices[i]
                bx = rect.x() + x_splits[c]
                by = rect.y() + y_splits[r]
                bw = x_splits[c+1] - x_splits[c]
                bh = y_splits[r+1] - y_splits[r]
                painter.drawRect(bx, by, bw, bh)

class MosaicDialog(QDialog):
    def __init__(self, main_window):
        super().__init__(main_window)
        self.setWindowTitle("Insert Mosaic")
        self.main_window = main_window
        self.setLayout(QVBoxLayout())
        
        lbl = QLabel("Click to insert mosaic character:")
        self.layout().addWidget(lbl)

        hint = QLabel("Note: Mosaics only appear if the line segment is in Graphics Mode.\n"
                      "Insert a Graphics Color code (e.g. Red Graphics 0x11) first.")
        hint.setStyleSheet("color: gray; font-style: italic;")
        self.layout().addWidget(hint)
        
        grid = QGridLayout()
        self.layout().addLayout(grid)
        
        # Ranges: 0x20-0x3F, 0x60-0x7F
        codes = []
        codes.extend(range(0x20, 0x40))
        codes.extend(range(0x60, 0x80))
        
        row = 0
        col = 0
        max_cols = 8
        
        for code in codes:
            btn = MosaicButton(code, main_window)
            grid.addWidget(btn, row, col)
            col += 1
            if col >= max_cols:
                col = 0
                row += 1
                
        close_btn = QPushButton("Close")
        close_btn.clicked.connect(self.accept)
        self.layout().addWidget(close_btn)

class FindDialog(QDialog):
    findNext = pyqtSignal(str, bool) # query, is_hex
    findPrev = pyqtSignal(str, bool) # query, is_hex

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Find")
        self.setFixedWidth(350)
        self.setLayout(QVBoxLayout())

        # Query Input
        self.query_input = QLineEdit()
        self.query_input.setPlaceholderText("Search text or hex (e.g. 1C 02)...")
        self.layout().addWidget(QLabel("Find:"))
        self.layout().addWidget(self.query_input)

        # Mode Selection
        mode_layout = QHBoxLayout()
        self.radio_text = QRadioButton("Text")
        self.radio_text.setChecked(True)
        self.radio_hex = QRadioButton("Hex")
        mode_layout.addWidget(self.radio_text)
        mode_layout.addWidget(self.radio_hex)
        mode_layout.addStretch()
        self.layout().addLayout(mode_layout)

        # Navigation Buttons
        nav_layout = QHBoxLayout()
        self.btn_prev = QPushButton("Previous")
        self.btn_prev.clicked.connect(self.on_find_prev)
        self.btn_next = QPushButton("Next")
        self.btn_next.clicked.connect(self.on_find_next)
        self.btn_next.setDefault(True)
        nav_layout.addWidget(self.btn_prev)
        nav_layout.addWidget(self.btn_next)
        self.layout().addLayout(nav_layout)

        # Status Label
        self.status_label = QLabel("")
        self.status_label.setStyleSheet("color: gray;")
        self.layout().addWidget(self.status_label)

    def on_find_next(self):
        query = self.query_input.text()
        if query:
            self.findNext.emit(query, self.radio_hex.isChecked())

    def on_find_prev(self):
        query = self.query_input.text()
        if query:
            self.findPrev.emit(query, self.radio_hex.isChecked())

    def set_status(self, text):
        self.status_label.setText(text)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Teletext Editor")
        self.resize(1024, 768)
        
        self.service = TeletextService()
        self.current_page: Page = None
        self.current_file_path = None
        self.clipboard = [] # List of (row, data_bytes)
        self.undo_stack = []
        self.redo_stack = []
        self.is_modified = False
        self.language_overrides = {} # Session-based viewer overrides: (mag, pnum) -> lang_idx
        
        self.language_names = ["English", "German", "Swedish/Finnish", "Italian", "French", "Portuguese/Spanish", "Turkish", "Romania"]

        # Search State
        self.find_dialog = None
        self.search_results = [] # List of (Page, row, col, length)
        self.current_search_index = -1

        # UI Components
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        
        self.layout = QHBoxLayout(self.central_widget)
        
        # Left Panel: Page List
        left_layout = QVBoxLayout()
        left_label = QLabel("Pages")
        left_layout.addWidget(left_label)
        
        self.page_list = QListWidget()
        self.page_list.setFixedWidth(150)
        self.page_list.itemClicked.connect(self.on_page_selected)
        left_layout.addWidget(self.page_list)
        
        # Hex Inspector
        hex_layout = QVBoxLayout()
        hex_label = QLabel("Hex Value:")
        self.hex_input = QLineEdit()
        self.hex_input.setMaxLength(2)
        self.hex_input.setPlaceholderText("00")
        self.hex_input.returnPressed.connect(self.on_hex_entered)
        hex_layout.addWidget(hex_label)
        hex_layout.addWidget(self.hex_input)
        left_layout.addLayout(hex_layout)
        
        left_layout.addStretch()
        
        self.layout.addLayout(left_layout)
        
        # Center Area Layout (Top Bar + Canvas)
        center_layout = QVBoxLayout()
        
        # Top Bar: Subpage Selector
        top_bar = QHBoxLayout()
        self.subpage_label = QLabel("Subpage:")
        self.subpage_combo = QComboBox()
        self.subpage_combo.setMinimumWidth(250)
        self.subpage_combo.currentIndexChanged.connect(self.on_subpage_changed)
        
        self.btn_prev_sub = QPushButton("<")
        self.btn_prev_sub.setFixedWidth(30)
        self.btn_prev_sub.clicked.connect(self.prev_subpage)
        
        self.btn_next_sub = QPushButton(">")
        self.btn_next_sub.setFixedWidth(30)
        self.btn_next_sub.clicked.connect(self.next_subpage)
        
        top_bar.addWidget(self.subpage_label)
        top_bar.addWidget(self.btn_prev_sub)
        top_bar.addWidget(self.subpage_combo)
        top_bar.addWidget(self.btn_next_sub)
        top_bar.addStretch()
        
        center_layout.addLayout(top_bar)
        
        # Middle Layout (Canvas + Right Sidebar)
        middle_layout = QHBoxLayout()
        center_layout.addLayout(middle_layout, 1)
        
        # Canvas
        self.canvas = TeletextCanvas()
        self.canvas.cursorChanged.connect(self.on_cursor_changed)
        middle_layout.addWidget(self.canvas, 1) # Expand
        
        # Right Sidebar
        right_sidebar = QWidget()
        right_sidebar.setFixedWidth(160)
        right_layout = QVBoxLayout(right_sidebar)
        right_layout.setContentsMargins(5, 0, 0, 0)
        middle_layout.addWidget(right_sidebar)

        # Color Shortcuts
        # Graphics Mode Toggle
        self.chk_graphics = QCheckBox("Graphics")
        self.chk_graphics.setToolTip("If checked, inserts Graphics Color codes (e.g. Red Graphics 0x11) instead of Alpha (0x01)")
        right_layout.addWidget(self.chk_graphics)

        colors_grid = QGridLayout()
        colors_grid.setSpacing(5)
        
        colors = [
            ("Black", 0x00, "#000000"),
            ("Red", 0x01, "#FF0000"),
            ("Green", 0x02, "#00FF00"),
            ("Yellow", 0x03, "#FFFF00"),
            ("Blue", 0x04, "#0000FF"),
            ("Magenta", 0x05, "#FF00FF"),
            ("Cyan", 0x06, "#00FFFF"),
            ("White", 0x07, "#FFFFFF"),
        ]
        
        row, col = 0, 0
        for name, code, hex_color in colors:
            btn = QPushButton(name)
            btn.setFixedSize(60, 30) 
            
            # Common style
            style = f"background-color: {hex_color}; font-weight: bold; border: 1px solid #555; border-radius: 3px;"
            
            if name == "Black":
                style += " color: white;"
            elif name in ["Blue", "Red", "Magenta"]: # Darker backgrounds
                style += " color: white;"
            else:
                style += " color: black;"
                
            btn.setStyleSheet(style)
            
            # Use separate method to handle graphics check
            btn.clicked.connect(lambda checked, c=code: self.insert_color(c))
            colors_grid.addWidget(btn, row, col)
            
            col += 1
            if col > 1:
                col = 0
                row += 1
        
        right_layout.addLayout(colors_grid)
            
        # Mosaics Button
        btn_mosaic = QPushButton("Mosaics...")
        btn_mosaic.clicked.connect(self.open_mosaic_dialog)
        right_layout.addWidget(btn_mosaic)

        right_layout.addSpacing(10)

        # Background Controls
        bg_label = QLabel("Background:")
        right_layout.addWidget(bg_label)

        # New Background (0x1D)
        btn_new_bg = QPushButton("New BG")
        btn_new_bg.setFixedSize(80, 30)
        btn_new_bg.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
        btn_new_bg.setToolTip("Sets the current foreground color as the new background color (0x1D)")
        btn_new_bg.clicked.connect(lambda: self.insert_char(0x1D))
        right_layout.addWidget(btn_new_bg)

        # Black Background (0x1C)
        btn_black_bg = QPushButton("Black BG")
        btn_black_bg.setFixedSize(80, 30)
        btn_black_bg.setStyleSheet("background-color: black; color: white; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
        btn_black_bg.setToolTip("Resets the background color to Black (0x1C)")
        btn_black_bg.clicked.connect(lambda: self.insert_char(0x1C))
        right_layout.addWidget(btn_black_bg)
        
        right_layout.addSpacing(10)

        # Graphics Control
        gfx_ctrl_label = QLabel("Graphics Control:")
        right_layout.addWidget(gfx_ctrl_label)

        # Hold Graphics (0x1E)
        btn_hold = QPushButton("Hold Gfx")
        btn_hold.setFixedSize(80, 30)
        btn_hold.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
        btn_hold.setToolTip("Hold Graphics (0x1E): Displays the last graphic char in place of subsequent control codes.")
        btn_hold.clicked.connect(lambda: self.insert_char(0x1E))
        right_layout.addWidget(btn_hold)

        # Release Graphics (0x1F)
        btn_release = QPushButton("Release Gfx")
        btn_release.setFixedSize(90, 30)
        btn_release.setStyleSheet("background-color: #CCCCCC; color: black; font-weight: bold; border: 1px solid #555; border-radius: 3px;")
        btn_release.setToolTip("Release Graphics (0x1F): Ends the 'Hold Graphics' effect.")
        btn_release.clicked.connect(lambda: self.insert_char(0x1F))
        right_layout.addWidget(btn_release)
        
        right_layout.addSpacing(10)

        # Page Language Setting
        lang_group_label = QLabel("Page Language:")
        right_layout.addWidget(lang_group_label)
        
        lang_layout = QHBoxLayout()
        self.lang_combo = QComboBox()
        self.lang_combo.addItems(self.language_names)
        self.lang_combo.setToolTip("Select the National Option Character Set for this page.")
        
        btn_set_lang = QPushButton("Set")
        btn_set_lang.setFixedWidth(40)
        btn_set_lang.clicked.connect(self.apply_language_change)
        btn_set_lang.setToolTip("Apply this language setting to the page header.")
        
        lang_layout.addWidget(self.lang_combo)
        lang_layout.addWidget(btn_set_lang)
        right_layout.addLayout(lang_layout)

        right_layout.addSpacing(10)

        # CRC Checksum
        crc_label = QLabel("CRC Checksum:")
        right_layout.addWidget(crc_label)
        
        self.lbl_crc_info = QLabel("Page: ----\nCalc: ----")
        self.lbl_crc_info.setStyleSheet("font-family: monospace; font-weight: bold;")
        right_layout.addWidget(self.lbl_crc_info)
        
        right_layout.addStretch()
        
        self.layout.addLayout(center_layout, 1)
        
        # Status Bar
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        
        self.progress_bar = QProgressBar()
        self.progress_bar.setFixedWidth(200)
        self.progress_bar.setVisible(False)
        self.status_bar.addPermanentWidget(self.progress_bar)
        
        self.status_label = QLabel("Ready")
        self.status_bar.addWidget(self.status_label)

        self.mode_label = QLabel("Mode: Text")
        self.mode_label.setFixedWidth(120)
        self.status_bar.addPermanentWidget(self.mode_label)
        
        self.language_label = QLabel("Lang: English")
        self.status_bar.addPermanentWidget(self.language_label)

        # Menus
        self.create_menus()

    def update_crc_display(self):
        if not self.current_page:
            self.lbl_crc_info.setText("Page: ----\nCalc: ----")
            self.lbl_crc_info.setStyleSheet("font-family: monospace; font-weight: bold;")
            return
            
        calc_crc = self.current_page.calculate_crc()
        stored_crc = self.current_page.get_stored_crc()
        
        stored_str = f"{stored_crc:04X}" if stored_crc is not None else "----"
        calc_str = f"{calc_crc:04X}"
        
        # Highlight if match
        if stored_crc is not None:
            if stored_crc == calc_crc:
                style = "font-family: monospace; font-weight: bold; color: green;"
            else:
                style = "font-family: monospace; font-weight: bold; color: red;"
        else:
             style = "font-family: monospace; font-weight: bold;"
             
        self.lbl_crc_info.setStyleSheet(style)
        self.lbl_crc_info.setText(f"Page: {stored_str}\nCalc: {calc_str}")

    def update_language_label(self):
        idx = self.canvas.subset_idx
        if 0 <= idx < len(self.language_names):
            self.language_label.setText(f"Lang: {self.language_names[idx]}")
            self.lang_combo.setCurrentIndex(idx)
        else:
            self.language_label.setText(f"Lang: Unknown ({idx})")

    def set_modified(self, modified: bool):
        self.is_modified = modified
        title = "Teletext Editor"
        if self.current_file_path:
            title += f" - {os.path.basename(self.current_file_path)}"
        else:
             title += " - Untitled"
        
        if self.is_modified:
            title += " *"
        self.setWindowTitle(title)

    def maybe_save_changes(self) -> bool:
        if not self.is_modified:
            return True
        
        ret = QMessageBox.warning(self, "Unsaved Changes",
                                  "The document has been modified.\nDo you want to save your changes?",
                                  QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
        
        if ret == QMessageBox.StandardButton.Save:
            return self.save_file()
        elif ret == QMessageBox.StandardButton.Discard:
            return True
        else:
            return False # Cancel

    def closeEvent(self, event):
        if self.maybe_save_changes():
            event.accept()
        else:
            event.ignore()

    def update_progress(self, current, total):
        self.progress_bar.setMaximum(total)
        self.progress_bar.setValue(current)
        QApplication.processEvents() # Force UI update
        
    def create_menus(self):
        menu_bar = self.menuBar()
        
        file_menu = menu_bar.addMenu("File")
        
        open_action = QAction("Open T42...", self)
        open_action.setShortcut("Ctrl+O")
        open_action.triggered.connect(self.open_file)
        file_menu.addAction(open_action)
        
        save_action = QAction("Save T42...", self)
        save_action.setShortcut("Ctrl+S")
        save_action.triggered.connect(self.save_file)
        file_menu.addAction(save_action)
        
        save_as_action = QAction("Save As...", self)
        save_as_action.setShortcut("Ctrl+Shift+S")
        save_as_action.triggered.connect(self.save_as_file)
        file_menu.addAction(save_as_action)
        
        export_tti_action = QAction("Export to TTI...", self)
        export_tti_action.triggered.connect(self.export_tti)
        file_menu.addAction(export_tti_action)
        
        close_action = QAction("Close File", self)
        close_action.triggered.connect(self.close_file)
        file_menu.addAction(close_action)
        
        file_menu.addSeparator()
        
        exit_action = QAction("Exit", self)
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)
        
        # Edit Menu
        edit_menu = menu_bar.addMenu("Edit")
        
        copy_action = QAction("Copy Page Content", self)
        copy_action.triggered.connect(self.copy_page_content)
        edit_menu.addAction(copy_action)
        
        paste_action = QAction("Paste Page Content", self)
        paste_action.triggered.connect(self.paste_page_content)
        edit_menu.addAction(paste_action)
        
        edit_menu.addSeparator()
        
        find_action = QAction("Find...", self)
        find_action.setShortcut("Ctrl+F")
        find_action.triggered.connect(self.open_find_dialog)
        edit_menu.addAction(find_action)
        
        delete_page_action = QAction("Delete Page", self)
        delete_page_action.triggered.connect(self.delete_page)
        edit_menu.addAction(delete_page_action)
        
        edit_menu.addSeparator()
        
        undo_action = QAction("Undo", self)
        undo_action.setShortcut("Ctrl+Z")
        undo_action.triggered.connect(self.undo)
        edit_menu.addAction(undo_action)
        
        redo_action = QAction("Redo", self)
        redo_action.setShortcut("Ctrl+Y")
        redo_action.triggered.connect(self.redo)
        edit_menu.addAction(redo_action)
        
        view_menu = menu_bar.addMenu("View")
        
        lang_menu = view_menu.addMenu("Language")
        for i, lang in enumerate(self.language_names):
            action = QAction(lang, self)
            action.setData(i)
            action.triggered.connect(self.set_language)
            lang_menu.addAction(action)

    def set_language(self):
        action = self.sender()
        if action:
            idx = action.data()
            self.canvas.subset_idx = idx
            
            # Store session override for the current page (magazine + page number)
            if self.current_page:
                key = (self.current_page.magazine, self.current_page.page_number)
                self.language_overrides[key] = idx
                
            self.canvas.redraw()
            self.canvas.update()
            self.update_language_label()

    def apply_language_change(self):
        if not self.current_page:
            return
            
        idx = self.lang_combo.currentIndex()
        if idx < 0: return
        
        # Update the model
        self.current_page.language = idx
        
        # Also update the session override/viewer to match
        self.canvas.subset_idx = idx
        key = (self.current_page.magazine, self.current_page.page_number)
        self.language_overrides[key] = idx
        
        # Patch Row 0 packet data to persist language selection to file
        # Language bits are in Byte 7 (Control Bits C11-C14)
        # Byte 7 encoded structure: bit 0:C11, bit 1:C12, bit 2:C13, bit 3:C14
        # National Option index corresponds to (C14 C13 C12)
        
        # Find Row 0 packet
        header_packet = None
        for p in self.current_page.packets:
            if p.row == 0:
                header_packet = p
                break
        
        if header_packet and len(header_packet.data) >= 8:
            try:
                # Byte 7 contains C11, C12, C13, C14
                old_val = decode_hamming_8_4(header_packet.data[7])
                
                l0 = (idx >> 0) & 1 # C12
                l1 = (idx >> 1) & 1 # C13
                l2 = (idx >> 2) & 1 # C14
                
                d1 = (old_val >> 0) & 1 # Preserve C11
                d2 = l0
                d3 = l1
                d4 = l2
                
                new_val = d1 | (d2 << 1) | (d3 << 2) | (d4 << 3)
                
                header_packet.data[7] = encode_hamming_8_4(new_val)
                self.set_modified(True)
                self.status_label.setText(f"Language set to {self.language_names[idx]} (saved to header).")
            except Exception as e:
                self.status_label.setText(f"Error setting language: {e}")
        else:
            self.status_label.setText("No header packet found to update.")
            
        self.canvas.redraw()
        self.canvas.update()
        self.update_language_label()

    def prev_subpage(self):
        count = self.subpage_combo.count()
        if count <= 1: return
        
        current = self.subpage_combo.currentIndex()
        new_index = (current - 1) % count
        self.subpage_combo.setCurrentIndex(new_index)

    def next_subpage(self):
        count = self.subpage_combo.count()
        if count <= 1: return
        
        current = self.subpage_combo.currentIndex()
        new_index = (current + 1) % count
        self.subpage_combo.setCurrentIndex(new_index)

    def close_file(self):
        if not self.maybe_save_changes():
            return
            
        # Reset everything
        self.service = TeletextService()
        self.current_page = None
        self.current_file_path = None
        self.populate_list()
        self.subpage_combo.clear()
        self.page_groups = {}
        # Clear canvas
        self.canvas.set_page(None)
        # Maybe reset text of hex input
        self.hex_input.clear()
        
        self.undo_stack.clear()
        self.redo_stack.clear()
        self.language_overrides.clear()
        
        self.status_label.setText("File closed.")
        QTimer.singleShot(3000, lambda: self.status_label.setText("Ready"))
        self.set_modified(False)


    def open_file(self):
        if not self.maybe_save_changes():
            return

        fname, _ = QFileDialog.getOpenFileName(self, "Open T42", "", "Teletext Files (*.t42);;All Files (*)")
        if fname:
            try:
                self.progress_bar.setVisible(True)
                self.status_label.setText(f"Loading {os.path.basename(fname)}...")
                self.progress_bar.setValue(0)
                
                self.service = load_t42(fname, progress_callback=self.update_progress)
                self.current_file_path = fname
                self.populate_list()
                
                self.progress_bar.setVisible(False)
                self.status_label.setText(f"Loaded {len(self.service.pages)} pages from {os.path.basename(fname)}")
                
                self.undo_stack.clear()
                self.redo_stack.clear()
                self.set_modified(False)
            except Exception as e:
                self.progress_bar.setVisible(False)
                QMessageBox.critical(self, "Error", f"Failed to load file: {e}")
                self.status_label.setText("Error loading file")

    def save_as_file(self):
        fname, _ = QFileDialog.getSaveFileName(self, "Save T42 As...", "", "Teletext Files (*.t42)")
        if not fname: return
        
        self.current_file_path = fname
        self.save_file()

    def save_file(self) -> bool:
        if not self.current_file_path:
             # User requested status message instead of Save As behavior for empty state
             self.status_label.setText("No file loaded to save. Please use 'Save As...' or 'Open' first.")
             return False
        
        try:
            self.progress_bar.setVisible(True)
            self.status_label.setText(f"Saving {os.path.basename(self.current_file_path)}...")
            
            # Rebuild all_packets from pages to ensure edits/undos/new packets are included.
            # This serializes the pages in order, effectively "cleaning" the stream of orphans
            # and ensuring the file matches the editor state.
            new_all_packets = []
            for page in self.service.pages:
                new_all_packets.extend(page.packets)
            self.service.all_packets = new_all_packets
            
            save_t42(self.current_file_path, self.service, progress_callback=self.update_progress)
            
            self.progress_bar.setVisible(False)
            self.status_label.setText(f"Saved {len(self.service.pages)} pages to {os.path.basename(self.current_file_path)}")
            self.set_modified(False)
            return True
        except Exception as e:
            self.progress_bar.setVisible(False)
            QMessageBox.critical(self, "Error", f"Failed to save file: {e}")
            self.status_label.setText("Error saving file")
            return False

    def export_tti(self):
        if not self.current_page:
            QMessageBox.warning(self, "Export TTI", "No page selected to export.")
            return
            
        fname, _ = QFileDialog.getSaveFileName(self, "Export to TTI", "", "Teletext Text Image (*.tti)")
        if not fname: return
        
        try:
            save_tti(fname, self.current_page)
            QMessageBox.information(self, "Export Successful", f"Page exported to {os.path.basename(fname)}")
        except Exception as e:
            QMessageBox.critical(self, "Export Failed", f"Failed to export TTI: {e}")

    def open_find_dialog(self):
        if not self.find_dialog:
            self.find_dialog = FindDialog(self)
            self.find_dialog.findNext.connect(lambda q, h: self.execute_search(q, h, forward=True))
            self.find_dialog.findPrev.connect(lambda q, h: self.execute_search(q, h, forward=False))
            self.find_dialog.finished.connect(self.canvas.clear_highlight)
        
        self.find_dialog.show()
        self.find_dialog.raise_()
        self.find_dialog.activateWindow()
        self.find_dialog.query_input.setFocus()
        self.find_dialog.query_input.selectAll()

    def execute_search(self, query, is_hex, forward=True):
        # If query or mode changed, restart search
        is_new_search = False
        if not hasattr(self, '_last_query') or self._last_query != query or self._last_is_hex != is_hex:
            is_new_search = True
            self._last_query = query
            self._last_is_hex = is_hex
            self.search_results = []
            
            if is_hex:
                # Parse hex query: "1C 02" -> b'\x1c\x02'
                try:
                    clean_hex = query.replace(" ", "").replace("0x", "")
                    search_bytes = bytes.fromhex(clean_hex)
                except ValueError:
                    self.find_dialog.set_status("Invalid hex string")
                    return
            else:
                search_text = query.lower()

            # Iterate all pages
            for page in self.service.pages:
                # Determine language for this page
                lang = page.language
                key = (page.magazine, page.page_number)
                if key in self.language_overrides:
                    lang = self.language_overrides[key]
                
                # Check each packet
                for packet in page.packets:
                    if packet.row > 25: continue
                    
                    if is_hex:
                        # Hex search in raw data
                        # Strip parity for comparison? Teletext usually uses 7-bit + parity.
                        # The search should probably be parity-agnostic.
                        raw_data = bytes([b & 0x7F for b in packet.data])
                        idx = raw_data.find(search_bytes)
                        while idx != -1:
                            self.search_results.append((page, packet.row, idx, len(search_bytes)))
                            idx = raw_data.find(search_bytes, idx + 1)
                    else:
                        # Text search in decoded string
                        row_str = ""
                        for b in packet.data:
                            # Map byte to char, but treat control codes as spaces for search
                            bv = b & 0x7F
                            if bv < 0x20:
                                row_str += " "
                            else:
                                row_str += get_char(bv, lang)
                        
                        idx = row_str.lower().find(search_text)
                        while idx != -1:
                            self.search_results.append((page, packet.row, idx, len(search_text)))
                            idx = row_str.lower().find(search_text, idx + 1)

            if not self.search_results:
                self.find_dialog.set_status("No matches found")
                self.current_search_index = -1
                self.canvas.clear_highlight()
                return
            
            self.current_search_index = 0
        else:
            # Continue search
            if forward:
                self.current_search_index = (self.current_search_index + 1) % len(self.search_results)
            else:
                self.current_search_index = (self.current_search_index - 1) % len(self.search_results)

        self.find_dialog.set_status(f"Match {self.current_search_index + 1} of {len(self.search_results)}")
        self.show_search_result(self.search_results[self.current_search_index])

    def show_search_result(self, result):
        page, row, col, length = result
        
        # Switch to page
        mag_pnum_key = (page.magazine, page.page_number)
        
        # Find and select in list
        for i in range(self.page_list.count()):
            item = self.page_list.item(i)
            if item.data(Qt.ItemDataRole.UserRole) == mag_pnum_key:
                self.page_list.setCurrentItem(item)
                # This triggers on_page_selected which populates subpages
                break
        
        # Select correct subpage
        for i in range(self.subpage_combo.count()):
            if self.subpage_combo.itemData(i) == page:
                self.subpage_combo.setCurrentIndex(i)
                break
        
        # Highlight on canvas
        self.canvas.set_highlight(row, col, col + length - 1)
        # Also move cursor to start of match
        self.canvas.set_cursor(col, row)

    def copy_page_content(self):
        if not self.current_page:
            return
            
        self.clipboard = []
        # Copy rows 1 to 24 (or up to 25 if exists)
        # Skip row 0 (Header)
        for packet in self.current_page.packets:
            if 1 <= packet.row <= 24:
                # Store row number and copy of data
                self.clipboard.append((packet.row, bytearray(packet.data)))
        
        self.status_label.setText(f"Copied {len(self.clipboard)} rows to clipboard.")

    def paste_page_content(self):
        if not self.current_page:
            return
            
        if not self.clipboard:
            self.status_label.setText("Clipboard is empty.")
            return
            
        self.push_undo_state()
            
        # Paste content
        # Strategy: For each copied row, update existing packet or create new one.
        
        # Create a map of existing packets by row for quick lookup
        existing_packets = {p.row: p for p in self.current_page.packets}
        
        modified_count = 0
        
        for row, data in self.clipboard:
            if row in existing_packets:
                # Update existing
                packet = existing_packets[row]
                packet.data = bytearray(data) # Clone again to be safe
                # Note: Packet header (original_data) is now stale if we just update .data
                # but save functionality reconstructs header from .magazine and .row
                # so it should be fine.
                modified_count += 1
            else:
                # Create new packet
                new_packet = create_blank_packet(self.current_page.magazine, row)
                new_packet.data = bytearray(data)
                self.current_page.packets.append(new_packet)
                # Update lookup map just in case (though we won't hit it again usually)
                existing_packets[row] = new_packet
                modified_count += 1
                
        # Force redraw
        self.canvas.redraw()
        self.canvas.update()
        self.update_crc_display()
        self.status_label.setText(f"Pasted {modified_count} rows.")
        self.push_undo_state() # Push state after paste? NO, before! 
        # Wait, usually we push before modifying.
        # But here I just modified it. 
        # Correct pattern: Push state BEFORE modifying.
        # So I need to refactor paste_page_content to call push_undo_state() first.
        # For now, I'll add the methods here.

    def delete_page(self):
        if not self.current_page:
            return
            
        ret = QMessageBox.question(self, "Delete Page", 
                                   f"Are you sure you want to delete Page {self.current_page.full_page_number} (Sub {self.current_page.sub_code:04X})?",
                                   QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
                                   
        if ret == QMessageBox.StandardButton.Yes:
            # Remove from service.pages
            if self.current_page in self.service.pages:
                self.service.pages.remove(self.current_page)
                
            # Remove packets from all_packets
            # This is important for saving cleanly
            # Filter out packets that belong to this page instance
            # Note: We rely on object identity or need robust tracking. 
            # Since we reconstruct all_packets on save from pages, we don't strictly need to prune all_packets NOW,
            # but it's good practice or we can just rely on save_file's reconstruction logic.
            # save_file logic:
            # new_all_packets = []
            # for page in self.service.pages: ...
            # So removing from service.pages is sufficient for the next Save.
            
            self.set_modified(True)
            self.current_page = None
            self.canvas.set_page(None)
            self.populate_list()
            self.status_label.setText("Page deleted.")

    def push_undo_state(self):
        if not self.current_page: return
        self.canvas.clear_highlight()
        # Push deep copy of current page
        snapshot = copy.deepcopy(self.current_page)
        self.undo_stack.append(snapshot)
        self.redo_stack.clear() # Clear redo on new edit
        # Limit stack size
        if len(self.undo_stack) > 50:
            self.undo_stack.pop(0)
        self.set_modified(True)

    def undo(self):
        if not self.undo_stack:
            self.status_label.setText("Nothing to undo.")
            return
            
        # Push current state to redo
        if self.current_page:
            self.redo_stack.append(copy.deepcopy(self.current_page))
            
        snapshot = self.undo_stack.pop()
        self.restore_snapshot(snapshot)
        self.status_label.setText("Undone.")
        self.set_modified(True)

    def redo(self):
        if not self.redo_stack:
            self.status_label.setText("Nothing to redo.")
            return
            
        # Push current state to undo
        if self.current_page:
            self.undo_stack.append(copy.deepcopy(self.current_page))
            
        snapshot = self.redo_stack.pop()
        self.restore_snapshot(snapshot)
        self.status_label.setText("Redone.")
        self.set_modified(True)

    def restore_snapshot(self, snapshot: Page):
        # We need to update self.current_page content
        # AND update the usage in service.pages?
        # Actually, self.current_page IS the object in service.pages for now (referenced).
        # But we need to make sure we are modifying the SAME object or replacing it in the list.
        
        # Best way: Update attributes of self.current_page matches snapshot
        # snapshot is a Page object.
        if not self.current_page: return
        
        # Verify it's the same page ID just in case
        if (self.current_page.magazine != snapshot.magazine or 
            self.current_page.page_number != snapshot.page_number):
            # This is tricky if we changed pages. Undo should typically track page switches?
            # For now, we assume undo is local to editing the CURRENT page content.
            # If user switched pages, we might prevent undo or warn.
            # But let's just restoring content.
            pass

        self.current_page.packets = snapshot.packets
        # Also attributes
        self.current_page.sub_code = snapshot.sub_code
        
        self.canvas.set_page(self.current_page)
        self.canvas.redraw()
        self.canvas.update()
        self.update_crc_display()

    def populate_list(self):
        self.page_list.clear()
        
        # Group pages by Mag+PageNum
        # We want unique list items
        self.page_groups = {} # Key: (mag, page_num) -> List[Page]
        
        for p in self.service.pages:
            key = (p.magazine, p.page_number)
            if key not in self.page_groups:
                self.page_groups[key] = []
            self.page_groups[key].append(p)
            
        # Sort keys
        sorted_keys = sorted(self.page_groups.keys())
        
        for mag, pnum in sorted_keys:
            # Display as Hex
            label = f"{mag}{pnum:02X}"
            item = QListWidgetItem(label)
            item.setData(Qt.ItemDataRole.UserRole, (mag, pnum))
            self.page_list.addItem(item)

    def on_page_selected(self, item):
        mag, pnum = item.data(Qt.ItemDataRole.UserRole)
        pages = self.page_groups.get((mag, pnum), [])
        
        # Populate Subpage Combo
        self.subpage_combo.blockSignals(True)
        self.subpage_combo.clear()
        
        for i, p in enumerate(pages):
            # Try to find the clock in Row 0 (last 8 characters)
            clock_str = ""
            for pkt in p.packets:
                if pkt.row == 0:
                    # Bytes 32-39 of the 40-byte data are the clock
                    raw_clock = pkt.data[32:40].decode('latin-1', errors='replace')
                    # Strip parity from each char and filter non-printables
                    clock_str = "".join([chr(ord(c) & 0x7F) if 32 <= (ord(c) & 0x7F) <= 126 else " " for c in raw_clock])
                    break
            
            label = f"{i+1}/{len(pages)} "
            if clock_str.strip():
                label += f"[{clock_str.strip()}] "
            label += f"(Sub {p.sub_code:04X})"
            
            self.subpage_combo.addItem(label, p)
            
        self.subpage_combo.blockSignals(False)
        
        if pages:
            self.subpage_combo.setCurrentIndex(0)
            # Trigger update (manual because blockSignals)
            self.on_subpage_changed(0)
            
    def on_subpage_changed(self, index):
        if index < 0: return
        page = self.subpage_combo.itemData(index)
        if isinstance(page, Page):
            self.current_page = page
            self.canvas.set_page(page)
            
            # Apply session language override if it exists for this page
            key = (page.magazine, page.page_number)
            if key in self.language_overrides:
                self.canvas.subset_idx = self.language_overrides[key]
                self.canvas.redraw()
                self.canvas.update()
                
            self.update_language_label()
            self.update_crc_display()
            self.canvas.setFocus()

    def insert_char(self, char_code):
        self.push_undo_state()
        self.canvas.set_byte_at_cursor(char_code)
        # Advance cursor
        self.canvas.move_cursor(1, 0)
        self.canvas.setFocus()

    def insert_color(self, base_code):
        code = base_code
        if self.chk_graphics.isChecked():
            # Convert 0x01..0x07 to 0x11..0x17
            code += 0x10
        self.insert_char(code)

    def open_mosaic_dialog(self):
        dlg = MosaicDialog(self)
        dlg.exec()
            
    def on_cursor_changed(self, x, y, val, is_graphics):
        self.hex_input.setText(f"{val:02X}")
        mode_str = "Graphics" if is_graphics else "Text"
        self.mode_label.setText(f"Mode: {mode_str}")
        self.update_crc_display()
        
    def on_hex_entered(self):
        text = self.hex_input.text()
        try:
            val = int(text, 16)
            if 0 <= val <= 255:
                # Update canvas input
                # We can call handle_input with char, OR set byte directly. 
                # Direct byte set is safer for non-printable.
                self.push_undo_state()
                self.canvas.set_byte_at_cursor(val)
                self.canvas.setFocus() # Return focus to canvas
        except ValueError:
            pass # Ignore invalid hex
            
    # Input Handling (Editor Logic)
    def keyPressEvent(self, event: QKeyEvent):
        if not self.current_page:
            return
            
        key = event.key()
        text = event.text()
        
        # Navigation
        if key == Qt.Key.Key_Up:
            self.canvas.move_cursor(0, -1)
        elif key == Qt.Key.Key_Down:
            self.canvas.move_cursor(0, 1)
        elif key == Qt.Key.Key_Left:
            self.canvas.move_cursor(-1, 0)
        elif key == Qt.Key.Key_Right:
            self.canvas.move_cursor(1, 0)
        elif key == Qt.Key.Key_Home:
            # Move to start of line
            self.canvas.set_cursor(0, self.canvas.cursor_y)
        elif key == Qt.Key.Key_End:
            # Move to end of line (39)
            self.canvas.set_cursor(39, self.canvas.cursor_y)
        elif key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
            # Move to start of next line
            self.canvas.cursor_x = 0
            self.canvas.move_cursor(0, 1)
        else:
            # Typing
            # Allow wider range of chars for national support
            if text and len(text) == 1 and ord(text) >= 32:
                self.push_undo_state()
                self.canvas.handle_input(text)
            elif key == Qt.Key.Key_Backspace:
                self.push_undo_state()
                # Move back and delete
                self.canvas.move_cursor(-1, 0)
                self.canvas.handle_input(' ')
                self.canvas.move_cursor(-1, 0) # Compensate for the auto-advance logic if any

