"""
Thumbnail Browser Widget - displays thumbnails from a folder for drag-and-drop into album.
"""

import os
from pathlib import Path
from typing import Optional, List, Tuple

from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QDockWidget, QScrollBar
from PyQt6.QtCore import Qt, QSize, QMimeData, QUrl, QPoint
from PyQt6.QtGui import QDrag, QCursor, QPainter, QFont, QColor
from PyQt6.QtOpenGLWidgets import QOpenGLWidget

from pyPhotoAlbum.gl_imports import *
from pyPhotoAlbum.mixins.viewport import ViewportMixin

IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp"]


class DateHeader:
    """Represents a date separator header in the thumbnail list."""

    def __init__(self, date_text: str, y_position: float):
        self.date_text = date_text
        self.y = y_position
        self.height = 30.0  # Height of the header bar


class ThumbnailItem:
    """Represents a thumbnail with position and path information."""

    def __init__(self, image_path: str, grid_pos: Tuple[int, int], thumbnail_size: float = 100.0):
        self.image_path = image_path
        self.grid_row, self.grid_col = grid_pos
        self.thumbnail_size = thumbnail_size
        self.is_used_in_project = False  # Will be updated when checking against project

        # Position in mm (will be calculated based on grid)
        spacing = 10.0  # mm spacing between thumbnails
        self.x = self.grid_col * (self.thumbnail_size + spacing) + spacing
        self.y = self.grid_row * (self.thumbnail_size + spacing) + spacing

        # Texture info (loaded async)
        self._texture_id = None
        self._pending_pil_image = None
        self._async_loading = False
        self._img_width = None
        self._img_height = None

    def get_bounds(self) -> Tuple[float, float, float, float]:
        """Return (x, y, width, height) bounds."""
        return (self.x, self.y, self.thumbnail_size, self.thumbnail_size)

    def contains_point(self, x: float, y: float) -> bool:
        """Check if point is inside this thumbnail."""
        return self.x <= x <= self.x + self.thumbnail_size and self.y <= y <= self.y + self.thumbnail_size


