package cz.kamma.kfmanager.ui;

import cz.kamma.kfmanager.model.FileItem;
import cz.kamma.kfmanager.service.ClipboardService;
import cz.kamma.kfmanager.service.FileOperations;

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import com.github.junrar.Archive;
import com.github.junrar.rarfile.FileHeader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

/**
 * Single tab in a panel - displays the contents of one directory
 */
public class FilePanelTab extends JPanel {
    
    private File currentDirectory;
    private JTable fileTable;
    private FileTableModel tableModel;
    private JLabel statusLabel;
    private ViewMode viewMode = ViewMode.FULL;
    private int briefCurrentColumn = 0;
    private Runnable onDirectoryChanged;
    private Runnable onSwitchPanelRequested;
    // Appearance customization
    private Color selectionColor = new Color(184, 207, 229);
    private Color markedColor = new Color(204, 153, 0);
    // Sorting state for FULL mode header clicks
    private int sortColumn = -1; // 0=name,1=size,2=date
    private boolean sortAscending = true;
    private cz.kamma.kfmanager.config.AppConfig persistedConfig;
    // Track last selection to restore it if focus is requested on empty area
    private int lastValidRow = 0;
    private int lastValidBriefColumn = 0;
    // If an archive was opened, we may extract it to a temp directory; track it so
    // we can cleanup older temp directories when navigation changes.
    private Path currentArchiveTempDir = null;
    private File currentArchiveSourceFile = null;
    private boolean inlineRenameActive = false;
    
    public FilePanelTab(String initialPath) {
        this(initialPath, true);
    }
    
    public FilePanelTab(String initialPath, boolean requestFocus) {
        this.currentDirectory = new File(initialPath);
        initComponents();
        loadDirectory(currentDirectory, true, requestFocus);
    }

    /** Start inline rename for currently selected item (if single selection). */
    public void startInlineRename() {
        // Determine selected row/column according to view mode
        int selRow = fileTable.getSelectedRow();
        if (selRow < 0) return;

        int editCol = 0;
        if (viewMode == ViewMode.BRIEF) {
            editCol = briefCurrentColumn;
            // If briefCurrentColumn is out of range, pick column 0
            if (editCol < 0 || editCol >= tableModel.getColumnCount()) editCol = 0;
        } else {
            editCol = 0; // name column in FULL
        }

        // Enable editing temporarily
        inlineRenameActive = true;

        // Only allow editing if the cell represents a real item
        if (!tableModel.isCellEditable(selRow, editCol)) {
            inlineRenameActive = false;
            return;
        }

        // Start editing and select all text in editor
        boolean started = fileTable.editCellAt(selRow, editCol);
        if (started) {
            Component ed = fileTable.getEditorComponent();
            if (ed instanceof JTextField) {
                JTextField tf = (JTextField) ed;
                tf.requestFocusInWindow();
                tf.selectAll();
            }
        } else {
            inlineRenameActive = false;
        }
    }

    /**
     * Ensure that column renderers are attached. Useful when the tab was just added to a container
     * and the ColumnModel may not yet be in its final state.
     */
    public void ensureRenderers() {
        SwingUtilities.invokeLater(() -> {
            updateColumnRenderers();
            updateColumnWidths();
            fileTable.revalidate();
            fileTable.repaint();
        });
    }

    // --- Appearance application methods ---
    public void applyGlobalFont(Font font) {
        if (font == null) return;
        fileTable.setFont(font);
        statusLabel.setFont(font);
        // Update row height based on font metrics
        FontMetrics fm = fileTable.getFontMetrics(font);
        fileTable.setRowHeight(Math.max(18, fm.getHeight() + 4));
        // If in BRIEF mode, recalculate brief layout (like on resize) so columns/rows fit
        SwingUtilities.invokeLater(() -> {
            if (viewMode == ViewMode.BRIEF) {
                tableModel.calculateBriefLayout();
                tableModel.fireTableStructureChanged();
                updateColumnRenderers();
                updateColumnWidths();
            } else {
                updateColumnWidths();
            }
            ensureRenderers();
            fileTable.revalidate();
            fileTable.repaint();
            statusLabel.revalidate();
            statusLabel.repaint();
        });
    }

