package cz.kamma.kfmanager.ui;

import cz.kamma.kfmanager.config.AppConfig;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.InputEvent;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;

/**
 * Internal file editor/viewer
 */
public class FileEditor extends JDialog {
    private JTextArea textArea;
    private File file;
    private AppConfig config;
    private boolean modified = false;
    private boolean readOnly;
    private JLabel statusPosLabel;
    private JLabel statusSelLabel;
    // Hex view support
    private boolean hexMode = false;
    private byte[] fileBytes = null;
    // For mapping byte index -> text offset in hex dump (for current page)
    private java.util.List<Integer> byteTextOffsets = new java.util.ArrayList<>();
    // Paged streaming fields
    private RandomAccessFile raf = null;
    private long fileLength = 0L;
    private long pageOffsetBytes = 0L;
    private final int pageSizeBytes = 64 * 1024; // 64 KB page size
    // Allow loading entire file into memory up to this limit (100 MB)
    private final long maxFullLoadBytes = 100L * 1024L * 1024L; // 100 MB
    private JPanel hexControlPanel = null;
    private JButton prevPageBtn = null;
    private JButton nextPageBtn = null;
    private JLabel pageOffsetLabel = null;
    private javax.swing.undo.UndoManager undoManager;
    
    public FileEditor(Window parent, File file, AppConfig config, boolean readOnly) {
        super(parent, (readOnly ? "Prohlížeč - " : "Editor - ") + file.getName(), ModalityType.MODELESS);
        this.file = file;
        this.config = config;
        this.readOnly = readOnly;
        
        initComponents();
    loadFile();
        
        setSize(800, 600);
        setLocationRelativeTo(parent);
        // Intercept window close (X) so we run the same save-confirm flow as other close actions
        setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        addWindowListener(new java.awt.event.WindowAdapter() {
            @Override
            public void windowClosing(java.awt.event.WindowEvent e) {
                closeEditor();
            }
        });
    }
    
