"""
User interface components for displaying Tarot cards.

This module provides a Tkinter-based image displayer for Tarot cards,
supporting multiple decks and automatic image resolution.
"""

import os
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, ttk
from typing import List, Optional

try:
    from PIL import Image, ImageDraw, ImageFont, ImageTk

    HAS_PILLOW = True
except ImportError:
    HAS_PILLOW = False

from tarot.card.image_loader import ImageDeckLoader
from tarot.deck import Card


class CardDisplay:
    """
    Displays Tarot cards using Tkinter and Pillow.
    """

    def __init__(self, deck_name: str = "default"):
        """
        Initialize the displayer with a specific deck.

        Args:
            deck_name: Name of the deck folder in src/tarot/deck/
        """
        self.deck_name = deck_name
        self.deck_path = self._resolve_deck_path(deck_name)
        self.loader: Optional[ImageDeckLoader] = None

        if self.deck_path.exists() and self.deck_path.is_dir():
            try:
                self.loader = ImageDeckLoader(str(self.deck_path))
            except Exception as e:
                print(f"Warning: Failed to initialize deck loader for '{deck_name}': {e}")
        else:
            print(f"Warning: Deck folder not found: {self.deck_path}")

    def _resolve_deck_path(self, deck_name: str) -> Path:
        """Resolve the path to the deck folder."""
        # src/tarot/ui.py -> src/tarot -> src/tarot/deck
        current_dir = Path(__file__).parent
        return current_dir / "deck" / deck_name

    def show_cards(self, cards: List[Card], title: str = "Tarot Spread"):
        """
        Display the given cards in a window using SpreadDisplay.

        Args:
            cards: List of Card objects to display.
            title: Window title.
        """
        if not HAS_PILLOW:
            print("Error: Pillow library is not installed. Cannot display images.")
            print("Please install it with: pip install Pillow")
            return

        # Import spread classes here to avoid circular imports if any
        from tarot.card.spread import DrawnCard, SpreadPosition, SpreadReading

        # Create a dummy spread class since Spread requires a valid name
        class SimpleSpread:
            def __init__(self, name, description):
                self.name = name
                self.description = description
                self.positions = []

        # Create positions and drawn cards
        drawn_cards = []
        positions = []

        for i, card in enumerate(cards, 1):
            # Create a generic position
            pos = SpreadPosition(number=i, name=f"Card {i}", meaning="Display Card")
            positions.append(pos)

            # Create drawn card
            drawn = DrawnCard(position=pos, card=card, is_reversed=False)
            drawn_cards.append(drawn)

        # Create a synthetic spread
        spread = SimpleSpread("Card List", title)
        spread.positions = positions

        # Create reading
        reading = SpreadReading(spread, drawn_cards)  # type: ignore

        # Use SpreadDisplay
        display = SpreadDisplay(reading, self.deck_name)
        display.root.title(f"{title} - {self.deck_name}")
        display.run()