class ThumbnailGLWidget(QOpenGLWidget):
    """
    OpenGL widget that displays thumbnails in a grid.
    Uses the same async loading and texture system as the main canvas.
    """

    def __init__(self, main_window=None):
        super().__init__()

        self.thumbnails: List[ThumbnailItem] = []
        self.date_headers: List[DateHeader] = []
        self.current_folder: Optional[Path] = None

        # Store reference to main window
        self._main_window = main_window

        # Viewport state
        self.zoom_level = 1.0
        self.pan_offset = (0, 0)

        # Dragging state
        self.drag_start_pos = None
        self.dragging_thumbnail = None

        # Scrollbar (created but managed by parent)
        self.scrollbar = None
        self._updating_scrollbar = False  # Flag to prevent circular updates

        # Sort mode (set by parent dock)
        self.sort_mode = "name"
        self._get_image_date_func = None  # Function to get date from parent

        # Enable OpenGL
        self.setMinimumSize(QSize(250, 300))

    def window(self):
        """Override window() to return stored main_window reference."""
        return self._main_window if self._main_window else super().window()

    def update(self):
        """Override update to batch repaints for better performance."""
        # Just schedule the update - Qt will automatically batch multiple
        # update() calls into a single paintGL() invocation
        super().update()

    def initializeGL(self):
        """Initialize OpenGL context."""
        glClearColor(0.95, 0.95, 0.95, 1.0)  # Light gray background
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glEnable(GL_TEXTURE_2D)

    def resizeGL(self, w, h):
        """Handle resize events."""
        glViewport(0, 0, w, h)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glOrtho(0, w, h, 0, -1, 1)  # 2D orthographic projection
        glMatrixMode(GL_MODELVIEW)

        # Rearrange thumbnails to fit new width
        if hasattr(self, "image_files") and self.image_files:
            self._arrange_thumbnails()
        else:
            # Still update scrollbar even if no thumbnails
            self._update_scrollbar_range()

    def paintGL(self):
        """Render thumbnails."""
        glClear(GL_COLOR_BUFFER_BIT)
        glLoadIdentity()

        if not self.thumbnails:
            return

        # Apply zoom and pan
        glTranslatef(self.pan_offset[0], self.pan_offset[1], 0)
        glScalef(self.zoom_level, self.zoom_level, 1.0)

        # Render date headers first (so they appear behind thumbnails)
        for header in self.date_headers:
            self._render_date_header(header)

        # Render each thumbnail (placeholders or textures)
        for thumb in self.thumbnails:
            self._render_thumbnail(thumb)

    def paintEvent(self, event):
        """Override paintEvent to add text labels after OpenGL rendering."""
        # Call the default OpenGL paint
        super().paintEvent(event)

        # Draw text labels for date headers using QPainter
        if self.date_headers and self.sort_mode == "date":
            painter = QPainter(self)
            painter.setRenderHint(QPainter.RenderHint.Antialiasing)

            # Set font for date labels
            font = QFont("Arial", 11, QFont.Weight.Bold)
            painter.setFont(font)
            painter.setPen(QColor(255, 255, 255))  # White text

            for header in self.date_headers:
                # Transform header position to screen coordinates
                screen_y = header.y * self.zoom_level + self.pan_offset[1]
                screen_h = header.height * self.zoom_level

                # Only draw if header is visible
                if screen_y + screen_h >= 0 and screen_y <= self.height():
                    # Draw text centered vertically in the header bar
                    text_y = int(screen_y + screen_h / 2)
                    painter.drawText(10, text_y + 5, header.date_text)

            painter.end()

    def _render_thumbnail(self, thumb: ThumbnailItem):
        """Render a single thumbnail using placeholder pattern."""
        x, y, w, h = thumb.get_bounds()

        # If we have a pending image, convert it to texture (happens once per image)
        if hasattr(thumb, "_pending_pil_image") and thumb._pending_pil_image is not None:
            self._create_texture_for_thumbnail(thumb)

        # Render based on state: texture, loading placeholder, or empty placeholder
        if thumb._texture_id:
            # Calculate aspect-ratio-corrected dimensions
            if hasattr(thumb, "_img_width") and hasattr(thumb, "_img_height"):
                img_aspect = thumb._img_width / thumb._img_height
                thumb_aspect = w / h

                if img_aspect > thumb_aspect:
                    # Image is wider - fit to width
                    render_w = w
                    render_h = w / img_aspect
                    render_x = x
                    render_y = y + (h - render_h) / 2
                else:
                    # Image is taller - fit to height
                    render_h = h
                    render_w = h * img_aspect
                    render_x = x + (w - render_w) / 2
                    render_y = y
            else:
                # No aspect ratio info, use full bounds
                render_x, render_y, render_w, render_h = x, y, w, h

            # Render actual texture
            glEnable(GL_TEXTURE_2D)
            glBindTexture(GL_TEXTURE_2D, thumb._texture_id)

            # If used in project, desaturate by tinting grey
            if thumb.is_used_in_project:
                glColor4f(0.5, 0.5, 0.5, 0.6)  # Grey tint + partial transparency
            else:
                glColor4f(1.0, 1.0, 1.0, 1.0)

            glBegin(GL_QUADS)
            glTexCoord2f(0.0, 0.0)
            glVertex2f(render_x, render_y)
            glTexCoord2f(1.0, 0.0)
            glVertex2f(render_x + render_w, render_y)
            glTexCoord2f(1.0, 1.0)
            glVertex2f(render_x + render_w, render_y + render_h)
            glTexCoord2f(0.0, 1.0)
            glVertex2f(render_x, render_y + render_h)
            glEnd()

            glDisable(GL_TEXTURE_2D)
        else:
            # Render placeholder (grey box while loading or if load failed)
            glColor3f(0.8, 0.8, 0.8)
            glBegin(GL_QUADS)
            glVertex2f(x, y)
            glVertex2f(x + w, y)
            glVertex2f(x + w, y + h)
            glVertex2f(x, y + h)
            glEnd()

        # Border
        glColor3f(0.5, 0.5, 0.5)
        glLineWidth(1.0)
        glBegin(GL_LINE_LOOP)
        glVertex2f(x, y)
        glVertex2f(x + w, y)
        glVertex2f(x + w, y + h)
        glVertex2f(x, y + h)
        glEnd()

    def _render_date_header(self, header: DateHeader):
        """Render a date separator header."""
        # Calculate full width bar
        widget_width = self.width() / self.zoom_level
        x = 0
        y = header.y
        w = widget_width
        h = header.height

        # Draw background bar (dark blue-gray)
        glColor3f(0.3, 0.4, 0.5)
        glBegin(GL_QUADS)
        glVertex2f(x, y)
        glVertex2f(x + w, y)
        glVertex2f(x + w, y + h)
        glVertex2f(x, y + h)
        glEnd()

        # Draw bottom border
        glColor3f(0.2, 0.3, 0.4)
        glLineWidth(2.0)
        glBegin(GL_LINES)
        glVertex2f(x, y + h)
        glVertex2f(x + w, y + h)
        glEnd()

        # Note: Text rendering would require QPainter overlay
        # For now, the colored bar serves as a visual separator
        # Text will be added using QPainter in a future enhancement

    def _create_texture_for_thumbnail(self, thumb: ThumbnailItem):
        """Create OpenGL texture from pending PIL image."""
        if not thumb._pending_pil_image:
            return False

        try:
            pil_image = thumb._pending_pil_image

            # Ensure RGBA
            if pil_image.mode != "RGBA":
                pil_image = pil_image.convert("RGBA")

            # Delete old texture
            if thumb._texture_id:
                glDeleteTextures([thumb._texture_id])

            # Create texture
            img_data = pil_image.tobytes()
            texture_id = glGenTextures(1)
            glBindTexture(GL_TEXTURE_2D, texture_id)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
            glTexImage2D(
                GL_TEXTURE_2D, 0, GL_RGBA, pil_image.width, pil_image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data
            )

            thumb._texture_id = texture_id
            thumb._img_width = pil_image.width
            thumb._img_height = pil_image.height
            thumb._pending_pil_image = None

            return True

        except Exception as e:
            print(f"Error creating texture for thumbnail: {e}")
            thumb._pending_pil_image = None
            return False

    def load_folder(self, folder_path: Path):
        """Load thumbnails from a folder."""
        self.current_folder = folder_path

        # Find all image files
        self.image_files: list[Path] = []
        for ext in IMAGE_EXTENSIONS:
            self.image_files.extend(folder_path.glob(f"*{ext}"))
            self.image_files.extend(folder_path.glob(f"*{ext.upper()}"))

        self.image_files.sort()

        # Arrange thumbnails based on current widget size and zoom
        self._arrange_thumbnails()

        # Update which images are already in use
        self.update_used_images()

        self.update()

    def _arrange_thumbnails(self):
        """Arrange thumbnails in a grid based on widget width and zoom level."""
        if not hasattr(self, "image_files") or not self.image_files:
            self.thumbnails.clear()
            return

        # Calculate number of columns that fit
        widget_width = self.width()
        if widget_width <= 0:
            widget_width = 250  # Default minimum width

        # Thumbnail size in screen pixels (affected by zoom)
        thumb_size_screen = 100.0 * self.zoom_level
        spacing_screen = 10.0 * self.zoom_level

        # Calculate columns
        columns = max(1, int((widget_width - spacing_screen) / (thumb_size_screen + spacing_screen)))

        # Calculate total grid width to center it
        spacing = 10.0
        grid_width = columns * (100.0 + spacing) - spacing  # Total width in base units
        # Horizontal offset to center the grid
        h_offset = max(0, (widget_width / self.zoom_level - grid_width) / 2)

        # Build a map of existing thumbnails by path to reuse them
        existing_thumbs = {thumb.image_path: thumb for thumb in self.thumbnails}

        # Clear lists but reuse thumbnail objects
        self.thumbnails.clear()
        self.date_headers.clear()

        # For date mode: track current date and positioning
        current_date_str = None
        section_start_y = spacing
        row_in_section = 0
        col = 0

        for idx, image_file in enumerate(self.image_files):
            image_path = str(image_file)

            # Check if we need a date header (only in date sort mode)
            if self.sort_mode == "date" and self._get_image_date_func:
                from datetime import datetime

                timestamp = self._get_image_date_func(image_file)
                date_obj = datetime.fromtimestamp(timestamp)
                date_str = date_obj.strftime("%B %d, %Y")  # e.g., "December 13, 2025"

                if date_str != current_date_str:
                    # Starting a new date section
                    if current_date_str is not None:
                        # Not the first section - calculate where this section starts
                        # It should start after the last thumbnail of the previous section
                        if self.thumbnails:
                            last_thumb = self.thumbnails[-1]
                            # Start after the last row of previous section
                            last_row_y = last_thumb.y + last_thumb.thumbnail_size
                            section_start_y = last_row_y + spacing * 2  # Extra spacing between sections

                    # Add header at section start
                    header = DateHeader(date_str, section_start_y)
                    self.date_headers.append(header)

                    # Update section_start_y to after the header
                    section_start_y += header.height + spacing

                    current_date_str = date_str
                    row_in_section = 0
                    col = 0

            # Calculate position
            if self.sort_mode == "date":
                # In date mode: position relative to section start
                row = row_in_section
                thumb_y = section_start_y + row * (100.0 + spacing)
            else:
                # In other modes: simple grid based on overall index
                row = idx // columns
                thumb_y = row * (100.0 + spacing) + spacing

            # Calculate X position (always centered)
            thumb_x = h_offset + col * (100.0 + spacing) + spacing

            # Reuse existing thumbnail if available, otherwise create new
            if image_path in existing_thumbs:
                thumb = existing_thumbs[image_path]
                thumb.grid_row = row
                thumb.grid_col = col
                thumb.x = thumb_x
                thumb.y = thumb_y
            else:
                # Create new placeholder thumbnail
                thumb = ThumbnailItem(image_path, (row, col))
                thumb.x = thumb_x
                thumb.y = thumb_y
                # Request async load
                self._request_thumbnail_load(thumb)

            self.thumbnails.append(thumb)

            # Update column and row counters
            col += 1
            if col >= columns:
                col = 0
                row_in_section += 1

        # Update scrollbar range after arranging
        self._update_scrollbar_range()

    def _update_scrollbar_range(self):
        """Update scrollbar range based on content height."""
        if not self.scrollbar or self._updating_scrollbar:
            return

        if not self.thumbnails:
            self.scrollbar.setRange(0, 0)
            self.scrollbar.setPageStep(self.height())
            return

        # Calculate total content height
        if self.thumbnails:
            # Find the maximum Y position
            max_y = max(thumb.y + thumb.thumbnail_size for thumb in self.thumbnails)
            content_height = max_y * self.zoom_level
        else:
            content_height = 0

        # Visible height
        visible_height = self.height()

        # Scrollable range
        scroll_range = max(0, int(content_height - visible_height))

        self._updating_scrollbar = True
        self.scrollbar.setRange(0, scroll_range)
        self.scrollbar.setPageStep(visible_height)
        self.scrollbar.setSingleStep(int(visible_height / 10))  # 10% of visible height per step

        # Update scrollbar position based on current pan
        scroll_pos = int(-self.pan_offset[1])
        self.scrollbar.setValue(scroll_pos)
        self._updating_scrollbar = False

    def _on_scrollbar_changed(self, value):
        """Handle scrollbar value change."""
        if self._updating_scrollbar:
            return

        # Update pan offset based on scrollbar value
        self.pan_offset = (0, -value)
        self.update()

    def _update_scrollbar_position(self):
        """Update scrollbar position based on current pan offset."""
        if not self.scrollbar or self._updating_scrollbar:
            return

        self._updating_scrollbar = True
        scroll_pos = int(-self.pan_offset[1])
        self.scrollbar.setValue(scroll_pos)
        self._updating_scrollbar = False

    def update_used_images(self):
        """Update which thumbnails are already used in the project."""
        # Get reference to main window's project
        main_window = self.window()
        if not hasattr(main_window, "project") or not main_window.project:
            return

        project = main_window.project

        # Collect all image paths used in the project
        used_paths = set()
        for page in project.pages:
            from pyPhotoAlbum.models import ImageData

            for element in page.layout.elements:
                if isinstance(element, ImageData) and element.image_path:
                    # Resolve to absolute path for comparison
                    abs_path = element.resolve_image_path()
                    if abs_path:
                        used_paths.add(abs_path)

        # Mark thumbnails as used
        for thumb in self.thumbnails:
            thumb.is_used_in_project = thumb.image_path in used_paths

    def _request_thumbnail_load(self, thumb: ThumbnailItem):
        """Request async load for a thumbnail using main window's loader."""
        # Skip if already loading or loaded
        if thumb._async_loading or thumb._texture_id:
            return

        # Get main window's async loader
        main_window = self.window()
        if not main_window or not hasattr(main_window, "_gl_widget"):
            return

        gl_widget = main_window._gl_widget
        if not hasattr(gl_widget, "async_image_loader"):
            return

        from pyPhotoAlbum.async_backend import LoadPriority

        try:
            # Mark as loading to prevent duplicate requests
            thumb._async_loading = True

            # Request load through main window's async loader
            # Use LOW priority for thumbnails to not interfere with main canvas
            gl_widget.async_image_loader.request_load(
                Path(thumb.image_path),
                priority=LoadPriority.LOW,
                target_size=(200, 200),  # Small thumbnails
                user_data=thumb,
            )
        except RuntimeError:
            thumb._async_loading = False  # Reset on error

    def _on_image_loaded(self, path: Path, image, user_data):
        """Handle async image loaded - sets pending image on the placeholder."""
        if isinstance(user_data, ThumbnailItem):
            # Store the loaded image in the placeholder
            user_data._pending_pil_image = image
            user_data._img_width = image.width
            user_data._img_height = image.height
            user_data._async_loading = False

            # Schedule a repaint (will be batched if many images load quickly)
            self.update()

    def _on_image_load_failed(self, path: Path, error_msg: str, user_data):
        """Handle async image load failure."""
        pass  # Silently ignore load failures for thumbnails

    def screen_to_viewport(self, screen_x: int, screen_y: int) -> Tuple[float, float]:
        """Convert screen coordinates to viewport coordinates (accounting for zoom/pan)."""
        vp_x = (screen_x - self.pan_offset[0]) / self.zoom_level
        vp_y = (screen_y - self.pan_offset[1]) / self.zoom_level
        return vp_x, vp_y

    def get_thumbnail_at(self, screen_x: int, screen_y: int) -> Optional[ThumbnailItem]:
        """Get thumbnail at screen position."""
        vp_x, vp_y = self.screen_to_viewport(screen_x, screen_y)

        for thumb in self.thumbnails:
            if thumb.contains_point(vp_x, vp_y):
                return thumb

        return None

    def mousePressEvent(self, event):
        """Handle mouse press for drag."""
        if event.button() == Qt.MouseButton.LeftButton:
            self.drag_start_pos = event.pos()
            self.dragging_thumbnail = self.get_thumbnail_at(event.pos().x(), event.pos().y())

    def mouseMoveEvent(self, event):
        """Handle mouse move for drag or pan."""
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return

        if self.drag_start_pos is None:
            return

        # Check if we should start dragging a thumbnail
        if self.dragging_thumbnail:
            # Start drag operation
            drag = QDrag(self)
            mime_data = QMimeData()

            # Set file URL for the drag
            url = QUrl.fromLocalFile(self.dragging_thumbnail.image_path)
            mime_data.setUrls([url])

            drag.setMimeData(mime_data)

            # Execute drag (this blocks until drop or cancel)
            drag.exec(Qt.DropAction.CopyAction)

            # Reset drag state
            self.drag_start_pos = None
            self.dragging_thumbnail = None
        else:
            # Pan the view (right-click or middle-click drag)
            # Only allow vertical panning - grid is always horizontally centered
            delta = event.pos() - self.drag_start_pos
            self.pan_offset = (0, self.pan_offset[1] + delta.y())  # No horizontal pan - grid is centered
            self.drag_start_pos = event.pos()
            self._update_scrollbar_position()
            self.update()

    def mouseReleaseEvent(self, event):
        """Handle mouse release."""
        self.drag_start_pos = None
        self.dragging_thumbnail = None

    def wheelEvent(self, event):
        """Handle mouse wheel for scrolling (or zooming with Ctrl)."""
        delta = event.angleDelta().y()

        # Check if Ctrl is pressed for zooming
        if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
            # Zoom mode
            mouse_y = event.position().y()

            zoom_factor = 1.1 if delta > 0 else 0.9

            # Calculate vertical world position before zoom
            world_y = (mouse_y - self.pan_offset[1]) / self.zoom_level

            # Apply zoom
            old_zoom = self.zoom_level
            self.zoom_level *= zoom_factor
            self.zoom_level = max(0.1, min(5.0, self.zoom_level))  # Clamp

            # Rearrange thumbnails if zoom level changed significantly
            # This recalculates horizontal centering
            if abs(self.zoom_level - old_zoom) > 0.01:
                self._arrange_thumbnails()

            # Adjust vertical pan to keep mouse position fixed
            # Keep horizontal pan at 0 (grid is always horizontally centered)
            self.pan_offset = (
                0,  # No horizontal pan - grid is centered in _arrange_thumbnails
                mouse_y - world_y * self.zoom_level,
            )
        else:
            # Scroll mode - scroll vertically only
            scroll_amount = delta * 0.5  # Adjust sensitivity
            self.pan_offset = (0, self.pan_offset[1] + scroll_amount)  # No horizontal pan

        self._update_scrollbar_position()
        self.update()