    private void initComponents() {
        setLayout(new BorderLayout());
        
        // Menu bar
        createMenuBar();
        
        // Textová oblast (editable nebo read-only)
        textArea = new JTextArea();
        textArea.setFont(config.getEditorFont());
        textArea.setTabSize(4);
        textArea.setEditable(!readOnly);
        
        // Undo/Redo support
        undoManager = new javax.swing.undo.UndoManager();
        textArea.getDocument().addUndoableEditListener(e -> {
            undoManager.addEdit(e.getEdit());
        });
        
        // Sledování změn (pouze pro editovatelný režim)
        if (!readOnly) {
            textArea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
                public void insertUpdate(javax.swing.event.DocumentEvent e) { setModified(true); }
                public void removeUpdate(javax.swing.event.DocumentEvent e) { setModified(true); }
                public void changedUpdate(javax.swing.event.DocumentEvent e) { setModified(true); }
            });
        }
        
        JScrollPane scrollPane = new JScrollPane(textArea);
        add(scrollPane, BorderLayout.CENTER);
        // Status bar (position, selection)
        JPanel statusPanel = new JPanel(new BorderLayout());
        statusPosLabel = new JLabel(" ");
        statusSelLabel = new JLabel(" ");
        statusPosLabel.setBorder(BorderFactory.createEmptyBorder(2,6,2,6));
        statusSelLabel.setBorder(BorderFactory.createEmptyBorder(2,6,2,6));
        statusPanel.add(statusPosLabel, BorderLayout.WEST);
        statusPanel.add(statusSelLabel, BorderLayout.EAST);
        add(statusPanel, BorderLayout.SOUTH);
        
        // Apply appearance settings (colors, contrast caret etc)
        applyAppearance();

        // Kontextové menu pro clipboard
        setupContextMenu();

        // Klávesové zkratky
        setupKeyBindings();

        // Caret listener to update position and selection info
        textArea.addCaretListener(e -> updateStatus());
        // Also update status when document changes (to refresh byte counts)
        textArea.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
            public void insertUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); }
            public void removeUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); }
            public void changedUpdate(javax.swing.event.DocumentEvent e) { updateStatus(); }
        });
    }
    
    private void setupContextMenu() {
        JPopupMenu popup = new JPopupMenu();
        
        JMenuItem undoItem = new JMenuItem("Zpět");
        undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK));
        undoItem.addActionListener(e -> { if (undoManager.canUndo()) undoManager.undo(); });
        
        JMenuItem redoItem = new JMenuItem("Znovu");
        redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK));
        redoItem.addActionListener(e -> { if (undoManager.canRedo()) undoManager.redo(); });
        
        popup.add(undoItem);
        popup.add(redoItem);
        popup.addSeparator();

        JMenuItem cutItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CutAction());
        cutItem.setText("Vyjmout");
        cutItem.setMnemonic(KeyEvent.VK_X);
        
        JMenuItem copyItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CopyAction());
        copyItem.setText("Kopírovat");
        copyItem.setMnemonic(KeyEvent.VK_C);
        
        JMenuItem pasteItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.PasteAction());
        pasteItem.setText("Vložit");
        pasteItem.setMnemonic(KeyEvent.VK_V);

        popup.add(cutItem);
        popup.add(copyItem);
        popup.add(pasteItem);
        popup.addSeparator();
        
        JMenuItem selectAllItem = new JMenuItem("Vybrat vše");
        selectAllItem.setMnemonic(KeyEvent.VK_A);
        selectAllItem.addActionListener(e -> textArea.selectAll());
        popup.add(selectAllItem);

        popup.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent e) {
                boolean editable = textArea.isEditable();
                undoItem.setEnabled(editable && undoManager.canUndo());
                redoItem.setEnabled(editable && undoManager.canRedo());
                cutItem.setEnabled(editable);
                pasteItem.setEnabled(editable);
            }
            @Override public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent e) {}
            @Override public void popupMenuCanceled(javax.swing.event.PopupMenuEvent e) {}
        });

        textArea.setComponentPopupMenu(popup);
    }
    
    private void applyAppearance() {
        Color bg = config.getBackgroundColor();
        if (bg != null) {
            boolean dark = isDark(bg);
            // Apply to JTextArea
            textArea.setBackground(bg);
            textArea.setForeground(dark ? Color.WHITE : Color.BLACK);
            textArea.setCaretColor(dark ? Color.WHITE : Color.BLACK);
            
            // Apply to the rest of the components recursively
            applyRecursiveColors(getContentPane(), bg, dark);
        } else {
            // Default colors - make sure caret is at least visible
            Color defaultBg = textArea.getBackground();
            if (defaultBg != null) {
                boolean dark = isDark(defaultBg);
                textArea.setCaretColor(dark ? Color.WHITE : Color.BLACK);
            } else {
                textArea.setCaretColor(Color.BLACK);
            }
        }
        
        // Force caret visibility in read-only mode
        if (readOnly) {
            textArea.getCaret().setVisible(true);
        }
    }

    private void applyRecursiveColors(Container container, Color bg, boolean dark) {
        if (container == null) return;
        container.setBackground(bg);
        for (Component c : container.getComponents()) {
            if (c instanceof JPanel || c instanceof JScrollPane || c instanceof JViewport) {
                c.setBackground(bg);
            }
            if (c instanceof JLabel) {
                c.setForeground(dark ? Color.WHITE : Color.BLACK);
            }
            if (c instanceof Container) {
                applyRecursiveColors((Container) c, bg, dark);
            }
        }
    }

    private boolean isDark(Color c) {
        if (c == null) return false;
        double darkness = 1 - (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255;
        return darkness >= 0.5;
    }

    private void createMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        
    // File menu
    JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic(java.awt.event.KeyEvent.VK_S);
        
        // Uložit - pouze v editovacím režimu
        if (!readOnly) {
            JMenuItem saveItem = new JMenuItem("Uložit");
            saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0));
            saveItem.addActionListener(e -> saveFile());
            fileMenu.add(saveItem);
            
            fileMenu.addSeparator();
        }
        
        JMenuItem closeItem = new JMenuItem("Zavřít");
        closeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
        closeItem.addActionListener(e -> closeEditor());
        fileMenu.add(closeItem);
        
        menuBar.add(fileMenu);
        
        // Menu Edit
        JMenu editMenu = new JMenu("Edit");
        editMenu.setMnemonic(java.awt.event.KeyEvent.VK_E);

        JMenuItem undoItem = new JMenuItem("Zpět");
        undoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK));
        undoItem.addActionListener(e -> { if (undoManager.canUndo()) undoManager.undo(); });
        editMenu.add(undoItem);

        JMenuItem redoItem = new JMenuItem("Znovu");
        redoItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK));
        redoItem.addActionListener(e -> { if (undoManager.canRedo()) undoManager.redo(); });
        editMenu.add(redoItem);

        editMenu.addSeparator();

        JMenuItem cutItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CutAction());
        cutItem.setText("Vyjmout");
        cutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK));
        editMenu.add(cutItem);

        JMenuItem copyItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.CopyAction());
        copyItem.setText("Kopírovat");
        copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK));
        editMenu.add(copyItem);

        JMenuItem pasteItem = new JMenuItem(new javax.swing.text.DefaultEditorKit.PasteAction());
        pasteItem.setText("Vložit");
        pasteItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_DOWN_MASK));
        editMenu.add(pasteItem);

        editMenu.addSeparator();

        JMenuItem selectAllItem = new JMenuItem("Vybrat vše");
        selectAllItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK));
        selectAllItem.addActionListener(e -> textArea.selectAll());
        editMenu.add(selectAllItem);

        // Update menu items state when menu is selected
        editMenu.addMenuListener(new javax.swing.event.MenuListener() {
            @Override
            public void menuSelected(javax.swing.event.MenuEvent e) {
                boolean editable = textArea.isEditable();
                undoItem.setEnabled(editable && undoManager.canUndo());
                redoItem.setEnabled(editable && undoManager.canRedo());
                cutItem.setEnabled(editable);
                pasteItem.setEnabled(editable);
            }
            @Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
            @Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
        });

        menuBar.add(editMenu);
        
        // Menu Nastavení
        JMenu settingsMenu = new JMenu("Nastavení");
        settingsMenu.setMnemonic(java.awt.event.KeyEvent.VK_N);
        
        JMenuItem fontItem = new JMenuItem("Font...");
        fontItem.addActionListener(e -> changeFont());
        settingsMenu.add(fontItem);
        
        menuBar.add(settingsMenu);
        
        // Add View menu (hex toggle)
        createViewMenu(menuBar);
        setJMenuBar(menuBar);
    }

    private void createViewMenu(JMenuBar menuBar) {
        JMenu viewMenu = new JMenu("View");
        JCheckBoxMenuItem hexItem = new JCheckBoxMenuItem("Hex view");
        hexItem.setState(hexMode);
        hexItem.addActionListener(e -> {
            boolean newState = hexItem.getState();
            setHexMode(newState);
        });
        viewMenu.add(hexItem);
        menuBar.add(viewMenu);
    }

    private void ensureHexControls() {
        if (hexControlPanel != null) return;
        hexControlPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        prevPageBtn = new JButton("◀ Prev");
        nextPageBtn = new JButton("Next ▶");
        pageOffsetLabel = new JLabel("Offset: 0x0");

        prevPageBtn.addActionListener(e -> prevHexPage());
        nextPageBtn.addActionListener(e -> nextHexPage());

        hexControlPanel.add(prevPageBtn);
        hexControlPanel.add(pageOffsetLabel);
        hexControlPanel.add(nextPageBtn);
    }
    
    private void setupKeyBindings() {
        JRootPane rootPane = getRootPane();
        
        // F2 - Uložit (pouze v editovacím režimu)
        if (!readOnly) {
            rootPane.registerKeyboardAction(e -> saveFile(),
                    KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0),
                    JComponent.WHEN_IN_FOCUSED_WINDOW);
        }
        
        // ESC - Zavřít
        rootPane.registerKeyboardAction(e -> closeEditor(),
                KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
                JComponent.WHEN_IN_FOCUSED_WINDOW);
        
        // F3/F4 - Zavřít
        rootPane.registerKeyboardAction(e -> closeEditor(),
                KeyStroke.getKeyStroke(readOnly ? KeyEvent.VK_F3 : KeyEvent.VK_F4, 0),
                JComponent.WHEN_IN_FOCUSED_WINDOW);

        // Ctrl+S - Save (common shortcut) but show confirmation dialog when there are unsaved changes
        if (!readOnly) {
            rootPane.registerKeyboardAction(e -> handleCtrlS(),
                    KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK),
                    JComponent.WHEN_IN_FOCUSED_WINDOW);
        }
        // PageUp/PageDown when in hex mode -> move pages
        rootPane.registerKeyboardAction(e -> {
                    if (hexMode) prevHexPage();
                },
                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
                JComponent.WHEN_IN_FOCUSED_WINDOW);
        rootPane.registerKeyboardAction(e -> {
                    if (hexMode) nextHexPage();
                },
                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
                JComponent.WHEN_IN_FOCUSED_WINDOW);
        
        // Ctrl+Z - Undo
        rootPane.registerKeyboardAction(e -> {
            if (undoManager.canUndo()) undoManager.undo();
        }, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
        
        // Ctrl+Y - Redo
        rootPane.registerKeyboardAction(e -> {
            if (undoManager.canRedo()) undoManager.redo();
        }, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);

        // Ctrl+Shift+Z - Redo (alternative shortcut)
        rootPane.registerKeyboardAction(e -> {
            if (undoManager.canRedo()) undoManager.redo();
        }, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    private void setHexMode(boolean on) {
        this.hexMode = on;
        if (on) {
            // ensure bytes loaded
            try {
                // For large files, prefer streaming only when file exceeds maxFullLoadBytes
                long size = Files.size(file.toPath());
                if (size > maxFullLoadBytes) {
                    // Open RAF for streaming
                    if (raf == null) raf = new RandomAccessFile(file, "r");
                    fileLength = raf.length();
                    pageOffsetBytes = 0L;
                    loadHexPage();
                } else {
                    if (fileBytes == null) fileBytes = java.nio.file.Files.readAllBytes(file.toPath());
                    buildHexViewText(0L);
                }
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(this, "Cannot load file for hex view: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
                return;
            }
            textArea.setEditable(false);
            // ensure monospaced font for alignment
            textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize()));
            ensureHexControls();
            if (hexControlPanel.getParent() == null) {
                add(hexControlPanel, BorderLayout.NORTH);
            }
            hexControlPanel.setVisible(true);
        } else {
            // switch back to text view if possible
            // close RA if open
            try { if (raf != null) { raf.close(); raf = null; } } catch (Exception ignore) {}
            pageOffsetBytes = 0L;
            fileLength = 0L;
            loadFile();
            textArea.setEditable(!readOnly);
            textArea.setFont(config.getEditorFont());
        }
        applyAppearance();
        updateStatus();
    }

    private void buildHexViewText(long baseOffset) {
        byteTextOffsets.clear();
        if (fileBytes == null) fileBytes = new byte[0];
        StringBuilder sb = new StringBuilder();
        int bytesPerLine = 16;
        for (int i = 0; i < fileBytes.length; i += bytesPerLine) {
            long displayOffset = baseOffset + i;
            sb.append(String.format("%08X  ", displayOffset));
            // hex bytes
            for (int j = 0; j < bytesPerLine; j++) {
                int idx = i + j;
                if (idx < fileBytes.length) {
                    int b = fileBytes[idx] & 0xFF;
                    int pos = sb.length();
                    byteTextOffsets.add(pos);
                    sb.append(String.format("%02X", b));
                } else {
                    // placeholder for missing byte
                    sb.append("  ");
                }
                if (j != bytesPerLine - 1) sb.append(' ');
                if (j == 7) sb.append(' '); // extra gap after 8 bytes
            }
            sb.append("  ");
            // ASCII representation
            for (int j = 0; j < bytesPerLine; j++) {
                int idx = i + j;
                if (idx < fileBytes.length) {
                    int b = fileBytes[idx] & 0xFF;
                    char c = (b >= 0x20 && b <= 0x7E) ? (char) b : '.';
                    sb.append(c);
                } else {
                    sb.append(' ');
                }
            }
            if (i + bytesPerLine < fileBytes.length) sb.append('\n');
        }
        textArea.setText(sb.toString());
        // Ensure caret stays within the rendered region; caller may set caret to preferred pos
        if (textArea.getCaretPosition() > textArea.getText().length()) {
            textArea.setCaretPosition(0);
        }
        // Ensure byteTextOffsets has one entry per byte (if some bytes were skipped, fill with -1)
        while (byteTextOffsets.size() < fileBytes.length) {
            byteTextOffsets.add(textArea.getText().length());
        }
        // Ensure byteTextOffsets length equals fileBytes length
        // If some bytes were missing due to empty file, leave as is
        // After building the view text, position caret to the start of the rendered data
        SwingUtilities.invokeLater(() -> {
            try {
                if (byteTextOffsets != null && !byteTextOffsets.isEmpty()) {
                    int pos = Math.max(0, byteTextOffsets.get(0));
                    textArea.setCaretPosition(pos);
                    // ensure the caret is visible at top-left
                    Rectangle vis = textArea.getVisibleRect();
                    Rectangle r = textArea.modelToView2D(pos).getBounds();
                    if (r != null) {
                        textArea.scrollRectToVisible(new Rectangle(r.x, r.y, vis.width, vis.height));
                    }
                } else {
                    textArea.setCaretPosition(0);
                }
            } catch (Exception ignore) {}
        });
    }

    private void loadHexPage() {
        if (raf == null) return;
        try {
            raf.seek(pageOffsetBytes);
            int toRead = (int)Math.min(pageSizeBytes, fileLength - pageOffsetBytes);
            byte[] page = new byte[toRead];
            raf.readFully(page);
            this.fileBytes = page;
            buildHexViewText(pageOffsetBytes);
            if (pageOffsetLabel != null) {
                long start = pageOffsetBytes;
                long end = pageOffsetBytes + page.length - 1;
                pageOffsetLabel.setText(String.format("Offset: 0x%08X - 0x%08X", start, end));
            }
            // Position caret at first byte hex position for consistent mapping
            if (byteTextOffsets != null && !byteTextOffsets.isEmpty()) {
                int pos = byteTextOffsets.get(0);
                textArea.setCaretPosition(Math.max(0, pos));
            } else {
                textArea.setCaretPosition(0);
            }
        } catch (IOException e) {
            JOptionPane.showMessageDialog(this, "Error reading file page: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }

    private void prevHexPage() {
        if (pageOffsetBytes <= 0) return;
        pageOffsetBytes = Math.max(0, pageOffsetBytes - pageSizeBytes);
        pageOffsetBytes = (pageOffsetBytes / 16) * 16;
        loadHexPage();
    }

    private void nextHexPage() {
        if (pageOffsetBytes + pageSizeBytes >= fileLength) return;
        pageOffsetBytes = pageOffsetBytes + pageSizeBytes;
        pageOffsetBytes = (pageOffsetBytes / 16) * 16;
        loadHexPage();
    }

    /**
     * Handle Ctrl+S: show the save/confirm dialog if document was modified, otherwise perform a save.
     */
    private void handleCtrlS() {
        if (readOnly) return;
        if (!modified) {
            // No changes - inform the user briefly
            JOptionPane.showMessageDialog(this, "Žádné změny k uložení.", "Info", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Uložit změny");

        if (result == JOptionPane.YES_OPTION) {
            saveFile();
        } else if (result == JOptionPane.NO_OPTION) {
            // Do nothing (user decided not to save)
        } else {
            // CANCEL - do nothing
        }
    }
    
    private void loadFile() {
        try {
            // Determine file size first to decide streaming vs full load
            long size = Files.size(file.toPath());
            // Read a small probe to detect binary nature without loading whole file
            int probeLen = (int)Math.min(512, size);
            byte[] probe = new byte[probeLen];
            try (java.io.InputStream is = Files.newInputStream(file.toPath())) {
                int read = is.read(probe);
                if (read < probeLen) {
                    byte[] tmp = new byte[read];
                    System.arraycopy(probe, 0, tmp, 0, read);
                    probe = tmp;
                }
            }

            boolean binaryProbe = isBinary(probe);

            if (binaryProbe && readOnly && size > maxFullLoadBytes) {
                // Open RAF and stream pages
                if (raf == null) raf = new RandomAccessFile(file, "r");
                fileLength = raf.length();
                pageOffsetBytes = 0L;
                loadHexPage();
                ensureHexControls();
                if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH);
                hexControlPanel.setVisible(true);
                textArea.setEditable(false);
                textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize()));
                hexMode = true;
            } else {
                // Small or text file: load fully
                fileBytes = Files.readAllBytes(file.toPath());
                boolean binary = isBinary(fileBytes);
                if (binary && readOnly) {
                    hexMode = true;
                    buildHexViewText(0L);
                    textArea.setEditable(false);
                    textArea.setFont(new Font("Monospaced", Font.PLAIN, textArea.getFont().getSize()));
                    ensureHexControls();
                    if (hexControlPanel.getParent() == null) add(hexControlPanel, BorderLayout.NORTH);
                    hexControlPanel.setVisible(true);
                } else if (hexMode) {
                    buildHexViewText(0L);
                } else {
                    String content = new String(fileBytes, "UTF-8");
                    textArea.setText(content);
                    textArea.setCaretPosition(0);
                }
            }
            if (undoManager != null) undoManager.discardAllEdits();
            modified = false;
            updateTitle();
            updateStatus();
        } catch (IOException e) {
            textArea.setText("Error loading file:\n" + e.getMessage());
        }
    }

    private boolean isBinary(byte[] bytes) {
        if (bytes == null || bytes.length == 0) return false;
        int nonPrintable = 0;
        int len = Math.min(bytes.length, 512);
        for (int i = 0; i < len; i++) {
            int b = bytes[i] & 0xFF;
            if (b == 0) return true; // NUL -> binary
            if (b < 0x09) return true;
            if (b > 0x7E) nonPrintable++;
        }
        return nonPrintable > (len / 4);
    }
    
    private void saveFile() {
        try {
            String content = textArea.getText();
            Files.write(file.toPath(), content.getBytes("UTF-8"));
            modified = false;
            updateTitle();
            JOptionPane.showMessageDialog(this, 
                    "Soubor uložen", 
                    "Úspěch", 
                    JOptionPane.INFORMATION_MESSAGE);
            updateStatus();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(this, 
                    "Chyba při ukládání:\n" + e.getMessage(), 
                    "Chyba", 
                    JOptionPane.ERROR_MESSAGE);
        }
    }
    
    private void closeEditor() {
        if (!readOnly && modified) {
            int result = showSaveConfirmDialog("Soubor byl změněn. Uložit změny?", "Neuložené změny");

            if (result == JOptionPane.YES_OPTION) {
                saveFile();
                dispose();
            } else if (result == JOptionPane.NO_OPTION) {
                dispose();
            }
            // CANCEL_OPTION - nedělat nic
        } else {
            dispose();
        }
    }
    
    private void setModified(boolean modified) {
        this.modified = modified;
        updateTitle();
    }
    
    private void updateTitle() {
        String title = (readOnly ? "Prohlížeč - " : "Editor - ") + file.getName();
        if (!readOnly && modified) {
            title += " *";
        }
        setTitle(title);
    }
    
    private void changeFont() {
        Font newFont = FontChooserDialog.showDialog(this, textArea.getFont());
        if (newFont != null) {
            textArea.setFont(newFont);
            config.setEditorFont(newFont);
            config.saveConfig();
            updateStatus();
        }
    }

    /**
     * Show a modal save-confirm dialog with Yes/No/Cancel options.
     * Buttons can be switched using left/right arrow keys and activated with Enter.
     * Returns JOptionPane.YES_OPTION / NO_OPTION / CANCEL_OPTION
     */
    private int showSaveConfirmDialog(String message, String title) {
        final JDialog dlg = new JDialog(this, title, Dialog.ModalityType.APPLICATION_MODAL);
        dlg.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        dlg.setLayout(new BorderLayout(8, 8));
        dlg.add(new JLabel(message), BorderLayout.CENTER);

        JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        JButton yesBtn = new JButton("Ano");
        JButton noBtn = new JButton("Ne");
        JButton cancelBtn = new JButton("Zrušit");

        final int[] result = {JOptionPane.CANCEL_OPTION};

        yesBtn.addActionListener(e -> {
            result[0] = JOptionPane.YES_OPTION;
            dlg.dispose();
        });
        noBtn.addActionListener(e -> {
            result[0] = JOptionPane.NO_OPTION;
            dlg.dispose();
        });
        cancelBtn.addActionListener(e -> {
            result[0] = JOptionPane.CANCEL_OPTION;
            dlg.dispose();
        });

        btnPanel.add(yesBtn);
        btnPanel.add(noBtn);
        btnPanel.add(cancelBtn);
        dlg.add(btnPanel, BorderLayout.SOUTH);

        // Key bindings: left/right to move focus between buttons, Enter to press, ESC to cancel
        JRootPane root = dlg.getRootPane();

        Action focusLeft = new AbstractAction() {
            @Override public void actionPerformed(java.awt.event.ActionEvent e) {
                java.awt.Component c = dlg.getFocusOwner();
                java.awt.Component[] comps = {yesBtn, noBtn, cancelBtn};
                int idx = -1;
                for (int i = 0; i < comps.length; i++) if (comps[i] == c) { idx = i; break; }
                if (idx == -1) { yesBtn.requestFocusInWindow(); return; }
                int prev = (idx - 1 + comps.length) % comps.length;
                comps[prev].requestFocusInWindow();
            }
        };
        Action focusRight = new AbstractAction() {
            @Override public void actionPerformed(java.awt.event.ActionEvent e) {
                java.awt.Component c = dlg.getFocusOwner();
                java.awt.Component[] comps = {yesBtn, noBtn, cancelBtn};
                int idx = -1;
                for (int i = 0; i < comps.length; i++) if (comps[i] == c) { idx = i; break; }
                if (idx == -1) { yesBtn.requestFocusInWindow(); return; }
                int next = (idx + 1) % comps.length;
                comps[next].requestFocusInWindow();
            }
        };

        root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "focusLeft");
        root.getActionMap().put("focusLeft", focusLeft);
        root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "focusRight");
        root.getActionMap().put("focusRight", focusRight);

        // Enter -> click focused button
        root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "press");
        root.getActionMap().put("press", new AbstractAction() {
            @Override public void actionPerformed(java.awt.event.ActionEvent e) {
                java.awt.Component c = dlg.getFocusOwner();
                if (c instanceof JButton) ((JButton)c).doClick();
            }
        });

        // ESC -> cancel
        root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "esc");
        root.getActionMap().put("esc", new AbstractAction() {
            @Override public void actionPerformed(java.awt.event.ActionEvent e) {
                result[0] = JOptionPane.CANCEL_OPTION;
                dlg.dispose();
            }
        });

        dlg.pack();
        dlg.setLocationRelativeTo(this);
        // Focus yes button initially
        SwingUtilities.invokeLater(() -> yesBtn.requestFocusInWindow());
        dlg.setVisible(true);
        return result[0];
    }

    /** Update status bar: caret position (line:col, offset, percent) and selection count (chars/bytes) */
    private void updateStatus() {
        SwingUtilities.invokeLater(() -> {
            try {
                if (hexMode && fileBytes != null) {
                    // When streaming (paged) we prefer to show the offset corresponding
                    // to the real visible position in the file (top of viewport). This
                    // makes the displayed offset follow scrolling (PgUp/PgDn or scrollbar)
                    // rather than only caret movement.
                    int refPos;
                        try {
                            Rectangle vis = textArea.getVisibleRect();
                            Point topLeft = new Point(vis.x, vis.y);
                            refPos = textArea.viewToModel2D(topLeft);
                        } catch (Exception ex) {
                            // fallback to caret position if viewToModel fails
                            refPos = textArea.getCaretPosition();
                        }
                    int caret = refPos;
                    long byteIndex = mapCaretToByteIndex(caret);
                    long totalBytes = (raf != null && fileLength > 0) ? fileLength : (fileBytes != null ? fileBytes.length : 0);
                    int percent = totalBytes > 0 ? (int)((byteIndex * 100L) / totalBytes) : 0;
                    statusPosLabel.setText(String.format("Offset %d/%d (%d%%)", byteIndex, totalBytes, percent));

                    int selStart = textArea.getSelectionStart();
                    int selEnd = textArea.getSelectionEnd();
                    long selBytes = 0;
                    if (selEnd > selStart) {
                        long b1 = mapCaretToByteIndex(selStart);
                        long b2 = mapCaretToByteIndex(Math.max(selStart, selEnd-1));
                        selBytes = Math.max(0L, b2 - b1 + 1L);
                    }
                    if (selBytes > 0) {
                        statusSelLabel.setText(String.format("Selected: %d bytes", selBytes));
                    } else {
                        statusSelLabel.setText(" ");
                    }
                
                } else {
                    int caret = textArea.getCaretPosition();
                    String text = textArea.getText();
                    int total = text != null ? text.length() : 0;
                    int line = 0, col = 0;
                    if (total == 0) {
                        line = 0; col = 0;
                    } else {
                        line = textArea.getLineOfOffset(Math.max(0, caret)) + 1;
                        int lineStart = textArea.getLineStartOffset(Math.max(0, line-1));
                        col = caret - lineStart + 1;
                    }
                    int percent = total > 0 ? (int) ((caret * 100L) / total) : 0;
                    statusPosLabel.setText(String.format("Ln %d, Col %d  |  Offset %d/%d (%d%%)", line, col, caret, total, percent));

                    int selStart = textArea.getSelectionStart();
                    int selEnd = textArea.getSelectionEnd();
                    int selChars = Math.max(0, selEnd - selStart);
                    int selBytes = 0;
                    if (selChars > 0) {
                        try {
                            String selText = textArea.getDocument().getText(selStart, selChars);
                            selBytes = selText.getBytes("UTF-8").length;
                        } catch (Exception ex) {
                            selBytes = 0;
                        }
                    }
                    if (selChars > 0) {
                        statusSelLabel.setText(String.format("Selected: %d chars / %d bytes", selChars, selBytes));
                    } else {
                        statusSelLabel.setText(" ");
                    }
                }
            } catch (Exception ex) {
                // ignore status update errors
            }
        });
    }

    private long mapCaretToByteIndex(int caretPos) {
        if (byteTextOffsets == null || byteTextOffsets.isEmpty()) return 0L;
        // find greatest index i such that byteTextOffsets.get(i) <= caretPos
        int idx = java.util.Collections.binarySearch(byteTextOffsets, caretPos);
        if (idx >= 0) return (long) idx + pageOffsetBytes;
        int ins = -idx - 1;
        int candidate = Math.max(0, ins - 1);
        int local = Math.min(candidate, Math.max(0, (fileBytes != null ? fileBytes.length : 0) - 1));
        long global = (long)local + pageOffsetBytes;
        if (fileLength > 0) global = Math.min(global, fileLength - 1);
        return global;
    }

    @Override
    public void dispose() {
        try {
            if (raf != null) raf.close();
        } catch (Exception ignore) {}
        super.dispose();
    }
}