class CubeDisplay:
    """
    Displays the Cube of Space with navigation.
    """

    NAVIGATION = {
        "North": {"Right": "East", "Left": "West", "Up": "Above", "Down": "Below"},
        "South": {"Right": "West", "Left": "East", "Up": "Above", "Down": "Below"},
        "East": {"Right": "South", "Left": "North", "Up": "Above", "Down": "Below"},
        "West": {"Right": "North", "Left": "South", "Up": "Above", "Down": "Below"},
        "Above": {"Right": "East", "Left": "West", "Up": "South", "Down": "North"},
        "Below": {"Right": "East", "Left": "West", "Up": "North", "Down": "South"},
    }
    # Zoom bounds used when UI is not initialized (headless tests)
    MIN_ZOOM = 0.5
    MAX_ZOOM = 3.0
    # Zoom bounds for the live UI (when canvas/root exist)
    UI_MIN_ZOOM = 0.1
    UI_MAX_ZOOM = 50.0

    def __init__(self, cube, deck_name: str = "default"):
        self.cube = cube
        self.deck_name = deck_name
        self.current_wall_name = "North"
        self.card_display = CardDisplay(deck_name)
        self.root = None
        self.content_frame = None
        self.zoom_level = 1.0

    def show(self):
        """Open the Cube display window."""
        if not HAS_PILLOW:
            print("Error: Pillow library is not installed. Cannot display images.")
            return

        self.root = tk.Tk()
        self.root.title("Cube of Space")

        # Bind arrow keys for navigation
        self.root.bind("<Up>", lambda e: self._navigate("Up"))
        self.root.bind("<Down>", lambda e: self._navigate("Down"))
        self.root.bind("<Left>", lambda e: self._navigate("Left"))
        self.root.bind("<Right>", lambda e: self._navigate("Right"))

        # Bind zoom keys (+ and -)
        self.root.bind("<plus>", lambda e: self._zoom(1.1))
        self.root.bind("<equal>", lambda e: self._zoom(1.1))  # Often same key as plus
        self.root.bind("<minus>", lambda e: self._zoom(0.9))
        self.root.bind("<underscore>", lambda e: self._zoom(0.9))  # Shift+minus
        self.root.bind("<KP_Add>", lambda e: self._zoom(1.1))  # Numpad +
        self.root.bind("<KP_Subtract>", lambda e: self._zoom(0.9))  # Numpad -

        # Bind WASD for panning
        self.root.bind("w", lambda e: self._pan_key("up"))
        self.root.bind("a", lambda e: self._pan_key("left"))
        self.root.bind("s", lambda e: self._pan_key("down"))
        self.root.bind("d", lambda e: self._pan_key("right"))
        self.root.bind("W", lambda e: self._pan_key("up"))
        self.root.bind("A", lambda e: self._pan_key("left"))
        self.root.bind("S", lambda e: self._pan_key("down"))
        self.root.bind("D", lambda e: self._pan_key("right"))

        # Main container (fills window)
        self.main_frame = ttk.Frame(self.root)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # Canvas for panning/zooming
        self.canvas = tk.Canvas(self.main_frame, bg="#f0f0f0")
        self.canvas.pack(fill=tk.BOTH, expand=True)

        # Panning bindings
        self.canvas.bind("<ButtonPress-1>", self._start_pan)
        self.canvas.bind("<B1-Motion>", self._pan)

        # Content Frame (inside canvas)
        self.content_frame = ttk.Frame(self.canvas)
        self.canvas_window = self.canvas.create_window(
            (0, 0), window=self.content_frame, anchor="center"
        )

        # Overlay Controls
        # Navigation Frame (Bottom Center)
        nav_frame = ttk.Frame(self.main_frame, relief="solid", borderwidth=1)
        nav_frame.place(relx=0.5, rely=0.95, anchor="s")

        # Zoom Frame (Top Right)
        zoom_frame = ttk.Frame(self.main_frame, relief="solid", borderwidth=1)
        zoom_frame.place(relx=0.95, rely=0.05, anchor="ne")

        # Populate Zoom Frame
        ttk.Label(zoom_frame, text="Zoom:").pack(side=tk.LEFT, padx=5)
        ttk.Button(zoom_frame, text="+", width=3, command=lambda: self._zoom(1.22)).pack(
            side=tk.LEFT
        )
        ttk.Button(zoom_frame, text="-", width=3, command=lambda: self._zoom(0.82)).pack(
            side=tk.LEFT
        )

        # Populate Navigation Frame
        dir_frame = ttk.Frame(nav_frame)
        dir_frame.pack(side=tk.TOP, padx=5, pady=5)

        ttk.Button(dir_frame, text="Up", command=lambda: self._navigate("Up")).pack(side=tk.TOP)

        mid_nav = ttk.Frame(dir_frame)
        mid_nav.pack(side=tk.TOP)
        ttk.Button(mid_nav, text="Left", command=lambda: self._navigate("Left")).pack(
            side=tk.LEFT, padx=5
        )
        ttk.Button(mid_nav, text="Right", command=lambda: self._navigate("Right")).pack(
            side=tk.LEFT, padx=5
        )

        ttk.Button(dir_frame, text="Down", command=lambda: self._navigate("Down")).pack(side=tk.TOP)

        # Initial render
        self._update_display()

        # Center window
        self.root.update_idletasks()
        width = 800
        height = 900
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()
        x = (screen_width // 2) - (width // 2)
        y = (screen_height // 2) - (height // 2)
        self.root.geometry(f"{width}x{height}+{x}+{y}")

        # Ensure window has focus for keyboard events
        self.root.focus_force()

        self.root.mainloop()

    def _start_pan(self, event):
        """Start panning operation."""
        self.canvas.scan_mark(event.x_root, event.y_root)

    def _pan(self, event):
        """Pan the canvas."""
        self.canvas.scan_dragto(event.x_root, event.y_root, gain=1)

    def _pan_key(self, direction):
        """Pan the canvas using keys."""
        if direction == "up":
            self.canvas.yview_scroll(-1, "units")
        elif direction == "down":
            self.canvas.yview_scroll(1, "units")
        elif direction == "left":
            self.canvas.xview_scroll(-1, "units")
        elif direction == "right":
            self.canvas.xview_scroll(1, "units")

    def _zoom(self, factor):
        """Adjust zoom level and redraw, keeping the view centered."""
        # If UI not initialized (no canvas), just update zoom level and return.
        if not getattr(self, "canvas", None):
            old_zoom = self.zoom_level
            self.zoom_level *= factor
            # Clamp zoom level to configured bounds
            self.zoom_level = max(self.MIN_ZOOM, min(self.zoom_level, self.MAX_ZOOM))
            # _update_display is safe and will no-op if content isn't initialized
            self._update_display()
            return

        # 1. Capture current state
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        # Center of viewport in canvas coordinates
        cx = self.canvas.canvasx(canvas_width / 2)
        cy = self.canvas.canvasy(canvas_height / 2)

        # Content position
        bbox = self.canvas.bbox("all")
        if not bbox:
            # Should not happen if initialized
            self.zoom_level *= factor
            # Clamp to UI bounds when canvas exists
            self.zoom_level = max(self.UI_MIN_ZOOM, min(self.zoom_level, self.UI_MAX_ZOOM))
            self._update_display()
            return

        content_left = bbox[0]
        content_top = bbox[1]

        # Point relative to content
        rel_x = cx - content_left
        rel_y = cy - content_top

        # 2. Update zoom level
        old_zoom = self.zoom_level
        self.zoom_level *= factor
        # Clamp zoom level (UI uses broader bounds when initialized)
        if getattr(self, "canvas", None):
            min_z, max_z = self.UI_MIN_ZOOM, self.UI_MAX_ZOOM
        else:
            min_z, max_z = self.MIN_ZOOM, self.MAX_ZOOM
        self.zoom_level = max(min_z, min(self.zoom_level, max_z))

        # Calculate effective factor in case of clamping
        effective_factor = self.zoom_level / old_zoom if old_zoom > 0 else factor

        # 3. Update display
        self._update_display()

        # 4. Restore position
        # New content position
        new_bbox = self.canvas.bbox("all")
        if not new_bbox:
            return

        new_content_left = new_bbox[0]
        new_content_top = new_bbox[1]
        scroll_width = new_bbox[2] - new_bbox[0]
        scroll_height = new_bbox[3] - new_bbox[1]

        # Target point in new content
        new_rel_x = rel_x * effective_factor
        new_rel_y = rel_y * effective_factor

        # Target canvas coordinate for center
        target_cx = new_content_left + new_rel_x
        target_cy = new_content_top + new_rel_y

        # We want target_cx to be at screen center (canvas_width/2)
        # So left of view should be:
        view_left = target_cx - (canvas_width / 2)
        view_top = target_cy - (canvas_height / 2)

        # Apply scroll
        if scroll_width > canvas_width:
            frac_x = (view_left - new_bbox[0]) / scroll_width
            self.canvas.xview_moveto(frac_x)

        if scroll_height > canvas_height:
            frac_y = (view_top - new_bbox[1]) / scroll_height
            self.canvas.yview_moveto(frac_y)

    def _navigate(self, direction: str):
        """Navigate to the next wall based on direction."""
        next_wall = self.NAVIGATION.get(self.current_wall_name, {}).get(direction)
        if next_wall:
            self.current_wall_name = next_wall
            self._update_display()

    def _update_display(self):
        """Render the current wall."""
        if self.content_frame is None:
            return

        # Clear content frame
        for widget in self.content_frame.winfo_children():
            widget.destroy()

        # Title
        ttk.Label(
            self.content_frame,
            text=f"Wall: {self.current_wall_name}",
            font=("Helvetica", 16, "bold"),
        ).pack(pady=(0, 20))

        # Grid for directions
        grid_frame = ttk.Frame(self.content_frame)
        grid_frame.pack()

        wall = self.cube.wall(self.current_wall_name)

        # Map directions to grid positions (row, col)
        # 3x3 Grid
        layout = {
            "North": (0, 1),
            "West": (1, 0),
            "Center": (1, 1),
            "East": (1, 2),
            "South": (2, 1),
        }

        # Calculate sizes based on zoom
        cell_width = int(200 * self.zoom_level)
        cell_height = int(250 * self.zoom_level)
        img_height = int(200 * self.zoom_level)

        # Keep images alive
        self.root.images = []

        for dir_name, (row, col) in layout.items():
            direction = wall.direction(dir_name)

            cell_frame = ttk.Frame(
                grid_frame, borderwidth=1, relief="solid", width=cell_width, height=cell_height
            )
            cell_frame.grid(row=row, column=col, padx=5, pady=5)
            cell_frame.grid_propagate(False)

            if direction:
                card = self._find_card_for_direction(direction)

                if card:
                    # Try to load image
                    img_path = None
                    if self.card_display.loader:
                        img_path = self.card_display.loader.get_image_path(card)
                    if not img_path and card.image_path:
                        img_path = card.image_path

                    if img_path and os.path.exists(img_path):
                        try:
                            pil_img = Image.open(img_path)
                            # Resize for grid
                            base_height = img_height
                            h_percent = base_height / float(pil_img.size[1])
                            w_size = int((float(pil_img.size[0]) * float(h_percent)))
                            pil_img = pil_img.resize(
                                (w_size, base_height), Image.Resampling.LANCZOS
                            )

                            tk_img = ImageTk.PhotoImage(pil_img)
                            self.root.images.append(tk_img)

                            lbl = tk.Label(cell_frame, image=tk_img, bg="black")
                            lbl.place(relx=0.5, rely=0.5, anchor="center")
                        except Exception:
                            self._render_text_fallback(cell_frame, direction, card.name)
                    else:
                        self._render_text_fallback(cell_frame, direction, card.name)
                else:
                    self._render_text_fallback(cell_frame, direction)
            else:
                ttk.Label(cell_frame, text="Empty").place(relx=0.5, rely=0.5, anchor="center")

        # Update canvas scrollregion
        self.content_frame.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox("all"))

        # Center content initially if it fits
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        content_width = self.content_frame.winfo_reqwidth()
        content_height = self.content_frame.winfo_reqheight()

        if canvas_width > content_width and canvas_height > content_height:
            self.canvas.coords(self.canvas_window, canvas_width / 2, canvas_height / 2)
        else:
            # Reset to top-left or center of scroll region
            self.canvas.coords(self.canvas_window, content_width / 2, content_height / 2)

        # Bind panning events to all content widgets
        self._bind_recursive(self.content_frame)

    def _bind_recursive(self, widget):
        """Recursively bind panning events to widget and its children."""
        self._bind_pan_events(widget)
        for child in widget.winfo_children():
            self._bind_recursive(child)

    def _bind_pan_events(self, widget):
        """Bind panning events to a widget."""
        widget.bind("<ButtonPress-1>", self._start_pan)
        widget.bind("<B1-Motion>", self._pan)

    def _render_text_fallback(self, parent, direction, card_name=None):
        text = f"{direction.name}\n{direction.letter}"
        if card_name:
            text += f"\n({card_name})"
        ttk.Label(parent, text=text, justify="center").place(relx=0.5, rely=0.5, anchor="center")

    def _find_card_for_direction(self, direction) -> Optional[Card]:
        """Find the Tarot card associated with a wall direction."""
        from tarot.tarot_api import Tarot

        letter_name = direction.letter
        if not letter_name:
            return None

        # Find path with this letter
        paths = Tarot.tree.path()
        target_path = None
        for p in paths.values():
            if p.hebrew_letter.lower() == letter_name.lower():
                target_path = p
                break

        if target_path and target_path.tarot_trump:
            # Find card by name (fuzzy match)
            trump_name = target_path.tarot_trump.lower()

            for card in Tarot.deck.card.filter():
                c_name = card.name.lower()
                # Check if card name is contained in trump name (e.g. "fool" in "0 - the fool")
                # Or if trump name is contained in card name
                if c_name in trump_name or trump_name in c_name:
                    return card

        return None


def display_cards(cards: List[Card], deck_name: str = "default"):
    """
    Convenience function to display cards.

    Args:
        cards: List of Card objects to display
        deck_name: Name of the deck to use (folder name in src/tarot/deck/)
    """
    display = CardDisplay(deck_name)
    display.show_cards(cards)


def display_cube(cube=None, deck_name: str = "default"):
    """
    Display the Cube of Space with interactive navigation.

    Args:
        cube: Cube object (optional, defaults to Tarot.cube)
        deck_name: Name of the deck to use
    """
    if cube is None:
        from tarot.tarot_api import Tarot

        cube = Tarot.cube

    display = CubeDisplay(cube, deck_name)
    display.show()


class SpreadDisplay:
    """
    Displays a Tarot spread visually using Tkinter and Pillow.
    """

    # Layout definitions: {spread_name: {position_number: {'pos': (x, y), 'rotate': degrees}}}
    # Coordinates are relative grid units (approx card width/height)
    # Using 1.02 spacing for tight layout
    LAYOUTS = {
        "Celtic Cross": {
            1: {"pos": (0, 0)},
            2: {"pos": (0, 0), "rotate": 90, "z": 10},  # Top layer
            3: {"pos": (0, -1.02)},
            4: {"pos": (0, 1.02)},
            5: {"pos": (-1.02, 0)},
            6: {"pos": (1.02, 0)},
            7: {"pos": (2.1, 1.53)},  # 1.5 * 1.02
            8: {"pos": (2.1, 0.51)},  # 0.5 * 1.02
            9: {"pos": (2.1, -0.51)},
            10: {"pos": (2.1, -1.53)},
        },
        "3-Card Spread": {1: {"pos": (-1.02, 0)}, 2: {"pos": (0, 0)}, 3: {"pos": (1.02, 0)}},
        "Golden Dawn 3-Card": {
            1: {"pos": (0, -1.02)},
            2: {"pos": (-1.02, 0.8)},
            3: {"pos": (1.02, 0.8)},
        },
        "Horseshoe": {
            1: {"pos": (-3.06, 1.02)},
            2: {"pos": (-2.04, 0)},
            3: {"pos": (-1.02, -0.51)},
            4: {"pos": (0, -1.02)},
            5: {"pos": (1.02, -0.51)},
            6: {"pos": (2.04, 0)},
            7: {"pos": (3.06, 1.02)},
        },
        "Pentagram": {
            1: {"pos": (0, -1.5)},  # Spirit (Top)
            2: {"pos": (1.5, -0.4)},  # Fire (Right Top)
            3: {"pos": (1.0, 1.5)},  # Water (Right Bottom)
            4: {"pos": (-1.0, 1.5)},  # Air (Left Bottom)
            5: {"pos": (-1.5, -0.4)},  # Earth (Left Top)
        },
        "Relationship": {
            1: {"pos": (-1.5, 0)},  # You
            2: {"pos": (1.5, 0)},  # Them
            3: {"pos": (0, -1.02)},  # Relationship (Center Top)
            4: {"pos": (0, 0.5)},  # Challenge (Center Bottom)
            5: {"pos": (0, 2.0)},  # Outcome (Bottom)
        },
        "Yes or No": {1: {"pos": (0, 0)}},
    }

    def __init__(self, reading, deck_name="default"):
        self.reading = reading
        self.root = tk.Tk()
        self.root.title(f"Tarot Spread: {reading.spread.name}")
        self.root.geometry("1200x900")

        self.zoom_level = 1.0
        self.show_text = True
        self.show_top_card = True
        self.drag_data = {"x": 0, "y": 0}
        self._tk_images = []  # Keep references

        # Setup UI
        self._setup_ui()

        # Load images
        current_dir = Path(__file__).parent
        deck_path = current_dir / "deck" / deck_name

        self.image_loader = None
        if deck_path.exists() and deck_path.is_dir():
            try:
                self.image_loader = ImageDeckLoader(str(deck_path))
            except Exception as e:
                print(f"Warning: Failed to initialize deck loader: {e}")

        # Initial draw
        self._draw_spread()

        # Center view
        self._center_view()

    def _setup_ui(self):
        # Toolbar
        toolbar = ttk.Frame(self.root)
        toolbar.pack(side=tk.TOP, fill=tk.X)

        ttk.Button(toolbar, text="Zoom In (+)", command=lambda: self._zoom(1.2)).pack(
            side=tk.LEFT, padx=2
        )
        ttk.Button(toolbar, text="Zoom Out (-)", command=lambda: self._zoom(0.8)).pack(
            side=tk.LEFT, padx=2
        )
        ttk.Button(toolbar, text="Reset View", command=self._reset_view).pack(side=tk.LEFT, padx=2)
        ttk.Button(toolbar, text="Toggle Text", command=self._toggle_text).pack(
            side=tk.LEFT, padx=2
        )
        ttk.Button(toolbar, text="Export PNG", command=self._export_image).pack(
            side=tk.LEFT, padx=2
        )

        # Only show toggle top card if relevant
        if self.reading.spread.name == "Celtic Cross":
            ttk.Button(toolbar, text="Toggle Cross", command=self._toggle_top_card).pack(
                side=tk.LEFT, padx=2
            )

        # Canvas
        self.canvas = tk.Canvas(self.root, bg="#2c3e50")
        self.canvas.pack(fill=tk.BOTH, expand=True)

        # Bindings
        self.canvas.bind("<ButtonPress-1>", self._on_drag_start)
        self.canvas.bind("<B1-Motion>", self._on_drag_motion)
        self.canvas.bind("<MouseWheel>", self._on_mousewheel)
        # Linux scroll
        self.canvas.bind("<Button-4>", lambda e: self._zoom(1.1))
        self.canvas.bind("<Button-5>", lambda e: self._zoom(0.9))

    def _draw_spread(self):
        self.canvas.delete("all")
        self._tk_images.clear()

        layout = self.LAYOUTS.get(self.reading.spread.name)
        if not layout:
            # Fallback to grid if layout not defined
            self._draw_grid_fallback()
            return

        # Base dimensions
        card_width = 100 * self.zoom_level
        card_height = 150 * self.zoom_level

        # Spacing units
        unit_x = card_width
        unit_y = card_height

        # Center of virtual space (arbitrary large number to allow scrolling)
        cx, cy = 2000, 2000

        min_x, min_y = float("inf"), float("inf")
        max_x, max_y = float("-inf"), float("-inf")

        # Sort cards by z-index (default 0)
        cards_to_draw = []
        for drawn in self.reading.drawn_cards:
            pos_data = layout.get(drawn.position.number)
            if not pos_data:
                continue

            z_index = pos_data.get("z", 0)

            # Skip if top card is hidden
            if z_index > 0 and not self.show_top_card:
                continue

            cards_to_draw.append((drawn, pos_data, z_index))

        # Sort by z-index ascending
        cards_to_draw.sort(key=lambda x: x[2])

        for drawn, pos_data, _ in cards_to_draw:
            rel_x, rel_y = pos_data["pos"]
            rotation = pos_data.get("rotate", 0)

            # Calculate position
            x = cx + (rel_x * unit_x)
            y = cy + (rel_y * unit_y)

            # Update bounds
            # x, y is center
            half_w = card_width / 2
            half_h = card_height / 2

            # If rotated 90 deg, width and height swap for bounding box
            if abs(rotation % 180) == 90:
                half_w, half_h = half_h, half_w

            min_x = min(min_x, x - half_w)
            max_x = max(max_x, x + half_w)
            min_y = min(min_y, y - half_h)
            max_y = max(max_y, y + half_h)

            # Draw card
            self._draw_card(drawn, x, y, card_width, card_height, rotation)

        # Set scroll region
        padding = 50 * self.zoom_level
        self.canvas.configure(
            scrollregion=(min_x - padding, min_y - padding, max_x + padding, max_y + padding)
        )

    def _draw_card(self, drawn, x, y, w, h, layout_rotation):
        card_name = drawn.card.name

        # Get image
        pil_image = None
        if self.image_loader:
            img_path = self.image_loader.get_image_path(drawn.card)
            if img_path and os.path.exists(img_path):
                try:
                    pil_image = Image.open(img_path)
                except Exception as e:
                    print(f"Error loading image for {card_name}: {e}")

        if not pil_image:
            # Draw placeholder
            self.canvas.create_rectangle(
                x - w / 2, y - h / 2, x + w / 2, y + h / 2, fill="white", outline="black"
            )
            self.canvas.create_text(x, y, text=card_name, width=w - 10)
        else:
            # Total rotation
            rotation = layout_rotation
            if drawn.is_reversed:
                rotation += 180

            # Resize
            # Note: We resize to w, h BEFORE rotation for consistency with layout
            pil_image = pil_image.resize((int(w), int(h)), Image.Resampling.LANCZOS)

            # Rotate
            if rotation % 360 != 0:
                pil_image = pil_image.rotate(rotation, expand=True)

            # Convert to ImageTk
            tk_image = ImageTk.PhotoImage(pil_image)
            self._tk_images.append(tk_image)

            self.canvas.create_image(x, y, image=tk_image)

        if not self.show_text:
            return

        # Text Overlay
        # Calculate visual dimensions
        is_vertical = layout_rotation % 180 == 0
        vis_w = w if is_vertical else h
        vis_h = h if is_vertical else w

        # Font setup
        font_size = max(8, int(10 * self.zoom_level))

        # Background rectangle for text (bottom of card)
        # Height needed: approx 3 lines of text
        text_h = font_size * 3.5

        bg_x1 = x - vis_w / 2
        bg_y1 = y + vis_h / 2 - text_h
        bg_x2 = x + vis_w / 2
        bg_y2 = y + vis_h / 2

        # Draw semi-transparent-ish background (stipple works on some platforms, otherwise solid)
        self.canvas.create_rectangle(
            bg_x1, bg_y1, bg_x2, bg_y2, fill="#000000", stipple="gray75", outline=""
        )

        # Position Name
        self.canvas.create_text(
            x,
            bg_y1 + font_size,
            text=f"{drawn.position.number}. {drawn.position.name}",
            fill="white",
            font=("Arial", font_size, "bold"),
            width=vis_w - 4,
            justify="center",
        )

        # Meaning (shortened)
        self.canvas.create_text(
            x,
            bg_y1 + font_size * 2.2,
            text=drawn.position.meaning,
            fill="#ecf0f1",
            font=("Arial", int(font_size * 0.8)),
            width=vis_w - 4,
            justify="center",
        )

    def _draw_grid_fallback(self):
        # Simple grid layout if no specific layout defined
        cx, cy = 2000, 2000
        card_width = 100 * self.zoom_level
        card_height = 150 * self.zoom_level
        padding = 20 * self.zoom_level

        min_x, min_y = float("inf"), float("inf")
        max_x, max_y = float("-inf"), float("-inf")

        cols = 5
        for i, drawn in enumerate(self.reading.drawn_cards):
            row = i // cols
            col = i % cols

            x = cx + (col - cols / 2) * (card_width + padding)
            y = cy + (row * 1.5) * (card_height + padding)

            self._draw_card(drawn, x, y, card_width, card_height, 0)

            # Update bounds (x,y is center)
            half_w = card_width / 2
            half_h = card_height / 2

            min_x = min(min_x, x - half_w)
            max_x = max(max_x, x + half_w)
            min_y = min(min_y, y - half_h)
            max_y = max(max_y, y + half_h)

        if min_x == float("inf"):  # No cards
            min_x, min_y, max_x, max_y = cx, cy, cx, cy

        scroll_padding = 50 * self.zoom_level
        self.canvas.configure(
            scrollregion=(
                min_x - scroll_padding,
                min_y - scroll_padding,
                max_x + scroll_padding,
                max_y + scroll_padding,
            )
        )

    def _zoom(self, factor):
        self.zoom_level *= factor
        self.zoom_level = max(0.1, min(self.zoom_level, 5.0))
        self._draw_spread()

    def _toggle_text(self):
        self.show_text = not self.show_text
        self._draw_spread()

    def _toggle_top_card(self):
        self.show_top_card = not self.show_top_card
        self._draw_spread()

    def _reset_view(self):
        self.zoom_level = 1.0
        self._draw_spread()
        self._center_view()

    def _export_image(self):
        if not HAS_PILLOW:
            print("Pillow library is not installed. Cannot export images.")
            return

        region = self.canvas.bbox("all")
        if not region:
            print("Nothing to export.")
            return

        x1, y1, x2, y2 = region
        padding = 10
        x1 -= padding
        y1 -= padding
        x2 += padding
        y2 += padding

        try:
            default_name = f"{self.reading.spread.name.replace(' ', '_').lower()}_spread.png"
            file_path = filedialog.asksaveasfilename(
                defaultextension=".png",
                initialfile=default_name,
                filetypes=[("PNG Image", "*.png"), ("All Files", "*.*")],
                title="Save spread as...",
            )
            if not file_path:
                return

            img = self._render_spread_to_image(padding=padding)
            img.save(file_path, "PNG")
            print(f"Exported spread to {file_path}")
        except Exception as exc:
            print(f"Failed to export spread: {exc}")

    def _center_view(self):
        # Center the scroll region in the window
        # This is a bit tricky in Tkinter without knowing window size,
        # but we can try to scroll to the center of our virtual space (2000, 2000)
        # We'll just scroll to the middle of the scrollregion
        self.root.update_idletasks()
        region = self.canvas.bbox("all")
        if region:
            # Scroll to center of content
            # This requires math with xview_moveto which takes a fraction
            # For simplicity, we'll just leave it or try to center roughly
            pass

    def _on_drag_start(self, event):
        self.canvas.scan_mark(event.x, event.y)

    def _on_drag_motion(self, event):
        self.canvas.scan_dragto(event.x, event.y, gain=1)

    def _on_mousewheel(self, event):
        if event.delta > 0:
            self._zoom(1.1)
        else:
            self._zoom(0.9)

    def _render_spread_to_image(self, padding: int = 40) -> Image.Image:
        """Render the current spread to a PIL Image (no screen capture)."""

        layout = self.LAYOUTS.get(self.reading.spread.name)
        if not layout:
            layout = {i + 1: {"pos": (i * 1.2, 0)} for i in range(len(self.reading.drawn_cards))}

        # Use 4x resolution multiplier for high-quality export
        card_width = int(400 * self.zoom_level)
        card_height = int(600 * self.zoom_level)

        unit_x = card_width
        unit_y = card_height

        positions = []
        min_x, min_y = float("inf"), float("inf")
        max_x, max_y = float("-inf"), float("-inf")

        cards_to_draw = []
        for drawn in self.reading.drawn_cards:
            pos_data = layout.get(drawn.position.number)
            if not pos_data:
                continue
            z_index = pos_data.get("z", 0)
            if z_index > 0 and not self.show_top_card:
                continue
            cards_to_draw.append((drawn, pos_data, z_index))

        cards_to_draw.sort(key=lambda x: x[2])

        for drawn, pos_data, _ in cards_to_draw:
            rel_x, rel_y = pos_data["pos"]
            rotation = pos_data.get("rotate", 0)
            x = rel_x * unit_x
            y = rel_y * unit_y

            half_w = card_width / 2
            half_h = card_height / 2
            if abs(rotation % 180) == 90:
                half_w, half_h = half_h, half_w

            min_x = min(min_x, x - half_w)
            max_x = max(max_x, x + half_w)
            min_y = min(min_y, y - half_h)
            max_y = max(max_y, y + half_h)
            positions.append((drawn, x, y, rotation))

        if min_x == float("inf"):
            min_x = min_y = 0
            max_x = max_y = 0

        width = int((max_x - min_x) + padding * 2)
        height = int((max_y - min_y) + padding * 2)

        bg_color = (44, 62, 80)
        img = Image.new("RGB", (width, height), bg_color)
        draw = ImageDraw.Draw(img)
        font = ImageFont.load_default()

        cx = -min_x + padding
        cy = -min_y + padding

        for drawn, rel_x, rel_y, rotation in positions:
            x = cx + rel_x
            y = cy + rel_y
            pil_image = None
            if self.image_loader:
                img_path = self.image_loader.get_image_path(drawn.card)
                if img_path and os.path.exists(img_path):
                    try:
                        pil_image = Image.open(img_path)
                        # Convert to RGB immediately to avoid transparency issues
                        if pil_image.mode in ("RGBA", "PA"):
                            # Create white background for transparency
                            background = Image.new("RGB", pil_image.size, "white")
                            background.paste(
                                pil_image,
                                mask=pil_image.split()[-1] if pil_image.mode == "RGBA" else None,
                            )
                            pil_image = background
                        elif pil_image.mode != "RGB":
                            pil_image = pil_image.convert("RGB")
                    except Exception:
                        pil_image = None
            if pil_image is None:
                pil_image = Image.new("RGB", (card_width, card_height), "white")
                ph_draw = ImageDraw.Draw(pil_image)
                ph_draw.rectangle([0, 0, card_width - 1, card_height - 1], outline="black", width=2)
                ph_draw.text((4, 4), drawn.card.name, fill="black", font=font)
            else:
                pil_image = pil_image.resize((card_width, card_height), Image.Resampling.LANCZOS)

            if drawn.is_reversed:
                rotation += 180
            if rotation % 360 != 0:
                # Always use RGB mode before rotation to avoid mask issues
                if pil_image.mode != "RGB":
                    pil_image = pil_image.convert("RGB")
                pil_image = pil_image.rotate(rotation, expand=True, fillcolor="white")

            # Final RGB conversion before paste
            if pil_image.mode != "RGB":
                pil_image = pil_image.convert("RGB")

            pw, ph = pil_image.size
            try:
                img.paste(pil_image, (int(x - pw / 2), int(y - ph / 2)))
            except Exception as e:
                print(f"  Debug: Paste failed for {drawn.card.name} at ({x}, {y}): {e}")
                print(f"    Image mode: {pil_image.mode}, size: {pil_image.size}")
                print(
                    f"    Canvas size: {img.size}, position: ({int(x - pw / 2)}, {int(y - ph / 2)})"
                )

            if self.show_text:
                text_font = font
                label = f"{drawn.position.number}. {drawn.position.name}"
                meaning = drawn.position.meaning
                text_h = text_font.getbbox("Ag")[3] - text_font.getbbox("Ag")[1]
                block_h = int(text_h * 3.2)
                bx1 = int(x - card_width / 2)
                by1 = int(y + card_height / 2 - block_h)
                bx2 = int(x + card_width / 2)
                by2 = int(y + card_height / 2)
                draw.rectangle([bx1, by1, bx2, by2], fill=(0, 0, 0, 180))
                draw.text((bx1 + 4, by1 + 4), label, fill="white", font=text_font)
                draw.text(
                    (bx1 + 4, by1 + 4 + text_h * 1.4), meaning, fill="#ecf0f1", font=text_font
                )

        return img.convert("RGB")

    def run(self):
        self.root.mainloop()


def display_spread(reading, deck_name="default"):
    """
    Opens a window displaying the given spread reading.

    Args:
        reading: A SpreadReading object returned by Tarot.deck.card.spread()
        deck_name: Name of the deck to use (default: "default")
    """
    if not HAS_PILLOW:
        print("Pillow library not found. Cannot display graphical spread.")
        print(reading)
        return

    display = SpreadDisplay(reading, deck_name)
    display.run()
