import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import random
import string
from reportlab.lib.pagesizes import letter as LETTER_SIZE
from reportlab.pdfgen import canvas as pdf_canvas
from reportlab.lib.units import inch


class WordSearchGenerator:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.grid = [['' for _ in range(width)] for _ in range(height)]
        self.placed_words = []

    def place_word(self, word):
        """Try to place a word in the grid in a random direction."""
        word = word.upper()
        directions = [
            (0, 1),   # horizontal right
            (1, 0),   # vertical down
            (1, 1),   # diagonal down-right
            (0, -1),  # horizontal left
            (-1, 0),  # vertical up
            (-1, -1), # diagonal up-left
            (1, -1),  # diagonal down-left
            (-1, 1),  # diagonal up-right
        ]

        random.shuffle(directions)

        for _ in range(100):  # Try 100 random positions
            row = random.randint(0, self.height - 1)
            col = random.randint(0, self.width - 1)

            for dr, dc in directions:
                if self._can_place_word(word, row, col, dr, dc):
                    self._place_word_at(word, row, col, dr, dc)
                    return True

        return False

    def _can_place_word(self, word, row, col, dr, dc):
        """Check if a word can be placed at the given position and direction."""
        positions = []
        for i, char in enumerate(word):
            r = row + i * dr
            c = col + i * dc

            # Check bounds
            if r < 0 or r >= self.height or c < 0 or c >= self.width:
                return False

            # Check if cell is empty or has the same letter
            if self.grid[r][c] != '' and self.grid[r][c] != char:
                return False

            positions.append((r, c))

        # Check if this exact word already exists at these positions
        for placed_word in self.placed_words:
            if set(placed_word['positions']) == set(positions):
                return False

        return True

    def _place_word_at(self, word, row, col, dr, dc):
        """Place a word at the given position and direction."""
        positions = []
        for i, char in enumerate(word):
            r = row + i * dr
            c = col + i * dc
            self.grid[r][c] = char
            positions.append((r, c))

        self.placed_words.append({
            'word': word,
            'positions': positions,
            'start': (row, col),
            'direction': (dr, dc)
        })

    def fill_empty_cells(self):
        """Fill empty cells with random letters."""
        for r in range(self.height):
            for c in range(self.width):
                if self.grid[r][c] == '':
                    self.grid[r][c] = random.choice(string.ascii_uppercase)

    def get_grid_string(self):
        """Return the grid as a formatted string."""
        return '\n'.join([' '.join(row) for row in self.grid])

    def get_word_list_string(self):
        """Return the list of successfully placed words."""
        return '\n'.join([word['word'] for word in self.placed_words])


class WordSearchApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Word Search Generator & Player")
        self.root.geometry("1000x700")

        # Game state
        self.word_search = None
        self.grid_canvas = None
        self.cell_size = 30
        self.cell_rects = {}  # (row, col) -> canvas rectangle id
        self.cell_texts = {}  # (row, col) -> canvas text id
        self.selected_cells = []
        self.found_words = set()
        self.word_labels = {}
        self.is_dragging = False

        # Create main frame
        main_frame = ttk.Frame(root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Configure grid weights
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=1)

        # Input section
        input_frame = ttk.LabelFrame(main_frame, text="Input", padding="10")
        input_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

        # Dimensions
        ttk.Label(input_frame, text="Width (X):").grid(row=0, column=0, sticky=tk.W)
        self.width_var = tk.StringVar(value="12")
        ttk.Entry(input_frame, textvariable=self.width_var, width=10).grid(row=0, column=1, sticky=tk.W, padx=(5, 20))

        ttk.Label(input_frame, text="Height (Y):").grid(row=0, column=2, sticky=tk.W)
        self.height_var = tk.StringVar(value="12")
        ttk.Entry(input_frame, textvariable=self.height_var, width=10).grid(row=0, column=3, sticky=tk.W, padx=(5, 20))

        # Words input
        ttk.Label(input_frame, text="Words (one per line):").grid(row=1, column=0, columnspan=4, sticky=tk.W, pady=(10, 5))
        self.words_text = scrolledtext.ScrolledText(input_frame, width=40, height=6)
        self.words_text.grid(row=2, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10))
        self.words_text.insert('1.0', 'PYTHON\nGUI\nWORDSEARCH\nGAME\nPUZZLE\nFUN')

        # Buttons
        button_frame = ttk.Frame(input_frame)
        button_frame.grid(row=3, column=0, columnspan=2, pady=10, sticky=tk.W)
        ttk.Button(button_frame, text="Generate & Play", command=self.generate_word_search).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Clear Selection", command=self.clear_selection).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Export to PDF", command=self.export_to_pdf).pack(side=tk.LEFT, padx=5)

        # Rules section - place to the right of buttons
        rules_frame = ttk.LabelFrame(input_frame, text="How to Play", padding="5")
        rules_frame.grid(row=3, column=2, columnspan=2, rowspan=1, sticky=(tk.W, tk.E, tk.N), padx=(20, 0))
        rules_text = (
            "• Words: horizontal, vertical, diagonal\n"
            "• Can go forwards or backwards\n"
            "• Click and drag to select • Green = found!"
        )
        ttk.Label(rules_frame, text=rules_text, justify=tk.LEFT, font=('TkDefaultFont', 9)).pack(anchor=tk.W)

        # Game section (grid on left, word list on right)
        game_frame = ttk.LabelFrame(main_frame, text="Play Word Search", padding="10")
        game_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        game_frame.columnconfigure(0, weight=3)
        game_frame.columnconfigure(1, weight=1)
        game_frame.rowconfigure(0, weight=1)

        # Grid frame
        self.grid_frame = ttk.Frame(game_frame)
        self.grid_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))

        # Word list frame
        word_list_frame = ttk.LabelFrame(game_frame, text="Words to Find", padding="10")
        word_list_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
        word_list_frame.rowconfigure(0, weight=1)
        word_list_frame.columnconfigure(0, weight=1)

        # Scrollable word list
        self.word_list_canvas = tk.Canvas(word_list_frame, width=150)
        scrollbar = ttk.Scrollbar(word_list_frame, orient="vertical", command=self.word_list_canvas.yview)
        self.word_list_inner = ttk.Frame(self.word_list_canvas)

        self.word_list_canvas.configure(yscrollcommand=scrollbar.set)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.word_list_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.word_list_canvas.create_window((0, 0), window=self.word_list_inner, anchor="nw")

        self.word_list_inner.bind("<Configure>", lambda e: self.word_list_canvas.configure(scrollregion=self.word_list_canvas.bbox("all")))

    def clear_selection(self):
        """Clear the current selection."""
        if not self.grid_canvas:
            return
        for row, col in self.selected_cells:
            if (row, col) not in self._get_all_found_positions():
                rect_id = self.cell_rects.get((row, col))
                if rect_id:
                    self.grid_canvas.itemconfig(rect_id, fill='white')
        self.selected_cells = []

    def _get_all_found_positions(self):
        """Get all positions that are part of found words."""
        positions = set()
        if self.word_search:
            for word_info in self.word_search.placed_words:
                if word_info['word'] in self.found_words:
                    positions.update(word_info['positions'])
        return positions

    def get_cell_from_coords(self, x, y):
        """Get cell (row, col) from canvas coordinates."""
        if not self.word_search:
            return None

        col = x // self.cell_size
        row = y // self.cell_size

        height = len(self.word_search.grid)
        width = len(self.word_search.grid[0]) if height > 0 else 0

        if 0 <= row < height and 0 <= col < width:
            return (row, col)
        return None

    def on_canvas_press(self, event):
        """Handle mouse button press on canvas."""
        cell = self.get_cell_from_coords(event.x, event.y)
        if cell is None:
            return

        self.is_dragging = True
        self.clear_selection()
        row, col = cell
        self.selected_cells.append((row, col))
        if (row, col) not in self._get_all_found_positions():
            rect_id = self.cell_rects.get((row, col))
            if rect_id:
                self.grid_canvas.itemconfig(rect_id, fill='lightblue')

    def on_canvas_drag(self, event):
        """Handle mouse drag on canvas."""
        if not self.is_dragging:
            return

        cell = self.get_cell_from_coords(event.x, event.y)
        if cell is None:
            return

        row, col = cell

        # Only add if not already selected and forms a valid line
        if (row, col) not in self.selected_cells:
            # Check if this continues a valid line
            temp_cells = self.selected_cells + [(row, col)]
            if self._is_valid_line_list(temp_cells):
                self.selected_cells.append((row, col))
                if (row, col) not in self._get_all_found_positions():
                    rect_id = self.cell_rects.get((row, col))
                    if rect_id:
                        self.grid_canvas.itemconfig(rect_id, fill='lightblue')

    def on_canvas_release(self, event):
        """Handle mouse button release."""
        self.is_dragging = False
        self.check_for_word()

    def check_for_word(self):
        """Check if the selected cells form a valid word."""
        if len(self.selected_cells) < 2:
            return

        # Sort cells to form a line
        if not self._is_valid_line():
            return

        # Get the word formed by selected cells
        selected_word = ''.join([self.word_search.grid[r][c] for r, c in self.selected_cells])

        # Check if it matches any unfound word (forward or backward)
        for word_info in self.word_search.placed_words:
            word = word_info['word']
            if word in self.found_words:
                continue

            positions = word_info['positions']
            if (set(self.selected_cells) == set(positions) or
                set(self.selected_cells) == set(reversed(positions))):
                # Found a word!
                self.mark_word_found(word, positions)
                return

    def _is_valid_line(self):
        """Check if selected cells form a straight line."""
        return self._is_valid_line_list(self.selected_cells)

    def _is_valid_line_list(self, cells):
        """Check if a list of cells forms a straight line."""
        if len(cells) < 2:
            return True

        # Get direction from first two cells
        first = cells[0]
        second = cells[1]

        dr = second[0] - first[0]
        dc = second[1] - first[1]

        # Normalize direction
        if dr != 0:
            dr = dr // abs(dr)
        if dc != 0:
            dc = dc // abs(dc)

        # Check if all cells follow the same direction
        for i in range(len(cells) - 1):
            curr = cells[i]
            next_cell = cells[i + 1]
            expected_next = (curr[0] + dr, curr[1] + dc)
            if next_cell != expected_next:
                return False

        return True

    def mark_word_found(self, word, positions):
        """Mark a word as found."""
        self.found_words.add(word)

        # Highlight the found word in green
        for row, col in positions:
            rect_id = self.cell_rects.get((row, col))
            if rect_id:
                self.grid_canvas.itemconfig(rect_id, fill='lightgreen')

        # Strike through the word in the list
        if word in self.word_labels:
            self.word_labels[word].config(font=('TkDefaultFont', 10, 'overstrike'))

        # Clear selection
        self.selected_cells = []

        # Check if all words are found
        if len(self.found_words) == len(self.word_search.placed_words):
            messagebox.showinfo("Congratulations!", "You found all the words!")

    def create_grid_ui(self):
        """Create the interactive grid UI using Canvas."""
        # Clear existing grid
        for widget in self.grid_frame.winfo_children():
            widget.destroy()

        self.cell_rects = {}
        self.cell_texts = {}

        height = len(self.word_search.grid)
        width = len(self.word_search.grid[0]) if height > 0 else 0

        # Create canvas
        canvas_width = width * self.cell_size
        canvas_height = height * self.cell_size
        self.grid_canvas = tk.Canvas(self.grid_frame, width=canvas_width, height=canvas_height,
                                     bg='white', highlightthickness=1, highlightbackground='gray')
        self.grid_canvas.pack()

        # Draw grid cells
        for row in range(height):
            for col in range(width):
                x1 = col * self.cell_size
                y1 = row * self.cell_size
                x2 = x1 + self.cell_size
                y2 = y1 + self.cell_size

                # Create rectangle
                rect_id = self.grid_canvas.create_rectangle(x1, y1, x2, y2,
                                                            fill='white',
                                                            outline='gray',
                                                            width=1)
                self.cell_rects[(row, col)] = rect_id

                # Create text
                letter = self.word_search.grid[row][col]
                text_x = x1 + self.cell_size // 2
                text_y = y1 + self.cell_size // 2
                text_id = self.grid_canvas.create_text(text_x, text_y,
                                                       text=letter,
                                                       font=('Courier', 14, 'bold'),
                                                       fill='black')
                self.cell_texts[(row, col)] = text_id

        # Bind mouse events to canvas
        self.grid_canvas.bind('<Button-1>', self.on_canvas_press)
        self.grid_canvas.bind('<B1-Motion>', self.on_canvas_drag)
        self.grid_canvas.bind('<ButtonRelease-1>', self.on_canvas_release)

    def update_word_list(self):
        """Update the word list display."""
        # Clear existing words
        for widget in self.word_list_inner.winfo_children():
            widget.destroy()

        self.word_labels = {}

        # Add each word
        for i, word_info in enumerate(self.word_search.placed_words):
            word = word_info['word']
            label = tk.Label(self.word_list_inner, text=word, font=('TkDefaultFont', 10),
                           anchor='w', padx=10, pady=5)
            label.pack(fill=tk.X)
            self.word_labels[word] = label

    def generate_word_search(self):
        """Generate the word search puzzle."""
        try:
            width = int(self.width_var.get())
            height = int(self.height_var.get())

            if width < 5 or width > 25 or height < 5 or height > 25:
                messagebox.showerror("Error", "Dimensions must be between 5 and 25")
                return

            # Get words
            words_input = self.words_text.get('1.0', tk.END).strip()
            words = [word.strip() for word in words_input.split('\n') if word.strip()]

            if not words:
                messagebox.showerror("Error", "Please enter at least one word")
                return

            # Create word search
            ws = WordSearchGenerator(width, height)

            failed_words = []
            for word in words:
                if len(word) > max(width, height):
                    failed_words.append(f"{word} (too long)")
                elif not ws.place_word(word):
                    failed_words.append(f"{word} (couldn't fit)")

            ws.fill_empty_cells()

            if len(ws.placed_words) == 0:
                messagebox.showerror("Error", "No words could be placed. Try increasing grid size.")
                return

            # Store the word search
            self.word_search = ws
            self.found_words = set()
            self.selected_cells = []

            # Create the interactive UI
            self.create_grid_ui()
            self.update_word_list()

            # Resize window to fit puzzle
            self.resize_window_for_puzzle(width, height)

            if failed_words:
                messagebox.showwarning("Warning",
                    f"{len(failed_words)} word(s) couldn't be placed. Try increasing grid size or removing some words.")

        except ValueError:
            messagebox.showerror("Error", "Please enter valid numbers for dimensions")

    def resize_window_for_puzzle(self, grid_width, grid_height):
        """Resize the window to fit the puzzle comfortably."""
        # Calculate required size for puzzle grid
        puzzle_width = grid_width * self.cell_size
        puzzle_height = grid_height * self.cell_size

        # Add generous padding for UI elements:
        # - Input section height: ~280px
        # - Right panel (word list) width: ~280px
        # - Frame padding, borders, and extra space: ~120px width, ~120px height
        total_width = puzzle_width + 280 + 120
        total_height = puzzle_height + 280 + 120

        # Set minimum based on puzzle size to ensure it fits
        min_width = max(900, total_width)
        min_height = max(700, total_height)
        max_width = 1800
        max_height = 1200

        # Clamp to maximum bounds only
        final_width = min(max_width, min_width)
        final_height = min(max_height, min_height)

        # Resize the window
        self.root.geometry(f"{final_width}x{final_height}")

        # Update the window to apply changes
        self.root.update_idletasks()

    def export_to_pdf(self):
        """Export the current word search puzzle to a PDF file."""
        if not self.word_search:
            messagebox.showerror("Error", "Please generate a word search puzzle first")
            return

        # Ask user for save location
        file_path = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
            title="Save Word Search as PDF"
        )

        if not file_path:
            return

        try:
            # Create PDF
            pdf = pdf_canvas.Canvas(file_path, pagesize=LETTER_SIZE)
            page_width, page_height = LETTER_SIZE

            # Title
            pdf.setFont("Helvetica-Bold", 20)
            pdf.drawCentredString(page_width / 2, page_height - 0.75 * inch, "Word Search Puzzle")

            # Grid setup
            height = len(self.word_search.grid)
            width = len(self.word_search.grid[0]) if height > 0 else 0

            # Calculate cell size to fit on page
            max_grid_width = 6.5 * inch
            max_grid_height = 6.5 * inch
            cell_size = min(max_grid_width / width, max_grid_height / height, 0.4 * inch)

            # Center the grid
            grid_width = width * cell_size
            grid_height = height * cell_size
            start_x = (page_width - grid_width) / 2
            start_y = page_height - 2 * inch

            # Draw grid
            pdf.setFont("Courier-Bold", int(cell_size * 0.6))
            for row in range(height):
                for col in range(width):
                    x = start_x + col * cell_size
                    y = start_y - row * cell_size

                    # Draw letter centered in cell (no border)
                    letter = self.word_search.grid[row][col]
                    text_x = x + cell_size / 2
                    text_y = y - cell_size / 2 - cell_size * 0.15
                    pdf.drawCentredString(text_x, text_y, letter)

            # Word list
            word_list_y = start_y - grid_height - 0.5 * inch
            pdf.setFont("Helvetica-Bold", 12)
            pdf.drawString(1 * inch, word_list_y, "Words to Find:")

            pdf.setFont("Helvetica", 10)
            words = [word_info['word'] for word_info in self.word_search.placed_words]

            # Arrange words in columns
            words_per_column = 10
            col_width = 1.5 * inch
            current_y = word_list_y - 0.25 * inch

            for i, word in enumerate(words):
                col_num = i // words_per_column
                row_num = i % words_per_column

                x = 1 * inch + col_num * col_width
                y = current_y - row_num * 0.2 * inch

                pdf.drawString(x, y, f"• {word}")

            # Save PDF
            pdf.save()

            messagebox.showinfo("Success", f"Word search exported to:\n{file_path}")

        except Exception as e:
            messagebox.showerror("Error", f"Failed to export PDF:\n{str(e)}")


def main():
    root = tk.Tk()
    app = WordSearchApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()