class ThumbnailBrowserDock(QDockWidget):
    """
    Dockable widget containing the thumbnail browser.
    """

    def __init__(self, parent=None):
        super().__init__("Image Browser", parent)

        # Create main widget
        main_widget = QWidget()
        layout = QVBoxLayout(main_widget)
        layout.setContentsMargins(5, 5, 5, 5)
        layout.setSpacing(5)

        # Header with folder selection
        header_layout = QHBoxLayout()

        self.folder_label = QLabel("No folder selected")
        self.folder_label.setStyleSheet("font-weight: bold; padding: 5px;")
        header_layout.addWidget(self.folder_label)

        self.select_folder_btn = QPushButton("Select Folder...")
        self.select_folder_btn.clicked.connect(self._select_folder)
        header_layout.addWidget(self.select_folder_btn)

        layout.addLayout(header_layout)

        # Sort toolbar
        sort_layout = QHBoxLayout()
        sort_layout.setContentsMargins(5, 0, 5, 5)

        sort_label = QLabel("Sort by:")
        sort_layout.addWidget(sort_label)

        self.sort_name_btn = QPushButton("Name")
        self.sort_name_btn.setCheckable(True)
        self.sort_name_btn.setChecked(True)  # Default sort
        self.sort_name_btn.clicked.connect(lambda: self._sort_by("name"))
        sort_layout.addWidget(self.sort_name_btn)

        self.sort_date_btn = QPushButton("Date")
        self.sort_date_btn.setCheckable(True)
        self.sort_date_btn.clicked.connect(lambda: self._sort_by("date"))
        sort_layout.addWidget(self.sort_date_btn)

        self.sort_camera_btn = QPushButton("Camera")
        self.sort_camera_btn.setCheckable(True)
        self.sort_camera_btn.clicked.connect(lambda: self._sort_by("camera"))
        sort_layout.addWidget(self.sort_camera_btn)

        sort_layout.addStretch()

        layout.addLayout(sort_layout)

        # Track current sort mode
        self.current_sort = "name"

        # Create horizontal layout for GL widget and scrollbar
        browser_layout = QHBoxLayout()
        browser_layout.setContentsMargins(0, 0, 0, 0)
        browser_layout.setSpacing(0)

        # GL Widget for thumbnails
        self.gl_widget = ThumbnailGLWidget(main_window=parent)
        browser_layout.addWidget(self.gl_widget)

        # Vertical scrollbar
        self.scrollbar = QScrollBar(Qt.Orientation.Vertical)
        self.scrollbar.valueChanged.connect(self.gl_widget._on_scrollbar_changed)
        browser_layout.addWidget(self.scrollbar)

        # Connect scrollbar to GL widget
        self.gl_widget.scrollbar = self.scrollbar

        layout.addLayout(browser_layout)

        self.setWidget(main_widget)

        # Dock settings
        self.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
        self.setFeatures(
            QDockWidget.DockWidgetFeature.DockWidgetClosable
            | QDockWidget.DockWidgetFeature.DockWidgetMovable
            | QDockWidget.DockWidgetFeature.DockWidgetFloatable
        )

        # Connect to main window's async loader when shown
        self._connect_async_loader()

    def _connect_async_loader(self):
        """Connect to main window's async image loader."""
        main_window = self.window()
        if not hasattr(main_window, "_gl_widget"):
            return

        gl_widget = main_window._gl_widget
        if not hasattr(gl_widget, "async_image_loader"):
            return

        # Avoid duplicate connections
        if hasattr(self, "_async_connected") and self._async_connected:
            return

        try:
            # Connect signals
            gl_widget.async_image_loader.image_loaded.connect(self.gl_widget._on_image_loaded)
            gl_widget.async_image_loader.load_failed.connect(self.gl_widget._on_image_load_failed)
            self._async_connected = True
        except Exception:
            pass  # Silently handle connection errors

    def showEvent(self, event):
        """Handle show event."""
        super().showEvent(event)
        # Ensure async loader is connected when shown
        self._connect_async_loader()

    def _select_folder(self):
        """Open dialog to select folder."""
        folder_path = QFileDialog.getExistingDirectory(
            self,
            "Select Image Folder",
            str(self.gl_widget.current_folder) if self.gl_widget.current_folder else str(Path.home()),
            QFileDialog.Option.ShowDirsOnly,
        )

        if folder_path:
            self.load_folder(Path(folder_path))

    def load_folder(self, folder_path: Path):
        """Load thumbnails from folder."""
        self.folder_label.setText(f"Folder: {folder_path.name}")
        self.gl_widget.load_folder(folder_path)
        # Apply current sort after loading
        self._apply_sort()
        self.gl_widget._arrange_thumbnails()
        self.gl_widget.update_used_images()
        self.gl_widget.update()

    def _sort_by(self, sort_mode: str):
        """Sort thumbnails by the specified mode."""
        # Update button states (only one can be checked)
        self.sort_name_btn.setChecked(sort_mode == "name")
        self.sort_date_btn.setChecked(sort_mode == "date")
        self.sort_camera_btn.setChecked(sort_mode == "camera")

        self.current_sort = sort_mode

        # Re-sort the image files in the GL widget
        if hasattr(self.gl_widget, "image_files") and self.gl_widget.image_files:
            self._apply_sort()
            # Re-arrange thumbnails with new order
            self.gl_widget._arrange_thumbnails()
            self.gl_widget.update_used_images()
            self.gl_widget.update()

    def _apply_sort(self):
        """Apply current sort mode to image files."""
        if not hasattr(self.gl_widget, "image_files") or not self.gl_widget.image_files:
            return
        if self.current_sort == "name":
            # Sort by filename only (not full path)
            self.gl_widget.image_files.sort(key=lambda p: p.name.lower())
            # Clear date headers for non-date sorts
            self.gl_widget.date_headers.clear()
            # Reset sort mode in GL widget
            self.gl_widget.sort_mode = "name"
            self.gl_widget._get_image_date_func = None
        elif self.current_sort == "date":
            # Sort by file modification time (or EXIF date if available)
            self.gl_widget.image_files.sort(key=self._get_image_date)
            # Date headers will be created during _arrange_thumbnails
            self.gl_widget.sort_mode = "date"
            self.gl_widget._get_image_date_func = self._get_image_date
        elif self.current_sort == "camera":
            # Sort by camera model from EXIF
            self.gl_widget.image_files.sort(key=self._get_camera_model)
            # Clear date headers for non-date sorts
            self.gl_widget.date_headers.clear()
            # Reset sort mode in GL widget
            self.gl_widget.sort_mode = "camera"
            self.gl_widget._get_image_date_func = None

    def _get_image_date(self, image_path: Path) -> float:
        """Get image date from EXIF or file modification time."""
        try:
            from PIL import Image
            from PIL.ExifTags import TAGS

            with Image.open(image_path) as img:
                exif = img.getexif()
                if exif:
                    # Look for DateTimeOriginal (when photo was taken)
                    for tag_id, value in exif.items():
                        tag = TAGS.get(tag_id, tag_id)
                        if tag == "DateTimeOriginal":
                            # Convert EXIF date format "2023:12:13 14:30:00" to timestamp
                            from datetime import datetime

                            try:
                                dt = datetime.strptime(value, "%Y:%m:%d %H:%M:%S")
                                return dt.timestamp()
                            except:
                                pass
        except Exception:
            pass

        # Fallback to file modification time
        return image_path.stat().st_mtime

    def _get_camera_model(self, image_path: Path) -> str:
        """Get camera model from EXIF metadata."""
        try:
            from PIL import Image
            from PIL.ExifTags import TAGS

            with Image.open(image_path) as img:
                exif = img.getexif()
                if exif:
                    # Look for camera model
                    for tag_id, value in exif.items():
                        tag = TAGS.get(tag_id, tag_id)
                        if tag == "Model":
                            return str(value).strip()
        except Exception:
            pass

        # Fallback to filename if no EXIF data
        return image_path.name.lower()