    public void applyBackgroundColor(Color bg) {
        if (bg == null) return;
        updateComponentBackground(this, bg);
        fileTable.setBackground(bg);
        statusLabel.setBackground(bg);
        repaint();
    }

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

    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;
    }

    public void applySelectionColor(Color sel) {
        if (sel == null) return;
        this.selectionColor = sel;
        fileTable.repaint();
    }

    public void applyMarkedColor(Color mark) {
        if (mark == null) return;
        this.markedColor = mark;
        fileTable.repaint();
    }
    
    /**
     * Set callback to notify when directory changes
     */
    public void setOnDirectoryChanged(Runnable callback) {
        this.onDirectoryChanged = callback;
    }

    public void setOnSwitchPanelRequested(Runnable callback) {
        this.onSwitchPanelRequested = callback;
    }
    
    private void initComponents() {
        setLayout(new BorderLayout());
        
    // Table showing files
        tableModel = new FileTableModel();
        // Use a custom JTable subclass to intercept mouse events so that mouse-driven
        // selection is disabled while still allowing double-click to open items.
        // Also override mouse-motion processing to suppress drag events and
        // prevent any drag-and-drop transfer handling.
        fileTable = new JTable(tableModel) {
            @Override
            public void editingStopped(javax.swing.event.ChangeEvent e) {
                super.editingStopped(e);
                inlineRenameActive = false;
            }

            @Override
            public void editingCanceled(javax.swing.event.ChangeEvent e) {
                super.editingCanceled(e);
                inlineRenameActive = false;
            }

            @Override
            protected void processMouseEvent(java.awt.event.MouseEvent e) {
                // Show system-like context menu on popup trigger (right-click) without
                // changing default mouse-driven selection behavior.
                try {
                    if (e.isPopupTrigger()) {
                        int col = columnAtPoint(e.getPoint());
                        int row = rowAtPoint(e.getPoint());
                        FileItem item = null;
                        if (row >= 0) {
                            if (viewMode == ViewMode.BRIEF) {
                                item = tableModel.getItemFromBriefLayout(row, col);
                                if (item != null) {
                                    int index = tableModel.items.indexOf(item);
                                    if (index >= 0) {
                                        int selRow = index % tableModel.briefRowsPerColumn;
                                        int selCol = index / tableModel.briefRowsPerColumn;
                                        briefCurrentColumn = selCol;
                                        // Select the logical row so actions apply to this item
                                        fileTable.setRowSelectionInterval(selRow, selRow);
                                    }
                                }
                            } else {
                                item = tableModel.getItem(row);
                                if (item != null) {
                                    fileTable.setRowSelectionInterval(row, row);
                                }
                            }
                        }

                        if (item != null) {
                            // Delegate to outer class to build and show the menu
                            FilePanelTab.this.showContextMenuForItem(item, e.getPoint());
                        }
                        e.consume();
                        return;
                    }
                } catch (Exception ex) {
                    // best-effort: ignore exceptions here to avoid breaking mouse handling
                }
                // Intercept mouse events to prevent changing selection via mouse.
                // Allow single-click to toggle marking of an item, allow BRIEF column
                // tracking, and preserve double-click to open items. Dragging/presses
                // that would normally change selection are consumed.
                if (e.getID() == java.awt.event.MouseEvent.MOUSE_CLICKED) {
                    int col = columnAtPoint(e.getPoint());
                    int row = rowAtPoint(e.getPoint());

                    if (viewMode == ViewMode.BRIEF && col >= 0) {
                        briefCurrentColumn = col;
                        repaint();
                    }

                    // Request focus on table even if clicking on empty area
                    fileTable.requestFocusInWindow();

                    // Single left-click should focus/select the item under cursor but
                    // should NOT toggle its marked state. This preserves keyboard
                    // marking (Insert) while making mouse clicks act as simple focus.
                    if (e.getClickCount() == 1 && javax.swing.SwingUtilities.isLeftMouseButton(e)) {
                        boolean selected = false;
                        if (row >= 0) {
                            // Convert brief layout coordinates to absolute index where needed
                            if (viewMode == ViewMode.BRIEF) {
                                FileItem item = tableModel.getItemFromBriefLayout(row, col);
                                if (item != null) {
                                    int index = tableModel.items.indexOf(item);
                                    if (index >= 0) {
                                        int selRow = index % tableModel.briefRowsPerColumn;
                                        int selCol = index / tableModel.briefRowsPerColumn;
                                        briefCurrentColumn = selCol;
                                        fileTable.setRowSelectionInterval(selRow, selRow);
                                        fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true));
                                        selected = true;
                                    }
                                }
                            } else {
                                // FULL mode: rows map directly
                                fileTable.setRowSelectionInterval(row, row);
                                fileTable.scrollRectToVisible(fileTable.getCellRect(row, 0, true));
                                selected = true;
                            }
                        }
                        
                        if (!selected) {
                            // Clicked on empty area (row < 0 or empty cell in BRIEF): select the last item in the panel
                            selectLastItem();
                        }
                        repaint();
                        updateStatus();
                    }

                    // Double-click opens the item under cursor (directories)
                    if (e.getClickCount() == 2) {
                        openItemAtPoint(e.getPoint());
                    }

                    // Consume to avoid default selection behavior (no selection change by mouse)
                    e.consume();
                    return;
                }

                // Allow MOUSE_PRESSED for drag initiating gestures, but block standard selection change.
                // We'll process selection manually in MOUSE_CLICKED above.
                if (e.getID() == java.awt.event.MouseEvent.MOUSE_PRESSED) {
                    fileTable.requestFocusInWindow();
                    // Start selection logic on press to support DnD initiate
                    int col = columnAtPoint(e.getPoint());
                    int row = rowAtPoint(e.getPoint());
                    boolean selected = false;
                    if (row >= 0) {
                        if (viewMode == ViewMode.BRIEF) {
                            FileItem item = tableModel.getItemFromBriefLayout(row, col);
                            if (item != null) {
                                int index = tableModel.items.indexOf(item);
                                if (index >= 0) {
                                    int selRow = index % tableModel.briefRowsPerColumn;
                                    fileTable.setRowSelectionInterval(selRow, selRow);
                                    briefCurrentColumn = index / tableModel.briefRowsPerColumn;
                                    selected = true;
                                }
                            }
                        } else {
                            fileTable.setRowSelectionInterval(row, row);
                            selected = true;
                        }
                    }
                    
                    if (!selected) {
                        // Clicked on empty area of the table: select the last item
                        selectLastItem();
                        // For empty area, we MUST consume the event to prevent JTable from clearing selection
                        e.consume();
                        return;
                    }
                    repaint();
                    // If we are on a row, we let it pass to super so Drag and Drop can be initiated,
                    // but since we already set selection, JTable will just confirm it.
                }

                if (e.getID() == java.awt.event.MouseEvent.MOUSE_RELEASED) {
                    e.consume();
                    return;
                }

                if (!e.isConsumed()) {
                    super.processMouseEvent(e);
                }
            }
            @Override
            protected void processMouseMotionEvent(java.awt.event.MouseEvent e) {
                // Allow mouse movement to pass through to support Drag and Drop
                super.processMouseMotionEvent(e);
            }
        };

        // Enable Drag and Drop on the table
        fileTable.setDragEnabled(true);
        fileTable.setTransferHandler(new TransferHandler() {
            @Override
            public int getSourceActions(JComponent c) {
                return COPY;
            }

            @Override
            protected Transferable createTransferable(JComponent c) {
                List<FileItem> selected = getSelectedItems();
                if (selected.isEmpty()) {
                    // If nothing explicitly marked, use the focused item
                    FileItem focused = getFocusedItem();
                    if (focused != null && !focused.getName().equals("..")) {
                        selected = new ArrayList<>();
                        selected.add(focused);
                    }
                }
                
                final List<File> files = new ArrayList<>();
                for (FileItem item : selected) {
                    if (!item.getName().equals("..")) {
                        files.add(item.getFile());
                    }
                }
                
                if (files.isEmpty()) return null;
                
                return new Transferable() {
                    @Override
                    public DataFlavor[] getTransferDataFlavors() {
                        return new DataFlavor[]{DataFlavor.javaFileListFlavor};
                    }

                    @Override
                    public boolean isDataFlavorSupported(DataFlavor flavor) {
                        return DataFlavor.javaFileListFlavor.equals(flavor);
                    }

                    @Override
                    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                        if (!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor);
                        return files;
                    }
                };
            }
        });
        
        fileTable.setFocusTraversalKeysEnabled(false);
        try {
            fileTable.setDropMode(null);
        } catch (Exception ignore) {
            // Some JVMs may not support null DropMode; ignore if unsupported.
        }
        // Prevent column reordering via header drag
        if (fileTable.getTableHeader() != null) {
            fileTable.getTableHeader().setReorderingAllowed(false);
            // Set header renderer to show sort arrow when applicable
            javax.swing.table.TableCellRenderer defaultHeaderRenderer = fileTable.getTableHeader().getDefaultRenderer();
            fileTable.getTableHeader().setDefaultRenderer(new SortHeaderRenderer(defaultHeaderRenderer));
            // Add header click listener to change sorting in FULL mode
            fileTable.getTableHeader().addMouseListener(new java.awt.event.MouseAdapter() {
                @Override
                public void mouseClicked(java.awt.event.MouseEvent e) {
                    if (viewMode != ViewMode.FULL) return;
                    int col = fileTable.columnAtPoint(e.getPoint());
                    if (col < 0) return;
                    // Toggle sort order if same column, otherwise set ascending
                    if (sortColumn == col) {
                        sortAscending = !sortAscending;
                    } else {
                        sortColumn = col;
                        // Default sort direction: for names ascending, for size/date show newest/largest first
                        if (col == 0) {
                            sortAscending = true;
                        } else {
                            sortAscending = false;
                        }
                    }
                    sortItemsByColumn(sortColumn, sortAscending);
                    tableModel.fireTableDataChanged();
                    updateStatus();
                    if (persistedConfig != null) {
                        persistedConfig.setDefaultSortColumn(sortColumn);
                        persistedConfig.setDefaultSortAscending(sortAscending);
                        persistedConfig.saveConfig();
                    }
                    if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
                }
            });
        }
    fileTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
    Font initialFont = new Font("Monospaced", Font.PLAIN, 12);
    fileTable.setFont(initialFont);
    // Set row height according to font metrics so rows scale with font size
    FontMetrics fmInit = fileTable.getFontMetrics(initialFont);
    fileTable.setRowHeight(Math.max(16, fmInit.getHeight() + 4));
    // Default auto-resize: in FULL mode we allow automatic column resizing, in BRIEF
    // mode we will disable auto-resize so horizontal scrolling works and full names
    // remain visible.
    fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
        
        // Set Cell Selection mode only in BRIEF mode
        fileTable.setCellSelectionEnabled(false);
        fileTable.setRowSelectionAllowed(true);
        fileTable.setColumnSelectionAllowed(false);
        
        // Remove table borders
        fileTable.setShowGrid(false);
        fileTable.setIntercellSpacing(new java.awt.Dimension(0, 0));
        
        // Set table background same as panel background
        fileTable.setBackground(this.getBackground());
        fileTable.setOpaque(true);
        
        fileTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        // Enforce that at least one item is always active (selected) if the table is not empty.
        fileTable.getSelectionModel().addListSelectionListener(e -> {
            int row = fileTable.getSelectedRow();
            if (row >= 0) {
                if (!e.getValueIsAdjusting()) {
                    lastValidRow = row;
                    lastValidBriefColumn = briefCurrentColumn;
                }
            } else {
                // Selection became empty. Attempt to restore it.
                // We do this even if e.getValueIsAdjusting() is true to prevent temporary selection loss.
                if (fileTable.getRowCount() > 0) {
                    int targetRow = Math.min(lastValidRow, fileTable.getRowCount() - 1);
                    if (targetRow < 0) targetRow = 0;
                    
                    final int finalRow = targetRow;
                    final int finalCol = lastValidBriefColumn;
                    
                    // Use invokeLater to avoid potential re-entrancy issues with selection model
                    SwingUtilities.invokeLater(() -> {
                        if (fileTable != null && fileTable.getSelectionModel().isSelectionEmpty() && fileTable.getRowCount() > 0) {
                            briefCurrentColumn = finalCol;
                            fileTable.setRowSelectionInterval(finalRow, finalRow);
                            // Ensure the restored selection is visible
                            try {
                                fileTable.scrollRectToVisible(fileTable.getCellRect(finalRow, finalCol, true));
                            } catch (Exception ignore) {}
                        }
                    });
                }
            }
        });
        
    JScrollPane scrollPane = new JScrollPane(fileTable);
    // Enable horizontal scrollbar when needed so BRIEF mode can scroll left-right
    scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

    // Implement mouse wheel navigation in BRIEF mode to match arrow key behavior
    fileTable.addMouseWheelListener(new java.awt.event.MouseWheelListener() {
        @Override
        public void mouseWheelMoved(java.awt.event.MouseWheelEvent e) {
            if (viewMode == ViewMode.BRIEF) {
                int rotation = e.getWheelRotation();
                if (rotation != 0) {
                    int steps = 10;
                    if (persistedConfig != null) {
                        steps = persistedConfig.getBriefMouseWheelSteps();
                    }
                    handleBriefNavigation(rotation > 0, steps);
                    e.consume();
                }
            }
        }
    });

    add(scrollPane, BorderLayout.CENTER);
        
    // Status bar
        statusLabel = new JLabel(" ");
        statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
        add(statusLabel, BorderLayout.SOUTH);
        
    // Add listener to handle panel resize
        scrollPane.addComponentListener(new java.awt.event.ComponentAdapter() {
            @Override
            public void componentResized(java.awt.event.ComponentEvent e) {
                if (viewMode == ViewMode.BRIEF) {
                    // Recalculate layout on resize in BRIEF mode
                    SwingUtilities.invokeLater(() -> {
                        // Remember selected item
                        String selectedItemName = null;
                        int selectedRow = fileTable.getSelectedRow();
                        if (selectedRow >= 0) {
                            FileItem item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
                            if (item != null) {
                                selectedItemName = item.getName();
                            }
                        }
                        
                        // Recalculate layout
                        tableModel.calculateBriefLayout();
                        tableModel.fireTableStructureChanged();
                        updateColumnRenderers();
                        updateColumnWidths();
                        
                        // Re-select item
                        if (selectedItemName != null) {
                            selectItemByName(selectedItemName, false);
                        }
                    });
                }
            }
        });
        
        // Note: mouse handling is implemented inside the custom JTable above so
        // that mouse-driven selection is disabled.
        
    // Setup custom key handling (Home/End etc.)
        setupKeyListeners();

    // Ensure TAB key switches panels even when multiple tabs are open.
    // Map TAB to an action that asks the top-level window to switch panels.
        fileTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                 .put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_TAB, 0), "switchPanelFromTab");
        fileTable.getActionMap().put("switchPanelFromTab", new AbstractAction() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent e) {
                if (onSwitchPanelRequested != null) {
                    onSwitchPanelRequested.run();
                }
            }
        });
    }
    
    private void setupKeyListeners() {
        fileTable.addKeyListener(new java.awt.event.KeyAdapter() {
            @Override
            public void keyPressed(java.awt.event.KeyEvent e) {
                if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER && !e.isControlDown()) {
                    openSelectedItem();
                    e.consume();
                } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_BACK_SPACE) {
                    navigateUp();
                    e.consume();
                } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_INSERT) {
                    toggleSelectionAndMoveDown();
                    e.consume();
                } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ESCAPE) {
                    if (fileTable.isEditing()) {
                        fileTable.getCellEditor().cancelCellEditing();
                        e.consume();
                    }
                } else if (viewMode == ViewMode.BRIEF) {
                    handleBriefKeyNavigation(e);
                } else if (viewMode == ViewMode.FULL) {
                    // Support Home/End in FULL mode to jump to first/last row
                    if (e.getKeyCode() == java.awt.event.KeyEvent.VK_HOME) {
                        if (tableModel.getRowCount() > 0) {
                            fileTable.setRowSelectionInterval(0, 0);
                            fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
                            updateStatus();
                        }
                        e.consume();
                    } else if (e.getKeyCode() == java.awt.event.KeyEvent.VK_END) {
                        int last = tableModel.getRowCount() - 1;
                        if (last >= 0) {
                            fileTable.setRowSelectionInterval(last, last);
                            fileTable.scrollRectToVisible(fileTable.getCellRect(last, 0, true));
                            updateStatus();
                        }
                        e.consume();
                    }
                }
            }
        });
    }
    
    private void handleBriefKeyNavigation(java.awt.event.KeyEvent e) {
        switch (e.getKeyCode()) {
            case java.awt.event.KeyEvent.VK_UP:
            case java.awt.event.KeyEvent.VK_DOWN:
                handleBriefNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_DOWN);
                e.consume();
                break;
            case java.awt.event.KeyEvent.VK_LEFT:
            case java.awt.event.KeyEvent.VK_RIGHT:
                handleBriefHorizontalNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_RIGHT);
                e.consume();
                break;
            case java.awt.event.KeyEvent.VK_HOME:
                if (tableModel.items.size() > 0) {
                    briefCurrentColumn = 0;
                    fileTable.setRowSelectionInterval(0, 0);
                    fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
                    fileTable.repaint();
                    updateStatus();
                }
                e.consume();
                break;
            case java.awt.event.KeyEvent.VK_END:
                if (tableModel.items.size() > 0) {
                    int lastIndex = tableModel.items.size() - 1;
                    int lastColumn = lastIndex / tableModel.briefRowsPerColumn;
                    int lastRow = lastIndex % tableModel.briefRowsPerColumn;
                    if (lastColumn < tableModel.getColumnCount()) {
                        briefCurrentColumn = lastColumn;
                        fileTable.setRowSelectionInterval(lastRow, lastRow);
                        fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, lastColumn, true));
                        fileTable.repaint();
                        updateStatus();
                    }
                }
                e.consume();
                break;
            case java.awt.event.KeyEvent.VK_PAGE_UP:
            case java.awt.event.KeyEvent.VK_PAGE_DOWN:
                handleBriefPageNavigation(e.getKeyCode() == java.awt.event.KeyEvent.VK_PAGE_UP);
                e.consume();
                break;
        }
    }
    
    private void handleBriefNavigation(boolean down) {
        handleBriefNavigation(down, 1);
    }

    private void handleBriefNavigation(boolean down, int amount) {
        int currentRow = fileTable.getSelectedRow();
        if (currentRow < 0) return;
        
        int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow;
        int newIndex = down ? currentIndex + amount : currentIndex - amount;
        
        if (newIndex < 0) newIndex = 0;
        if (newIndex >= tableModel.items.size()) newIndex = tableModel.items.size() - 1;
        
        int newColumn = newIndex / tableModel.briefRowsPerColumn;
        int newRow = newIndex % tableModel.briefRowsPerColumn;
        
        if (newColumn < tableModel.getColumnCount()) {
            briefCurrentColumn = newColumn;
            fileTable.setRowSelectionInterval(newRow, newRow);
            fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true));
            fileTable.repaint();
            updateStatus();
        }
    }
    
    private void handleBriefHorizontalNavigation(boolean right) {
        int currentRow = fileTable.getSelectedRow();
        if (currentRow < 0) return;
        
        int newColumn = right ? briefCurrentColumn + 1 : briefCurrentColumn - 1;
        int colCount = tableModel.getColumnCount();

        // If moving beyond edges, jump to first/last item instead of doing nothing
        if (newColumn < 0 || newColumn >= colCount) {
            if (right && briefCurrentColumn >= colCount - 1) {
                // Jump to last item
                int lastIndex = Math.max(0, tableModel.items.size() - 1);
                int lastCol = lastIndex / tableModel.briefRowsPerColumn;
                int lastRow = lastIndex % tableModel.briefRowsPerColumn;
                briefCurrentColumn = Math.min(lastCol, Math.max(0, colCount - 1));
                fileTable.setRowSelectionInterval(lastRow, lastRow);
                fileTable.scrollRectToVisible(fileTable.getCellRect(lastRow, briefCurrentColumn, true));
                fileTable.repaint();
                updateStatus();
            } else if (!right && briefCurrentColumn <= 0) {
                // Jump to first item
                briefCurrentColumn = 0;
                if (tableModel.items.size() > 0) {
                    fileTable.setRowSelectionInterval(0, 0);
                    fileTable.scrollRectToVisible(fileTable.getCellRect(0, 0, true));
                }
                fileTable.repaint();
                updateStatus();
            }
            return;
        }

        FileItem item = tableModel.getItemFromBriefLayout(currentRow, newColumn);
        int targetRow = currentRow;
        
        if (item == null) {
            for (int row = currentRow - 1; row >= 0; row--) {
                item = tableModel.getItemFromBriefLayout(row, newColumn);
                if (item != null) {
                    targetRow = row;
                    break;
                }
            }
            if (item == null) return;
        }
        
        briefCurrentColumn = newColumn;
        fileTable.setRowSelectionInterval(targetRow, targetRow);
        fileTable.scrollRectToVisible(fileTable.getCellRect(targetRow, briefCurrentColumn, true));
        fileTable.repaint();
        updateStatus();
    }
    
    private void handleBriefPageNavigation(boolean pageUp) {
        int currentRow = fileTable.getSelectedRow();
        if (currentRow < 0) return;
        
        int currentIndex = briefCurrentColumn * tableModel.briefRowsPerColumn + currentRow;
        
        Rectangle visible = fileTable.getVisibleRect();
        int rowHeight = fileTable.getRowHeight();
        int visibleRows = visible.height / rowHeight;
        if (visibleRows <= 0) visibleRows = 1;

        // Compute how many columns are visible in the current viewport (from left-most visible column)
        int visibleColumns = 1;
        try {
            int colCount = fileTable.getColumnModel().getColumnCount();
            // Find first visible column by using columnAtPoint on the left edge of the visible rect
            int firstVisibleCol = fileTable.columnAtPoint(new Point(visible.x, visible.y));
            if (firstVisibleCol < 0) firstVisibleCol = 0;

            int acc = 0;
            for (int c = firstVisibleCol; c < colCount; c++) {
                int w = fileTable.getColumnModel().getColumn(c).getWidth();
                // Determine the column's starting X by summing widths of prior columns
                acc += w;
                // If accumulated width (relative to firstVisibleCol) exceeds viewport width, stop
                if (acc > visible.width) {
                    break;
                }
                visibleColumns = c - firstVisibleCol + 1;
            }
        } catch (Exception ex) {
            visibleColumns = Math.max(1, fileTable.getColumnModel().getColumnCount());
        }

        if (visibleColumns <= 0) visibleColumns = 1;

        int visibleItems = visibleRows * visibleColumns;
        if (visibleItems <= 0) visibleItems = visibleRows;

        int newIndex = pageUp ? currentIndex - visibleItems : currentIndex + visibleItems;
        newIndex = Math.max(0, Math.min(newIndex, tableModel.items.size() - 1));
        
        int newColumn = newIndex / tableModel.briefRowsPerColumn;
        int newRow = newIndex % tableModel.briefRowsPerColumn;

        // Clamp newColumn to available columns
        int maxCols = tableModel.getColumnCount();
        if (newColumn >= maxCols) {
            newColumn = Math.max(0, maxCols - 1);
            // Recompute newRow to fit within that column
            int baseIndex = newColumn * tableModel.briefRowsPerColumn;
            newRow = Math.min(tableModel.briefRowsPerColumn - 1, newIndex - baseIndex);
        }

        briefCurrentColumn = newColumn;
        fileTable.setRowSelectionInterval(newRow, newRow);
        fileTable.scrollRectToVisible(fileTable.getCellRect(newRow, briefCurrentColumn, true));
        fileTable.repaint();
        updateStatus();
    }
    
    public void loadDirectory(File directory) {
        loadDirectory(directory, true, true);
    }
    
    public void loadDirectory(File directory, boolean autoSelectFirst) {
        loadDirectory(directory, autoSelectFirst, true);
    }

    public void loadDirectory(File directory, boolean autoSelectFirst, boolean requestFocus) {
        // If we are switching directories, cleanup any previously extracted archive temp dirs
        cleanupArchiveTempDirIfNeeded(directory);
        if (directory == null || !directory.isDirectory()) {
            return;
        }
        
        this.currentDirectory = directory;
        briefCurrentColumn = 0;
        lastValidRow = 0;
        lastValidBriefColumn = 0;
        
        File[] files = directory.listFiles();
        List<FileItem> items = new ArrayList<>();
        
        File parent = directory.getParentFile();
        if (parent != null) {
            items.add(new FileItem(parent) {
                @Override
                public String getName() {
                    return "..";
                }
            });
        }
        
        if (files != null && files.length > 0) {
            Arrays.sort(files, Comparator
                    .comparing((File f) -> !f.isDirectory())
                    .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER));
            
            for (File file : files) {
                items.add(new FileItem(file));
            }
        }
        
        tableModel.setItems(items);
        
        if (viewMode == ViewMode.BRIEF) {
            boolean selectFirst = autoSelectFirst;
            SwingUtilities.invokeLater(() -> {
                tableModel.calculateBriefLayout();
                tableModel.fireTableStructureChanged();
                updateColumnRenderers();
                updateColumnWidths();
                fileTable.revalidate();
                fileTable.repaint();
                
                if (selectFirst && tableModel.items.size() > 0) {
                    int startIndex = 0;
                    int selRow = startIndex % tableModel.briefRowsPerColumn;
                    int selCol = startIndex / tableModel.briefRowsPerColumn;
                    briefCurrentColumn = selCol;
                    fileTable.setRowSelectionInterval(selRow, selRow);
                    fileTable.scrollRectToVisible(fileTable.getCellRect(selRow, selCol, true));
                }
                if (requestFocus) {
                    fileTable.requestFocusInWindow();
                }
            });
        } else {
            if (autoSelectFirst && fileTable.getRowCount() > 0) {
                int startIndex = 0;
                fileTable.setRowSelectionInterval(startIndex, startIndex);
                if (requestFocus) {
                    SwingUtilities.invokeLater(() -> {
                        try { fileTable.requestFocusInWindow(); } catch (Exception ignore) {}
                    });
                }
            }
        }
        
        updateStatus();
        
        // Notify directory change
        if (onDirectoryChanged != null) {
            onDirectoryChanged.run();
        }
    }

    /**
     * Cleanup previous archive temp dir when navigating away from it.
     */
    private void cleanupArchiveTempDirIfNeeded(File newDirectory) {
        try {
            if (currentArchiveTempDir != null) {
                Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null;
                if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) {
                    deleteTempDirRecursively(currentArchiveTempDir);
                    currentArchiveTempDir = null;
                }
            }
        } catch (Exception ignore) {
            // best-effort cleanup
        }
    }

    private boolean isArchiveFile(File f) {
        if (f == null) return false;
        String n = f.getName().toLowerCase();
        return n.endsWith(".zip") || n.endsWith(".jar") || n.endsWith(".tar") || n.endsWith(".tar.gz") || n.endsWith(".tgz") || n.endsWith(".7z") || n.endsWith(".rar");
    }

    private Path extractArchiveToTemp(File archive) {
        if (archive == null || !archive.isFile()) return null;
        String name = archive.getName().toLowerCase();
        try {
            Path tempDir = Files.createTempDirectory("kfmanager-archive-");

            if (name.endsWith(".zip") || name.endsWith(".jar")) {
                extractZip(archive, tempDir);
            } else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
                extractTarGz(archive, tempDir);
            } else if (name.endsWith(".tar")) {
                extractTar(archive, tempDir);
            } else if (name.endsWith(".7z")) {
                extractSevenZ(archive, tempDir);
            } else if (name.endsWith(".rar")) {
                extractRar(archive, tempDir);
            }

            return tempDir;
        } catch (Exception ex) {
            // extraction failed; attempt best-effort cleanup
            try {
                if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir);
            } catch (Exception ignore) {}
            return null;
        }
    }

    private void extractZip(File archive, Path tempDir) throws IOException {
        try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(archive.toPath()))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                String entryName = entry.getName();
                Path resolved = tempDir.resolve(entryName).normalize();
                if (!resolved.startsWith(tempDir)) {
                    zis.closeEntry();
                    continue;
                }

                if (entry.isDirectory() || entryName.endsWith("/")) {
                    Files.createDirectories(resolved);
                } else {
                    Path parent = resolved.getParent();
                    if (parent != null) Files.createDirectories(parent);
                    Files.copy(zis, resolved, StandardCopyOption.REPLACE_EXISTING);
                }
                zis.closeEntry();
            }
        }
    }

    private void extractSevenZ(File archive, Path tempDir) throws IOException {
        try (SevenZFile sevenZFile = new SevenZFile(archive)) {
            SevenZArchiveEntry entry;
            while ((entry = sevenZFile.getNextEntry()) != null) {
                String entryName = entry.getName();
                Path resolved = tempDir.resolve(entryName).normalize();
                if (!resolved.startsWith(tempDir)) {
                    continue;
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(resolved);
                } else {
                    Path parent = resolved.getParent();
                    if (parent != null) Files.createDirectories(parent);
                    
                    try (OutputStream os = Files.newOutputStream(resolved)) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = sevenZFile.read(buffer)) != -1) {
                            os.write(buffer, 0, bytesRead);
                        }
                    }
                }
            }
        }
    }

    private void extractRar(File archiveFile, Path tempDir) throws Exception {
        try (Archive archive = new Archive(archiveFile)) {
            FileHeader fh = archive.nextFileHeader();
            while (fh != null) {
                String entryName = fh.getFileName().replace('\\', '/');
                Path resolved = tempDir.resolve(entryName).normalize();
                if (!resolved.startsWith(tempDir)) {
                    fh = archive.nextFileHeader();
                    continue;
                }

                if (fh.isDirectory()) {
                    Files.createDirectories(resolved);
                } else {
                    Path parent = resolved.getParent();
                    if (parent != null) Files.createDirectories(parent);
                    try (OutputStream os = Files.newOutputStream(resolved)) {
                        archive.extractFile(fh, os);
                    }
                }
                fh = archive.nextFileHeader();
            }
        }
    }

    private void extractTarGz(File archive, Path tempDir) throws IOException {
        try (InputStream fis = Files.newInputStream(archive.toPath());
             InputStream gzis = new GzipCompressorInputStream(fis);
             TarArchiveInputStream tais = new TarArchiveInputStream(gzis)) {
            extractTarInternal(tais, tempDir);
        }
    }

    private void extractTar(File archive, Path tempDir) throws IOException {
        try (InputStream fis = Files.newInputStream(archive.toPath());
             TarArchiveInputStream tais = new TarArchiveInputStream(fis)) {
            extractTarInternal(tais, tempDir);
        }
    }

    private void extractTarInternal(TarArchiveInputStream tais, Path tempDir) throws IOException {
        TarArchiveEntry entry;
        while ((entry = tais.getNextTarEntry()) != null) {
            String entryName = entry.getName();
            Path resolved = tempDir.resolve(entryName).normalize();
            if (!resolved.startsWith(tempDir)) {
                continue;
            }

            if (entry.isDirectory()) {
                Files.createDirectories(resolved);
            } else {
                Path parent = resolved.getParent();
                if (parent != null) Files.createDirectories(parent);
                Files.copy(tais, resolved, StandardCopyOption.REPLACE_EXISTING);
            }
        }
    }

    private void deleteTempDirRecursively(Path dir) {
        if (dir == null) return;
        try {
            if (!Files.exists(dir)) return;
            Files.walk(dir)
                    .sorted(java.util.Comparator.reverseOrder())
                    .forEach(p -> {
                        try { Files.deleteIfExists(p); } catch (Exception ignore) {}
                    });
        } catch (IOException ignore) {
        }
    }
    
    private void openSelectedItem() {
        int selectedRow = fileTable.getSelectedRow();
        if (selectedRow >= 0) {
            FileItem item;
            if (viewMode == ViewMode.BRIEF) {
                item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
            } else {
                item = tableModel.getItem(selectedRow);
            }
            
            if (item == null) return;
            
            if (item.getName().equals("..")) {
                navigateUp();
            } else if (isArchiveFile(item.getFile())) {
                Path temp = extractArchiveToTemp(item.getFile());
                    if (temp != null) {
                    // Delete any previous temp dir if different
                    try {
                        if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) {
                            deleteTempDirRecursively(currentArchiveTempDir);
                        }
                    } catch (Exception ignore) {}
                    currentArchiveTempDir = temp;
                    currentArchiveSourceFile = item.getFile();
                    loadDirectory(temp.toFile());
                }
            } else if (item.isDirectory()) {
                loadDirectory(item.getFile());
            } else if (item.getFile().isFile()) {
                openFileNative(item.getFile());
            }
        }
    }

    private void openFileNative(File file) {
        try {
            // 1. Check custom associations from config (wildcard matching)
            if (persistedConfig != null) {
                java.util.Map<String, String> associations = persistedConfig.getFileAssociations();
                String name = file.getName();
                String command = null;

                // Sort associations by length descending to match more specific patterns first (e.g. *.tar.gz before *.gz)
                java.util.List<String> patterns = new java.util.ArrayList<>(associations.keySet());
                patterns.sort((a, b) -> Integer.compare(b.length(), a.length()));

                for (String pattern : patterns) {
                    String glob = pattern;
                    // If user just provided an extension like "txt", convert to "*.txt"
                    if (!glob.contains("*") && !glob.contains("?")) {
                        glob = "*." + glob;
                    }

                    try {
                        java.nio.file.PathMatcher matcher = java.nio.file.FileSystems.getDefault().getPathMatcher("glob:" + glob);
                        if (matcher.matches(java.nio.file.Paths.get(name))) {
                            command = associations.get(pattern);
                            break;
                        }
                    } catch (Exception ignore) {
                        // Fallback for simple extension match if glob is invalid
                        if (name.toLowerCase().endsWith("." + pattern.toLowerCase())) {
                            command = associations.get(pattern);
                            break;
                        }
                    }
                }

                if (command != null && !command.trim().isEmpty()) {
                    String fullPath = file.getAbsolutePath();
                    String fileName = file.getName();
                    String trimmedCmd = command.trim();
                    boolean hasPlaceholder = trimmedCmd.contains("%f") || trimmedCmd.contains("%n");

                    java.util.List<String> cmdList = new java.util.ArrayList<>();
                    String[] parts = trimmedCmd.split("\\s+");
                    for (String part : parts) {
                        String processed = part.replace("%f", fullPath).replace("%n", fileName);
                        if (processed.startsWith("\"") && processed.endsWith("\"") && processed.length() >= 2) {
                            processed = processed.substring(1, processed.length() - 1);
                        }
                        cmdList.add(processed);
                    }

                    if (!hasPlaceholder) {
                        cmdList.add(fullPath);
                    }

                    new ProcessBuilder(cmdList).directory(file.getParentFile()).start();
                    return;
                }
            }

            // 2. If executable, start it directly
            if (file.canExecute() && !file.isDirectory()) {
                new ProcessBuilder(file.getAbsolutePath())
                    .directory(file.getParentFile())
                    .start();
            } else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
                // 3. Fallback to system default
                Desktop.getDesktop().open(file);
                // Try to keep focus or at least set it back for when user returns
                SwingUtilities.invokeLater(() -> {
                    fileTable.requestFocusInWindow();
                });
            }
        } catch (Exception ex) {
            try {
                JOptionPane.showMessageDialog(this, "Error opening file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            } catch (Exception ignore) {}
        } finally {
            fileTable.requestFocusInWindow();
        }
    }

    /**
     * Open the item located at the given point (used for double-clicks while
     * mouse-driven selection is blocked). This mirrors the behavior of
     * openSelectedItem but works from a mouse location instead of the table
     * selection.
     */
    private void openItemAtPoint(Point p) {
        int row = fileTable.rowAtPoint(p);
        int col = fileTable.columnAtPoint(p);
        if (row < 0) return;

        FileItem item = null;
        if (viewMode == ViewMode.BRIEF) {
            item = tableModel.getItemFromBriefLayout(row, col);
        } else {
            item = tableModel.getItem(row);
        }

        if (item == null) return;

        if (item.getName().equals("..")) {
            navigateUp();
        } else if (isArchiveFile(item.getFile())) {
            Path temp = extractArchiveToTemp(item.getFile());
            if (temp != null) {
                try {
                    if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) {
                        deleteTempDirRecursively(currentArchiveTempDir);
                    }
                } catch (Exception ignore) {}
                currentArchiveTempDir = temp;
                currentArchiveSourceFile = item.getFile();
                loadDirectory(temp.toFile(), true, true);
            }
        } else if (item.isDirectory()) {
            loadDirectory(item.getFile());
        } else if (item.getFile().isFile()) {
            openFileNative(item.getFile());
        }
    }

    /**
     * Show a context popup menu for a specific file item at the given point (table coordinates).
     */
    private void showContextMenuForItem(FileItem item, Point tablePoint) {
        if (item == null) return;

        JPopupMenu menu = new JPopupMenu();

        // Open
        JMenuItem openItem = new JMenuItem("Open");
        openItem.addActionListener(ae -> {
            if (item.getName().equals("..")) return;
            if (item.isDirectory()) {
                loadDirectory(item.getFile());
            } else {
                openFileNative(item.getFile());
            }
        });
        menu.add(openItem);

        // Edit
        JMenuItem editItem = new JMenuItem("Edit");
        editItem.addActionListener(ae -> {
            if (item.isDirectory() || item.getName().equals("..")) return;
            try {
                String ext = persistedConfig != null ? persistedConfig.getExternalEditorPath() : null;
                if (ext != null && !ext.trim().isEmpty()) {
                    java.util.List<String> cmd = new java.util.ArrayList<>();
                    cmd.add(ext);
                    cmd.add(item.getFile().getAbsolutePath());
                    new ProcessBuilder(cmd).start();
                } else if (Desktop.isDesktopSupported()) {
                    Desktop.getDesktop().edit(item.getFile());
                }
            } catch (Exception ex) {
                try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot edit: " + ex.getMessage()); } catch (Exception ignore) {}
            }
        });
        menu.add(editItem);

        // Show in folder / Reveal
        JMenuItem revealItem = new JMenuItem("Show in folder");
        revealItem.addActionListener(ae -> {
            try {
                String os = System.getProperty("os.name").toLowerCase();
                File f = item.getFile();
                if (os.contains("win")) {
                    new ProcessBuilder("explorer.exe", "/select,", f.getAbsolutePath()).start();
                } else if (os.contains("mac")) {
                    new ProcessBuilder("open", "-R", f.getAbsolutePath()).start();
                } else {
                    File parent = f.getParentFile();
                    if (parent != null) new ProcessBuilder("xdg-open", parent.getAbsolutePath()).start();
                }
            } catch (Exception ex) {
                try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot reveal file: " + ex.getMessage()); } catch (Exception ignore) {}
            }
        });
        menu.add(revealItem);

        // Copy path
        JMenuItem copyPath = new JMenuItem("Copy path");
        copyPath.addActionListener(ae -> {
            try {
                StringSelection sel = new StringSelection(item.getFile().getAbsolutePath());
                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, null);
            } catch (Exception ex) {
                try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot copy path: " + ex.getMessage()); } catch (Exception ignore) {}
            }
        });
        menu.add(copyPath);

        menu.addSeparator();

        // Cut
        JMenuItem cutItem = new JMenuItem("Cut");
        cutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK));
        cutItem.addActionListener(ae -> copyToClipboard(true));
        menu.add(cutItem);

        // Copy
        JMenuItem copyItem = new JMenuItem("Copy");
        copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK));
        copyItem.addActionListener(ae -> copyToClipboard(false));
        menu.add(copyItem);

        // Paste
        JMenuItem pasteItem = new JMenuItem("Paste");
        pasteItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK));
        pasteItem.addActionListener(ae -> pasteFromClipboard());
        menu.add(pasteItem);

        menu.addSeparator();

        // Associate with...
        JMenuItem associateItem = new JMenuItem("Associate with...");
        associateItem.addActionListener(ae -> {
            if (item.isDirectory() || item.getName().equals("..")) return;
            showAssociationDialog(item.getFile());
        });
        menu.add(associateItem);

        // Delete
        JMenuItem deleteItem = new JMenuItem("Delete");
        deleteItem.addActionListener(ae -> {
            int res = JOptionPane.showConfirmDialog(FilePanelTab.this,
                    "Really delete '" + item.getName() + "'?",
                    "Delete",
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE);
            if (res == JOptionPane.YES_OPTION) {
                java.util.List<FileItem> toDelete = new java.util.ArrayList<>();
                toDelete.add(item);
                final int rememberedIndex = getFocusedItemIndex();
                Window parentWindow = SwingUtilities.getWindowAncestor(FilePanelTab.this);
                ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, "Deleting");
                
                new Thread(() -> {
                    try {
                        cz.kamma.kfmanager.service.FileOperations.delete(toDelete, new cz.kamma.kfmanager.service.FileOperations.ProgressCallback() {
                            @Override
                            public void onProgress(long current, long total, String currentFile) {
                                progressDialog.updateProgress(current, total, currentFile);
                            }
                            @Override
                            public boolean isCancelled() {
                                return progressDialog.isCancelled();
                            }
                        });
                        SwingUtilities.invokeLater(() -> {
                            progressDialog.dispose();
                            loadDirectory(getCurrentDirectory(), false);
                            // Use another invokeLater to ensure BRIEF mode layout is updated
                            SwingUtilities.invokeLater(() -> {
                                selectItemByIndex(rememberedIndex - 1);
                            });
                        });
                    } catch (Exception ex) {
                        SwingUtilities.invokeLater(() -> {
                            progressDialog.dispose();
                            JOptionPane.showMessageDialog(FilePanelTab.this, "Delete failed: " + ex.getMessage());
                        });
                    }
                }).start();
                progressDialog.setVisible(true);
            }
        });
        menu.add(deleteItem);

        // Properties
        JMenuItem props = new JMenuItem("Properties");
        props.addActionListener(ae -> {
            try {
                Window parent = SwingUtilities.getWindowAncestor(FilePanelTab.this);
                File f = item.getFile();
                if (f != null) {
                    PropertiesDialog dialog = new PropertiesDialog(parent, f);
                    dialog.setVisible(true);
                    // Refresh current directory after potential attribute changes
                    loadDirectory(getCurrentDirectory(), false, true);
                }
            } catch (Exception ex) {
                try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot show properties: " + ex.getMessage()); } catch (Exception ignore) {}
            }
        });
        menu.add(props);

        // Show the popup at the click location
        SwingUtilities.invokeLater(() -> {
            try {
                menu.show(fileTable, tablePoint.x, tablePoint.y);
            } catch (Exception ignore) {}
        });
    }
    
    public void navigateUp() {
        // If we're currently browsing an extracted archive root, navigate back to the
        // original archive's parent and select the archive file.
        try {
            if (currentArchiveTempDir != null && currentArchiveSourceFile != null) {
                Path cur = currentDirectory.toPath().toAbsolutePath().normalize();
                if (cur.equals(currentArchiveTempDir)) {
                    File parent = currentArchiveSourceFile.getParentFile();
                    if (parent != null) {
                        String archiveName = currentArchiveSourceFile.getName();
                        // cleanup temp dir before switching back
                        deleteTempDirRecursively(currentArchiveTempDir);
                        currentArchiveTempDir = null;
                        currentArchiveSourceFile = null;
                        loadDirectory(parent, false);
                        
                        // Select the archive file we just left
                        SwingUtilities.invokeLater(() -> selectItemByName(archiveName));
                    }
                    return;
                }
            }
        } catch (Exception ignore) {}

        File parent = currentDirectory.getParentFile();
        if (parent != null) {
            String previousDirName = currentDirectory.getName();
            loadDirectory(parent, false);
            
            // Select the directory we just left
            SwingUtilities.invokeLater(() -> selectItemByName(previousDirName));
        }
    }
    
    public void selectItemByName(String name) {
        selectItemByName(name, true);
    }

    public void selectItemByName(String name, boolean requestFocus) {
        if (viewMode == ViewMode.BRIEF) {
            // Re-calculate layout if needed before searching to ensure current mapping
            if (tableModel.items.size() > 0 && (tableModel.briefColumns == 0 || tableModel.briefRowsPerColumn == 0)) {
                tableModel.calculateBriefLayout();
            }
            
            for (int i = 0; i < tableModel.items.size(); i++) {
                FileItem item = tableModel.items.get(i);
                if (item != null && item.getName().equalsIgnoreCase(name)) {
                    int column = i / tableModel.briefRowsPerColumn;
                    int row = i % tableModel.briefRowsPerColumn;
                    
                    if (column < fileTable.getColumnCount()) {
                        briefCurrentColumn = column;
                        fileTable.setRowSelectionInterval(row, row);
                        fileTable.scrollRectToVisible(fileTable.getCellRect(row, column, true));
                        fileTable.repaint();
                        if (requestFocus) {
                            fileTable.requestFocusInWindow();
                        }
                        updateStatus();
                    }
                    return;
                }
            }
        } else {
            for (int i = 0; i < tableModel.getRowCount(); i++) {
                FileItem item = tableModel.getItem(i);
                if (item != null && item.getName().equalsIgnoreCase(name)) {
                    fileTable.setRowSelectionInterval(i, i);
                    fileTable.scrollRectToVisible(fileTable.getCellRect(i, 0, true));
                    if (requestFocus) {
                        fileTable.requestFocusInWindow();
                    }
                    updateStatus();
                    return;
                }
            }
        }
    }

    /**
     * Public wrapper to select an item by name from outside this class.
     * Useful for other UI components to request focusing a specific file.
     */
    public void selectItem(String name) {
        selectItemByName(name, true);
    }

    public void selectItem(String name, boolean requestFocus) {
        selectItemByName(name, requestFocus);
    }
    
    public void toggleSelectionAndMoveDown() {
        int selectedRow = fileTable.getSelectedRow();
        if (selectedRow >= 0) {
            FileItem item = null;
            if (viewMode == ViewMode.BRIEF) {
                item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
            } else {
                item = tableModel.getItem(selectedRow);
            }
            
            if (item != null && !item.getName().equals("..")) {
                item.setMarked(!item.isMarked());
                fileTable.repaint();
                
                if (viewMode == ViewMode.BRIEF) {
                    handleBriefNavigation(true);
                } else {
                    int nextRow = selectedRow + 1;
                    if (nextRow < fileTable.getRowCount()) {
                        fileTable.setRowSelectionInterval(nextRow, nextRow);
                        fileTable.scrollRectToVisible(fileTable.getCellRect(nextRow, 0, true));
                    }
                }
                updateStatus();
            }
        }
    }
    
    public List<FileItem> getSelectedItems() {
        List<FileItem> selected = new ArrayList<>();
        for (FileItem item : tableModel.items) {
            if (item.isMarked() && !item.getName().equals("..")) {
                selected.add(item);
            }
        }
        
        if (selected.isEmpty()) {
            int selectedRow = fileTable.getSelectedRow();
            if (selectedRow >= 0) {
                FileItem item = null;
                if (viewMode == ViewMode.BRIEF) {
                    item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
                } else {
                    item = tableModel.getItem(selectedRow);
                }
                if (item != null && !item.getName().equals("..")) {
                    selected.add(item);
                }
            }
        }
        
        return selected;
    }

    public FileItem getFocusedItem() {
        int selectedRow = fileTable.getSelectedRow();
        if (selectedRow >= 0) {
            if (viewMode == ViewMode.BRIEF) {
                return tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
            } else {
                return tableModel.getItem(selectedRow);
            }
        }
        return null;
    }

    public int getFocusedItemIndex() {
        int row = fileTable.getSelectedRow();
        if (row < 0) return -1;
        if (viewMode == ViewMode.BRIEF) {
            return briefCurrentColumn * tableModel.briefRowsPerColumn + row;
        } else {
            return row;
        }
    }

    public void selectItemByIndex(int index) {
        if (index < 0) index = 0;
        if (index >= tableModel.items.size()) index = tableModel.items.size() - 1;
        if (index < 0) return;

        if (viewMode == ViewMode.BRIEF) {
            briefCurrentColumn = index / tableModel.briefRowsPerColumn;
            int row = index % tableModel.briefRowsPerColumn;
            if (briefCurrentColumn < fileTable.getColumnCount()) {
                fileTable.setRowSelectionInterval(row, row);
                fileTable.scrollRectToVisible(fileTable.getCellRect(row, briefCurrentColumn, true));
            }
        } else {
            fileTable.setRowSelectionInterval(index, index);
            fileTable.scrollRectToVisible(fileTable.getCellRect(index, 0, true));
        }
        fileTable.repaint();
        fileTable.requestFocusInWindow();
        updateStatus();
    }

    /**
     * Select all items that match the given wildcard pattern.
     * Supports * (any characters) and ? (single character).
     * Example patterns: *.txt, test*.*, file?.dat
     */
    public void selectByWildcard(String pattern) {
        if (pattern == null || pattern.trim().isEmpty()) {
            return;
        }
        
        pattern = pattern.trim();
        
        // Convert wildcard pattern to regex
        String regex = wildcardToRegex(pattern);
        java.util.regex.Pattern compiledPattern;
        try {
            compiledPattern = java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE);
        } catch (Exception e) {
            return; // Invalid pattern
        }
        
        // Mark all matching items
        int matchCount = 0;
        for (FileItem item : tableModel.items) {
            if (item.getName().equals("..")) {
                continue; // Skip parent directory
            }
            
            if (compiledPattern.matcher(item.getName()).matches()) {
                item.setMarked(true);
                matchCount++;
            }
        }
        
        // Refresh the table to show the marked items
        fileTable.repaint();
        updateStatus();
        
        // Request focus back to the table
        fileTable.requestFocusInWindow();
    }
    
    /**
     * Convert wildcard pattern to regex.
     * * matches any characters
     * ? matches single character
     */
    private String wildcardToRegex(String wildcard) {
        StringBuilder regex = new StringBuilder("^");
        for (int i = 0; i < wildcard.length(); i++) {
            char c = wildcard.charAt(i);
            switch (c) {
                case '*':
                    regex.append(".*");
                    break;
                case '?':
                    regex.append(".");
                    break;
                case '.':
                    regex.append("\\.");
                    break;
                case '\\':
                    regex.append("\\\\");
                    break;
                case '(':
                case ')':
                case '[':
                case ']':
                case '{':
                case '}':
                case '^':
                case '$':
                case '|':
                case '+':
                    regex.append("\\").append(c);
                    break;
                default:
                    regex.append(c);
                    break;
            }
        }
        regex.append("$");
        return regex.toString();
    }

    public void copyToClipboard(boolean cut) {
        List<FileItem> selected = getSelectedItems();
        if (selected.isEmpty()) return;

        List<File> files = new ArrayList<>();
        for (FileItem item : selected) {
            files.add(item.getFile());
        }

        ClipboardService.copyToClipboard(files,
            cut ? ClipboardService.ClipboardAction.CUT : ClipboardService.ClipboardAction.COPY);
    }

    public void pasteFromClipboard() {
        List<File> files = ClipboardService.getFilesFromClipboard();
        if (files == null || files.isEmpty()) return;

        ClipboardService.ClipboardAction action = ClipboardService.getClipboardAction();

        List<FileItem> itemsToPaste = new ArrayList<>();
        for (File f : files) {
            itemsToPaste.add(new FileItem(f));
        }

        File targetDir = getCurrentDirectory();
        Window parentWindow = SwingUtilities.getWindowAncestor(this);
        String titleName = action == ClipboardService.ClipboardAction.CUT ? "Moving" : "Copying";
        ProgressDialog progressDialog = new ProgressDialog(parentWindow instanceof Frame ? (Frame)parentWindow : null, titleName);

        new Thread(() -> {
            try {
                FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
                    @Override
                    public void onProgress(long current, long total, String currentFile) {
                        progressDialog.updateProgress(current, total, currentFile);
                    }

                    @Override
                    public boolean isCancelled() {
                        return progressDialog.isCancelled();
                    }

                    @Override
                    public FileOperations.OverwriteResponse confirmOverwrite(File file) {
                        final FileOperations.OverwriteResponse[] result = new FileOperations.OverwriteResponse[1];
                        try {
                            SwingUtilities.invokeAndWait(() -> {
                                Object[] options = {"Yes", "Yes to All", "No", "No to All", "Cancel"};
                                int n = JOptionPane.showOptionDialog(progressDialog,
                                        "File already exists: " + file.getName() + "\nOverwrite?",
                                        "Overwrite Confirmation",
                                        JOptionPane.DEFAULT_OPTION,
                                        JOptionPane.QUESTION_MESSAGE,
                                        null,
                                        options,
                                        options[0]);

                                switch (n) {
                                    case 0: result[0] = FileOperations.OverwriteResponse.YES; break;
                                    case 1: result[0] = FileOperations.OverwriteResponse.YES_TO_ALL; break;
                                    case 2: result[0] = FileOperations.OverwriteResponse.NO; break;
                                    case 3: result[0] = FileOperations.OverwriteResponse.NO_TO_ALL; break;
                                    default: 
                                        result[0] = FileOperations.OverwriteResponse.CANCEL;
                                        progressDialog.cancel();
                                        break;
                                }
                            });
                        } catch (Exception e) {
                            result[0] = FileOperations.OverwriteResponse.CANCEL;
                        }
                        return result[0];
                    }
                };

                if (action == ClipboardService.ClipboardAction.CUT) {
                    FileOperations.move(itemsToPaste, targetDir, callback);
                } else {
                    FileOperations.copy(itemsToPaste, targetDir, callback);
                }
                SwingUtilities.invokeLater(() -> {
                    progressDialog.dispose();
                    loadDirectory(targetDir, false);
                    if (!itemsToPaste.isEmpty()) {
                        String nameToSelect = itemsToPaste.get(0).getName();
                        File firstSource = itemsToPaste.get(0).getFile();
                        // Check if we were copying within the same directory - if so, it was renamed to copy-of-...
                        if (action == ClipboardService.ClipboardAction.COPY && 
                            firstSource.getParentFile() != null && 
                            firstSource.getParentFile().getAbsolutePath().equals(targetDir.getAbsolutePath())) {
                            nameToSelect = "copy-of-" + nameToSelect;
                        }
                        final String finalName = nameToSelect;
                        // Use invokeLater to ensure layout (especially BRIEF mode) is updated before selection
                        SwingUtilities.invokeLater(() -> selectItem(finalName));
                    }
                });
            } catch (Exception ex) {
                SwingUtilities.invokeLater(() -> {
                    progressDialog.dispose();
                    JOptionPane.showMessageDialog(this, "Paste failed: " + ex.getMessage());
                });
            }
        }).start();
        progressDialog.setVisible(true);
    }

    /**
     * Mark/Select the very last item in the list
     */
    public void selectLastItem() {
        int count = tableModel.getRowCount();
        if (count > 0) {
            if (viewMode == ViewMode.BRIEF) {
                int lastIndex = tableModel.items.size() - 1;
                briefCurrentColumn = lastIndex / tableModel.briefRowsPerColumn;
                int row = lastIndex % tableModel.briefRowsPerColumn;
                fileTable.setRowSelectionInterval(row, row);
                fileTable.scrollRectToVisible(fileTable.getCellRect(row, briefCurrentColumn, true));
            } else {
                int last = count - 1;
                fileTable.setRowSelectionInterval(last, last);
                fileTable.scrollRectToVisible(fileTable.getCellRect(last, 0, true));
            }
            repaint();
            updateStatus();
        }
    }
    
    public void setViewMode(ViewMode mode) {
        setViewMode(mode, true);
    }

    public void setViewMode(ViewMode mode, boolean requestFocus) {
        if (this.viewMode != mode) {
            String selectedItemName = null;
            int selectedRow = fileTable.getSelectedRow();
            if (selectedRow >= 0) {
                FileItem item = null;
                if (this.viewMode == ViewMode.BRIEF) {
                    item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
                } else {
                    item = tableModel.getItem(selectedRow);
                }
                if (item != null) {
                    selectedItemName = item.getName();
                }
            }
            
            this.viewMode = mode;
            final String itemToSelect = selectedItemName;
            
            SwingUtilities.invokeLater(() -> {
                tableModel.updateViewMode(mode);
                // Switch auto-resize behavior depending on mode so BRIEF can scroll horizontally
                if (mode == ViewMode.BRIEF) {
                    fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
                } else {
                    fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
                }
                // Hide table header in BRIEF mode to save vertical space and match requirements
                if (fileTable.getTableHeader() != null) {
                    fileTable.getTableHeader().setVisible(mode != ViewMode.BRIEF);
                }
                briefCurrentColumn = 0;
                updateColumnRenderers();
                updateColumnWidths();
                fileTable.revalidate();
                fileTable.repaint();
                
                if (itemToSelect != null) {
                    selectItemByName(itemToSelect, requestFocus);
                } else if (fileTable.getRowCount() > 0) {
                    fileTable.setRowSelectionInterval(0, 0);
                }
                
                if (requestFocus) {
                    fileTable.requestFocusInWindow();
                }
            });
        }
    }
    
    private void updateColumnRenderers() {
        int columnCount = tableModel.getColumnCount();
        if (columnCount == 0 || fileTable.getColumnModel().getColumnCount() != columnCount) {
            // Table columns might not have been converted yet after structure change.
            // Try again asynchronously after a short delay.
            SwingUtilities.invokeLater(() -> updateColumnRenderers());
            return;
        }
        
        for (int i = 0; i < columnCount; i++) {
            final int colIndex = i;
            DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() {
                @Override
                public Component getTableCellRendererComponent(JTable table, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
                    super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                    
                    FileItem item = null;
                    if (viewMode == ViewMode.BRIEF) {
                        item = tableModel.getItemFromBriefLayout(row, column);
                    } else {
                        item = tableModel.getItem(row);
                    }
                    
                    if (item == null) {
                        setBackground(FilePanelTab.this.getBackground());
                        setText("");
                        setIcon(null);
                        return this;
                    }

                    // Prepare displayed text: wrap directory names in square brackets
                    String displayText = "";
                    if (viewMode == ViewMode.BRIEF) {
                        displayText = item.getName();
                        if (item.isDirectory()) {
                            displayText = "[" + displayText + "]";
                        }
                    } else {
                        // FULL mode: only the first column shows the name
                        if (column == 0) {
                            displayText = item.getName();
                            if (item.isDirectory()) {
                                displayText = "[" + displayText + "]";
                            }
                        } else if (column == 1) {
                            displayText = item.getFormattedSize();
                        } else if (column == 2) {
                            displayText = item.getFormattedDate();
                        } else {
                            displayText = item.getName();
                        }
                    }
                    setText(displayText);
                    
                    boolean isMarked = item.isMarked();
                    boolean isCurrentCell = false;
                    if (viewMode == ViewMode.BRIEF) {
                        isCurrentCell = isSelected && column == briefCurrentColumn;
                    } else {
                        isCurrentCell = isSelected;
                    }

                    // Only show selection highlight when this table has keyboard focus.
                    // If the table is not focused, do not highlight any row (show normal background).
                    if (isCurrentCell && table.hasFocus()) {
                        setBackground(selectionColor);
                    } else {
                        setBackground(FilePanelTab.this.getBackground());
                    }
                    
                    // Preserve the table's base font/style and only add the marked-bold
                    Font baseFont = table.getFont();
                    if (baseFont == null) baseFont = getFont();
                    int baseStyle = baseFont != null ? baseFont.getStyle() : Font.PLAIN;
                    
                    if (isMarked) {
                        // Ensure marked items are at least bold, preserve italic if present
                        int newStyle = baseStyle | Font.BOLD;
                        setFont(baseFont.deriveFont(newStyle));
                        setForeground(markedColor);
                    } else {
                        // Preserve whatever style the base font has (do not force plain)
                        setFont(baseFont.deriveFont(baseStyle));
                        // Automatically adjust foreground contrast
                        if (isCurrentCell && table.hasFocus()) {
                            setForeground(isDark(selectionColor) ? Color.WHITE : Color.BLACK);
                        } else {
                            setForeground(isDark(FilePanelTab.this.getBackground()) ? Color.WHITE : Color.BLACK);
                        }
                    }
                    
                    // Show icon for file names
                    Icon icon = item.getIcon();
                    if (item.getName().equals("..")) {
                        icon = new UpArrowIcon(getForeground());
                    } else if (item.isDirectory()) {
                        if (persistedConfig != null) {
                            icon = new ColoredFolderIcon(persistedConfig.getFolderColor());
                        }
                    } else {
                        // Use type-specific icon for files
                        icon = new FileSpecificIcon(getFileType(item.getName()));
                    }

                    if (viewMode == ViewMode.BRIEF) {
                        // In BRIEF mode all columns are names
                        setIcon(icon);
                    } else if (viewMode == ViewMode.FULL && column == 0) {
                        // In FULL mode the icon is only in the first column (names)
                        setIcon(icon);
                    } else {
                        setIcon(null);
                    }
                    
                    setBorder(null);
                    return this;
                }
            };
            
            if (viewMode == ViewMode.FULL && colIndex == 1) {
                renderer.setHorizontalAlignment(SwingConstants.RIGHT);
            }
            
            fileTable.getColumnModel().getColumn(i).setCellRenderer(renderer);
        }
    }
    
    private void updateColumnWidths() {
        if (viewMode == ViewMode.FULL) {
            if (fileTable.getColumnModel().getColumnCount() == 3) {
                fileTable.getColumnModel().getColumn(0).setPreferredWidth(300);
                fileTable.getColumnModel().getColumn(1).setPreferredWidth(100);
                fileTable.getColumnModel().getColumn(2).setPreferredWidth(150);
            }
        } else if (viewMode == ViewMode.BRIEF) {
            int columnCount = tableModel.getColumnCount();
            if (columnCount == 0) {
                return;
            }

            // If ColumnModel has not yet adapted to the new structure (e.g. after fireTableStructureChanged()),
            // wait and try to set renderers later - otherwise new columns won't get a renderer and icons won't
            // show up in FULL mode.
            if (fileTable.getColumnModel().getColumnCount() != columnCount) {
                SwingUtilities.invokeLater(() -> updateColumnRenderers());
                return;
            }

            // Turn off auto-resize so preferred widths are honored and horizontal scrolling appears
            fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

            // Compute the preferred column width based on the longest displayed
            // name in the current directory (include brackets for directories)
            // plus the widest icon width and some padding.
            java.awt.FontMetrics fm = fileTable.getFontMetrics(fileTable.getFont());
            int maxTextWidth = 0;
            int maxIconWidth = 0;

            for (FileItem item : tableModel.items) {
                if (item == null) continue;
                String display = item.getName();
                if (item.isDirectory()) {
                    display = "[" + display + "]";
                }
                int w = fm.stringWidth(display);
                if (w > maxTextWidth) maxTextWidth = w;

                Icon icon = item.getIcon();
                if (icon != null) {
                    int iw = icon.getIconWidth();
                    if (iw > maxIconWidth) maxIconWidth = iw;
                }
            }

            if (maxTextWidth == 0) {
                maxTextWidth = fm.stringWidth("WWWWWWWWWW");
            }

            // Add icon width and padding (left/right + gap between icon and text)
            int padding = 36; // reasonable extra space
            int columnWidth = maxTextWidth + maxIconWidth + padding;

            for (int i = 0; i < columnCount; i++) {
                fileTable.getColumnModel().getColumn(i).setPreferredWidth(columnWidth);
            }
        }
    }
    
    private void updateStatus() {
        long totalSize = 0;
        int fileCount = 0;
        int dirCount = 0;
        int markedCount = 0;
        
        if (viewMode == ViewMode.BRIEF) {
            for (int i = 0; i < tableModel.items.size(); i++) {
                FileItem item = tableModel.items.get(i);
                if (item.isMarked() && !item.getName().equals("..")) {
                    markedCount++;
                    if (item.isDirectory()) {
                        dirCount++;
                    } else {
                        fileCount++;
                        totalSize += item.getSize();
                    }
                }
            }
        } else {
            for (int i = 0; i < tableModel.getRowCount(); i++) {
                FileItem item = tableModel.getItem(i);
                if (item != null && item.isMarked() && !item.getName().equals("..")) {
                    markedCount++;
                    if (item.isDirectory()) {
                        dirCount++;
                    } else {
                        fileCount++;
                        totalSize += item.getSize();
                    }
                }
            }
        }
        
        if (markedCount > 0) {
            statusLabel.setText(String.format(" Selected: %d files, %d directories (%s)", 
                    fileCount, dirCount, formatSize(totalSize)));
        } else {
            int selectedRow = fileTable.getSelectedRow();
            if (selectedRow >= 0) {
                FileItem item = null;
                if (viewMode == ViewMode.BRIEF) {
                    item = tableModel.getItemFromBriefLayout(selectedRow, briefCurrentColumn);
                } else {
                    item = tableModel.getItem(selectedRow);
                }
                
                if (item != null && !item.getName().equals("..")) {
                    if (item.isDirectory()) {
                        // Always display directory names in square brackets
                        statusLabel.setText(String.format(" [%s]  |  %s",
                                item.getName(),
                                item.getFormattedDate()));
                    } else {
                        statusLabel.setText(String.format(" %s  |  %s  |  %s", 
                                item.getName(), 
                                formatSize(item.getSize()),
                                item.getFormattedDate()));
                    }
                } else {
                    statusLabel.setText(String.format(" Items: %d", tableModel.items.size()));
                }
            } else {
                statusLabel.setText(String.format(" Items: %d", tableModel.items.size()));
            }
        }
    }

    /**
     * Sort items in the current table model according to column (FULL mode).
     * column: 0=name, 1=size, 2=date
     */
    private void sortItemsByColumn(int column, boolean asc) {
        if (tableModel == null || tableModel.items == null) return;
        java.util.List<FileItem> items = tableModel.items;
        if (items.isEmpty()) return;

    java.util.Comparator<FileItem> comp;
        switch (column) {
            case 1: // size
                comp = (a, b) -> {
                    boolean da = a.isDirectory(), db = b.isDirectory();
                    if (da != db) return da ? -1 : 1;
                    if (da && db) return a.getName().compareToIgnoreCase(b.getName());
                    int r = Long.compare(a.getSize(), b.getSize());
                    if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
                    return r;
                };
                break;
            case 2: // date
                // Sort by modification time regardless of directory flag so "newest first" places the newest item
                comp = (a, b) -> {
                    int r = Long.compare(a.getModified().getTime(), b.getModified().getTime());
                    if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
                    return r;
                };
                break;
            default: // name
                comp = (a, b) -> compareFileItemsByName(a, b);
                break;
        }

        if (!asc) comp = comp.reversed();

        items.sort(comp);

        // Refresh table on EDT
        SwingUtilities.invokeLater(() -> {
            if (viewMode == ViewMode.BRIEF) {
                tableModel.calculateBriefLayout();
                tableModel.fireTableStructureChanged();
            } else {
                tableModel.fireTableDataChanged();
            }
            updateColumnRenderers();
            updateColumnWidths();
            if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
        });
    }

    /**
     * Compare two FileItem objects by name with the following enhancements:
     * - directories come before files
     * - visible files come before hidden files
     * - natural numeric-aware comparison ("file2" < "file10")
     * - when names are equal ignoring case, prefer uppercase letters earlier (so "Apple" before "apple")
     */
    private int compareFileItemsByName(FileItem a, FileItem b) {
        if (a == b) return 0;
        // Directories first
        boolean da = a.isDirectory(), db = b.isDirectory();
        if (da != db) return da ? -1 : 1;

        // Hidden files ordering based on config (default: hidden last)
        boolean hiddenLast = true;
        try {
            if (persistedConfig != null) hiddenLast = persistedConfig.getHiddenFilesLast();
        } catch (Exception ignore) {}
        try {
            boolean ha = a.getFile() != null && a.getFile().isHidden();
            boolean hb = b.getFile() != null && b.getFile().isHidden();
            if (ha != hb) return hiddenLast ? (ha ? 1 : -1) : (ha ? -1 : 1);
        } catch (Exception ignore) {}

        String s1 = a.getName() != null ? a.getName() : "";
        String s2 = b.getName() != null ? b.getName() : "";

        // Optionally ignore leading dots (treat ".name" as "name") based on config
        try {
            if (persistedConfig != null && persistedConfig.getIgnoreLeadingDot()) {
                s1 = s1.replaceFirst("^\\.+", "");
                s2 = s2.replaceFirst("^\\.+", "");
            }
        } catch (Exception ignore) {}

        // Numeric-aware vs simple compare based on config
        boolean numericEnabled = true;
        try { if (persistedConfig != null) numericEnabled = persistedConfig.getNumericSortEnabled(); } catch (Exception ignore) {}

        if (numericEnabled) {
            // Use natural compare; uppercase preference handled inside
            return naturalCompareWithUppercasePreference(s1, s2);
        } else {
            // simple case-insensitive compare, optionally uppercase preference
            int ci = s1.compareToIgnoreCase(s2);
            if (ci != 0) return ci;
            boolean uppercasePref = true;
            try { if (persistedConfig != null) uppercasePref = persistedConfig.getUppercasePriority(); } catch (Exception ignore) {}
            if (uppercasePref) {
                // prefer uppercase earlier
                int len = Math.min(s1.length(), s2.length());
                for (int k = 0; k < len; k++) {
                    char aChar = s1.charAt(k);
                    char bChar = s2.charAt(k);
                    if (aChar == bChar) continue;
                    char la = Character.toLowerCase(aChar);
                    char lb = Character.toLowerCase(bChar);
                    if (la != lb) return la - lb;
                    boolean ua = Character.isUpperCase(aChar);
                    boolean ub = Character.isUpperCase(bChar);
                    if (ua != ub) return ua ? -1 : 1;
                }
                return s1.length() - s2.length();
            } else {
                return s1.compareTo(s2);
            }
        }
    }

    /**
     * Natural compare of two strings with numeric awareness and uppercase preference.
     */
    private int naturalCompareWithUppercasePreference(String s1, String s2) {
        int i = 0, j = 0;
        int n1 = s1.length(), n2 = s2.length();

        while (i < n1 && j < n2) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(j);

            // If both are digits, compare whole number sequences numerically
            if (Character.isDigit(c1) && Character.isDigit(c2)) {
                int start1 = i, start2 = j;
                while (i < n1 && Character.isDigit(s1.charAt(i))) i++;
                while (j < n2 && Character.isDigit(s2.charAt(j))) j++;
                String num1 = s1.substring(start1, i);
                String num2 = s2.substring(start2, j);

                // Remove leading zeros for numeric comparison, but keep length for tie-break
                String nz1 = num1.replaceFirst("^0+(?!$)", "");
                String nz2 = num2.replaceFirst("^0+(?!$)", "");
                if (nz1.length() != nz2.length()) return nz1.length() - nz2.length();
                int cmp = nz1.compareTo(nz2);
                if (cmp != 0) return cmp;
                // If equal numerically, shorter original with fewer leading zeros first
                if (num1.length() != num2.length()) return num1.length() - num2.length();
                continue;
            }

            // Non-digit comparison: case-insensitive first
            StringBuilder sb1 = new StringBuilder();
            StringBuilder sb2 = new StringBuilder();
            int ti = i, tj = j;
            while (ti < n1 && !Character.isDigit(s1.charAt(ti))) {
                sb1.append(s1.charAt(ti));
                ti++;
            }
            while (tj < n2 && !Character.isDigit(s2.charAt(tj))) {
                sb2.append(s2.charAt(tj));
                tj++;
            }

            String part1 = sb1.toString();
            String part2 = sb2.toString();

            int ci = compareStringPartWithUppercasePreference(part1, part2);
            if (ci != 0) return ci;

            i = ti;
            j = tj;
        }

        // If one string ended earlier, shorter one is first
        if (i < n1) return 1; // s2 ended, s1 longer -> s2 first
        if (j < n2) return -1;
        return 0;
    }

    /**
     * Compare two non-digit string parts: case-insensitive, but if equal ignoring case,
     * prefer the one with uppercase letters earlier.
     */
    private int compareStringPartWithUppercasePreference(String p1, String p2) {
        int ci = p1.compareToIgnoreCase(p2);
        if (ci != 0) return ci;
        // If equal ignoring case, prefer uppercase earlier
        int len = Math.min(p1.length(), p2.length());
        for (int k = 0; k < len; k++) {
            char a = p1.charAt(k);
            char b = p2.charAt(k);
            if (a == b) continue;
            char la = Character.toLowerCase(a);
            char lb = Character.toLowerCase(b);
            if (la != lb) return la - lb; // different letters
            // same letter ignoring case, decide by uppercase preference
            boolean ua = Character.isUpperCase(a);
            boolean ub = Character.isUpperCase(b);
            if (ua != ub) return ua ? -1 : 1; // uppercase comes first
        }
        // If equal up to min length, shorter one first
        return p1.length() - p2.length();
    }

    /** Header renderer that decorates the column title with an arrow for the sort column/direction */
    private class SortHeaderRenderer implements javax.swing.table.TableCellRenderer {
        private final javax.swing.table.TableCellRenderer delegate;

        public SortHeaderRenderer(javax.swing.table.TableCellRenderer delegate) {
            this.delegate = delegate;
        }

        @Override
        public java.awt.Component getTableCellRendererComponent(JTable table, Object value,
                                                                boolean isSelected, boolean hasFocus,
                                                                int row, int column) {
            java.awt.Component c = delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            if (c instanceof javax.swing.JLabel) {
                javax.swing.JLabel lbl = (javax.swing.JLabel) c;
                String txt = value != null ? value.toString() : "";
                if (sortColumn == column && fileTable.getTableHeader().isVisible()) {
                    String arrow = sortAscending ? " ▲" : " ▼";
                    lbl.setText(txt + arrow);
                } else {
                    lbl.setText(txt);
                }
            }
            return c;
        }
    }

    /**
     * Provide AppConfig so this tab can persist and restore sort settings.
     */
    public void setAppConfig(cz.kamma.kfmanager.config.AppConfig cfg) {
        this.persistedConfig = cfg;
        // Apply persisted sort if present
        if (cfg != null) {
            java.util.List<String> multi = cfg.getMultipleSortCriteria();
            if (multi != null && !multi.isEmpty()) {
                applyMultipleSortCriteria(multi);
            } else {
                int col = cfg.getDefaultSortColumn();
                boolean asc = cfg.getDefaultSortAscending();
                if (col >= 0) {
                    this.sortColumn = col;
                    this.sortAscending = asc;
                    sortItemsByColumn(sortColumn, sortAscending);
                    SwingUtilities.invokeLater(() -> {
                        tableModel.fireTableDataChanged();
                        if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
                    });
                }
            }
        }
    }

    /**
     * Apply a list of composite sort criteria in order (e.g. "name:asc", "size:desc").
     */
    private void applyMultipleSortCriteria(java.util.List<String> criteria) {
        if (criteria == null || criteria.isEmpty()) return;
        if (tableModel == null || tableModel.items == null) return;

        java.util.Comparator<FileItem> comp = null;

        for (String c : criteria) {
            if (c == null || c.trim().isEmpty()) continue;
            String[] parts = c.split(":");
            String field = parts[0].trim().toLowerCase();
            boolean asc = true;
            if (parts.length > 1 && parts[1].trim().equalsIgnoreCase("desc")) asc = false;

            java.util.Comparator<FileItem> partComp;
            switch (field) {
                case "size":
                    partComp = (a, b) -> {
                        boolean da = a.isDirectory(), db = b.isDirectory();
                        if (da != db) return da ? -1 : 1;
                        if (da && db) return a.getName().compareToIgnoreCase(b.getName());
                        int r = Long.compare(a.getSize(), b.getSize());
                        if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
                        return r;
                    };
                    break;
                case "date":
                    partComp = (a, b) -> {
                        int r = Long.compare(a.getModified().getTime(), b.getModified().getTime());
                        if (r == 0) r = a.getName().compareToIgnoreCase(b.getName());
                        return r;
                    };
                    break;
                default:
                    partComp = (a, b) -> {
                        boolean da = a.isDirectory(), db = b.isDirectory();
                        if (da != db) return da ? -1 : 1;
                        return a.getName().compareToIgnoreCase(b.getName());
                    };
                    break;
            }

            if (!asc) partComp = partComp.reversed();

            if (comp == null) comp = partComp;
            else comp = comp.thenComparing(partComp);
        }

        if (comp == null) return;

        java.util.List<FileItem> items = tableModel.items;
        items.sort(comp);

        SwingUtilities.invokeLater(() -> {
            if (viewMode == ViewMode.BRIEF) {
                tableModel.calculateBriefLayout();
                tableModel.fireTableStructureChanged();
            } else {
                tableModel.fireTableDataChanged();
            }
            updateColumnRenderers();
            updateColumnWidths();
            if (fileTable.getTableHeader() != null) fileTable.getTableHeader().repaint();
        });
    }
    
    private String formatSize(long size) {
        if (size < 1024) {
            return size + " B";
        } else if (size < 1024 * 1024) {
            return String.format("%.1f KB", size / 1024.0);
        } else if (size < 1024 * 1024 * 1024) {
            return String.format("%.1f MB", size / (1024.0 * 1024.0));
        } else {
            return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
        }
    }
    
    // Getters
    public JTable getFileTable() {
        return fileTable;
    }
    
    public File getCurrentDirectory() {
        return currentDirectory;
    }
    
    public ViewMode getViewMode() {
        return viewMode;
    }
    
    // FileTableModel
    private class FileTableModel extends AbstractTableModel {
        private List<FileItem> items = new ArrayList<>();
        private String[] columnNames = {"Name", "Size", "Date"};
        public int briefColumns = 1;
        public int briefRowsPerColumn = 10;
        
        public void setItems(List<FileItem> items) {
            this.items = items;
            if (viewMode == ViewMode.BRIEF) {
                calculateBriefLayout();
            }
            fireTableDataChanged();
        }
        
        public void updateViewMode(ViewMode mode) {
            if (mode == ViewMode.BRIEF) {
                calculateBriefLayout();
            }
            fireTableStructureChanged();
        }
        
        public void calculateBriefLayout() {
            if (items.isEmpty()) {
                briefColumns = 1;
                briefRowsPerColumn = 1;
                return;
            }
            
            int availableHeight = fileTable.getParent().getHeight();
            int rowHeight = fileTable.getRowHeight();
            
            if (availableHeight <= 0 || rowHeight <= 0) {
                briefRowsPerColumn = Math.max(1, items.size());
                briefColumns = 1;
                return;
            }
            
            briefRowsPerColumn = Math.max(1, availableHeight / rowHeight);
            briefColumns = (int) Math.ceil((double) items.size() / briefRowsPerColumn);
        }
        
        public FileItem getItemFromBriefLayout(int row, int column) {
            if (viewMode != ViewMode.BRIEF) {
                return getItem(row);
            }
            
            int index = column * briefRowsPerColumn + row;
            if (index >= 0 && index < items.size()) {
                return items.get(index);
            }
            return null;
        }
        
        @Override
        public int getRowCount() {
            if (viewMode == ViewMode.BRIEF) {
                return briefRowsPerColumn;
            }
            return items.size();
        }
        
        @Override
        public int getColumnCount() {
            if (viewMode == ViewMode.BRIEF) {
                return briefColumns;
            }
            return 3;
        }
        
        @Override
        public String getColumnName(int column) {
            if (viewMode == ViewMode.BRIEF) {
                return "";
            }
            return columnNames[column];
        }
        
        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            if (viewMode == ViewMode.BRIEF) {
                FileItem item = getItemFromBriefLayout(rowIndex, columnIndex);
                return item != null ? item.getName() : "";
            }
            
            FileItem item = getItem(rowIndex);
            if (item == null) {
                return "";
            }
            
            switch (columnIndex) {
                case 0: return item.getName();
                case 1: return item.getFormattedSize();
                case 2: return item.getFormattedDate();
                default: return "";
            }
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            if (!inlineRenameActive) return false;
            
            if (viewMode == ViewMode.BRIEF) {
                FileItem it = getItemFromBriefLayout(rowIndex, columnIndex);
                return it != null; // allow editing name in brief cells
            }
            // In FULL mode only the name column is editable
            return columnIndex == 0;
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            String newName = aValue != null ? aValue.toString().trim() : null;
            if (newName == null || newName.isEmpty()) return;

            FileItem item = null;
            if (viewMode == ViewMode.BRIEF) {
                item = getItemFromBriefLayout(rowIndex, columnIndex);
            } else {
                item = getItem(rowIndex);
            }

            if (item == null) return;

            // Perform rename using FileOperations and refresh the directory
            try {
                cz.kamma.kfmanager.service.FileOperations.rename(item.getFile(), newName);
                // reload current directory to reflect updated names
                FilePanelTab.this.loadDirectory(FilePanelTab.this.getCurrentDirectory());
                // After reload, select the renamed item and focus the table
                SwingUtilities.invokeLater(() -> {
                    try {
                        FilePanelTab.this.selectItem(newName);
                        FilePanelTab.this.getFileTable().requestFocusInWindow();
                    } catch (Exception ignore) {}
                });
            } catch (Exception ex) {
                // show error to user
                try {
                    JOptionPane.showMessageDialog(FilePanelTab.this,
                            "Rename failed: " + ex.getMessage(),
                            "Error",
                            JOptionPane.ERROR_MESSAGE);
                } catch (Exception ignore) {}
            }
        }
        
        public FileItem getItem(int index) {
            if (index >= 0 && index < items.size()) {
                return items.get(index);
            }
            return null;
        }
    }

    /**
     * Custom icon for directories that uses a configurable color.
     */
    private static class ColoredFolderIcon implements Icon {
        private final Color color;

        public ColoredFolderIcon(Color color) {
            this.color = color;
        }

        @Override
        public void paintIcon(Component c, Graphics g, int x, int y) {
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            g2.setColor(color);
            // Draw a simple folder shape
            // Main body
            g2.fillRect(x + 2, y + 4, 12, 8);
            // Tab
            g2.fillRect(x + 2, y + 2, 5, 2);
            
            // Optional: outline for depth
            g2.setColor(color.darker());
            g2.drawRect(x + 2, y + 4, 12, 8);
            
            g2.dispose();
        }

        @Override
        public int getIconWidth() {
            return 16;
        }

        @Override
        public int getIconHeight() {
            return 16;
        }
    }

    /**
     * Custom icon for ".." that shows an upward arrow.
     */
    private static class UpArrowIcon implements Icon {
        private final Color color;

        public UpArrowIcon(Color color) {
            this.color = color;
        }

        @Override
        public void paintIcon(Component c, Graphics g, int x, int y) {
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            g2.setColor(color);
            // Draw a simple upward arrow
            int[] xPoints = {x + 8, x + 3, x + 13};
            int[] yPoints = {y + 3, y + 10, y + 10};
            g2.fillPolygon(xPoints, yPoints, 3);
            g2.fillRect(x + 7, y + 10, 2, 4);
            
            g2.dispose();
        }

        @Override
        public int getIconWidth() {
            return 16;
        }

        @Override
        public int getIconHeight() {
            return 16;
        }
    }

    /**
     * Custom icon for files that provides different visuals for different file types.
     */
    private static class FileSpecificIcon implements Icon {
        public enum Type { IMAGE, VIDEO, AUDIO, ARCHIVE, CODE, TEXT, DOC, EXEC, OTHER }
        private final Type type;
        private final Color color;

        public FileSpecificIcon(Type type) {
            this.type = type;
            switch (type) {
                case IMAGE: this.color = new Color(70, 130, 180); break; // SteelBlue
                case VIDEO: this.color = new Color(205, 92, 92); break; // IndianRed
                case AUDIO: this.color = new Color(147, 112, 219); break; // MediumSlateBlue
                case ARCHIVE: this.color = new Color(210, 180, 140); break; // Tan
                case CODE: this.color = new Color(60, 179, 113); break; // MediumSeaGreen
                case TEXT: this.color = Color.GRAY; break;
                case DOC: this.color = new Color(70, 90, 180); break; // Darker blue
                case EXEC: this.color = new Color(0, 128, 128); break; // Teal
                default: this.color = Color.LIGHT_GRAY; break;
            }
        }

        @Override
        public void paintIcon(Component c, Graphics g, int x, int y) {
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            g2.setColor(color);
            // Draw a basic "sheet" shape
            g2.fillRect(x + 3, y + 2, 10, 13);
            
            // Draw a tiny folded corner
            g2.setColor(color.darker());
            int[] cx = {x + 10, x + 13, x + 10};
            int[] cy = {y + 2, y + 5, y + 5};
            g2.fillPolygon(cx, cy, 3);

            // Draw a small indicator for the type
            g2.setColor(Color.WHITE);
            switch (type) {
                case IMAGE:
                    g2.fillOval(x + 5, y + 6, 2, 2); // "Sun"
                    g2.drawRect(x + 5, y + 9, 4, 3); // Simple mountain
                    break;
                case VIDEO:
                    int[] vxp = {x + 6, x + 6, x + 10};
                    int[] vyp = {y + 5, y + 11, y + 8};
                    g2.fillPolygon(vxp, vyp, 3);
                    break;
                case AUDIO:
                    g2.fillRect(x + 6, y + 5, 3, 2);
                    g2.fillRect(x + 8, y + 5, 1, 6);
                    g2.fillOval(x + 5, y + 10, 3, 3);
                    break;
                case ARCHIVE:
                    g2.drawRect(x + 5, y + 4, 6, 8);
                    g2.drawLine(x + 5, y + 7, x + 11, y + 7);
                    g2.drawLine(x + 5, y + 10, x + 11, y + 10);
                    break;
                case CODE:
                    g2.drawLine(x + 5, y + 9, x + 7, y + 7); // <
                    g2.drawLine(x + 5, y + 9, x + 7, y + 11);
                    g2.drawLine(x + 9, y + 7, x + 11, y + 9); // >
                    g2.drawLine(x + 9, y + 11, x + 11, y + 9);
                    break;
                case TEXT:
                case DOC:
                    g2.drawLine(x + 5, y + 6, x + 11, y + 6);
                    g2.drawLine(x + 5, y + 9, x + 11, y + 9);
                    g2.drawLine(x + 5, y + 12, x + 8, y + 12);
                    break;
                case EXEC:
                    g2.drawOval(x + 5, y + 5, 6, 6);
                    g2.drawLine(x + 8, y + 4, x + 8, y + 12);
                    g2.drawLine(x + 4, y + 8, x + 12, y + 8);
                    break;
                default:
                    break;
            }
            
            g2.dispose();
        }

        @Override
        public int getIconWidth() { return 16; }

        @Override
        public int getIconHeight() { return 16; }
    }

    private FileSpecificIcon.Type getFileType(String fileName) {
        String name = fileName.toLowerCase();
        if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png") ||
            name.endsWith(".gif") || name.endsWith(".bmp") || name.endsWith(".svg") ||
            name.endsWith(".webp") || name.endsWith(".ico") || name.endsWith(".tif")) return FileSpecificIcon.Type.IMAGE;
        
        if (name.endsWith(".mp4") || name.endsWith(".mkv") || name.endsWith(".avi") ||
            name.endsWith(".mov") || name.endsWith(".wmv") || name.endsWith(".flv") ||
            name.endsWith(".webm") || name.endsWith(".mpg") || name.endsWith(".mpeg")) return FileSpecificIcon.Type.VIDEO;
            
        if (name.endsWith(".mp3") || name.endsWith(".wav") || name.endsWith(".ogg") ||
            name.endsWith(".flac") || name.endsWith(".m4a") || name.endsWith(".aac") ||
            name.endsWith(".wma")) return FileSpecificIcon.Type.AUDIO;
            
        if (name.endsWith(".zip") || name.endsWith(".rar") || name.endsWith(".7z") ||
            name.endsWith(".tar") || name.endsWith(".gz") || name.endsWith(".bz2") ||
            name.endsWith(".xz") || name.endsWith(".jar") || name.endsWith(".deb") ||
            name.endsWith(".rpm")) return FileSpecificIcon.Type.ARCHIVE;
            
        if (name.endsWith(".java") || name.endsWith(".c") || name.endsWith(".cpp") ||
            name.endsWith(".h") || name.endsWith(".py") || name.endsWith(".js") ||
            name.endsWith(".html") || name.endsWith(".css") || name.endsWith(".ts") ||
            name.endsWith(".xml") || name.endsWith(".json") || name.endsWith(".sh") ||
            name.endsWith(".php") || name.endsWith(".kt") || name.endsWith(".cs") ||
            name.endsWith(".go") || name.endsWith(".rb") || name.endsWith(".sql")) return FileSpecificIcon.Type.CODE;
            
        if (name.endsWith(".pdf") || name.endsWith(".doc") || name.endsWith(".docx") ||
            name.endsWith(".odt") || name.endsWith(".xls") || name.endsWith(".xlsx") ||
            name.endsWith(".ppt") || name.endsWith(".pptx") || name.endsWith(".rtf")) return FileSpecificIcon.Type.DOC;
            
        if (name.endsWith(".txt") || name.endsWith(".md") || name.endsWith(".log") ||
            name.endsWith(".conf") || name.endsWith(".ini") || name.endsWith(".csv") ||
            name.endsWith(".properties")) return FileSpecificIcon.Type.TEXT;

        if (name.endsWith(".exe") || name.endsWith(".bin") || name.endsWith(".com") ||
            name.endsWith(".bat") || name.endsWith(".msi") || name.endsWith(".appimage")) return FileSpecificIcon.Type.EXEC;
            
        return FileSpecificIcon.Type.OTHER;
    }

    private void showAssociationDialog(File file) {
        String name = file.getName();
        String ext = "";
        int lastDot = name.lastIndexOf('.');
        if (lastDot > 0 && lastDot < name.length() - 1) {
            ext = name.substring(lastDot + 1);
        } else {
            ext = name; // fallback to full name if no extension
        }

        String pattern = JOptionPane.showInputDialog(this, 
            "File pattern (e.g. *.txt or txt):", 
            ext);
        
        if (pattern == null || pattern.trim().isEmpty()) return;

        // If user just provided an extension like "txt", convert it to "*.txt" internally for consistency
        String normalizedPattern = pattern.trim();
        if (!normalizedPattern.contains("*") && !normalizedPattern.contains("?")) {
            normalizedPattern = "*." + normalizedPattern;
        }

        String existingCmd = "";
        if (persistedConfig != null) {
            java.util.Map<String, String> associations = persistedConfig.getFileAssociations();
            existingCmd = associations.getOrDefault(normalizedPattern, "");
        }

        String command = JOptionPane.showInputDialog(this, 
            "Command to run (%f for full path, %n for name):", 
            existingCmd);
        
        if (command != null && !command.trim().isEmpty()) {
            if (persistedConfig != null) {
                persistedConfig.setFileAssociation(normalizedPattern, command.trim());
                JOptionPane.showMessageDialog(this, "Association saved for " + normalizedPattern);
            }
        }
    }
}
