# main.py

import tkinter as tk
from render import Render
import time
import threading
from maze_generator import generate_maze
from tkinter import messagebox
from pathfinding_algorithms import bfs_generator, dfs_generator, a_star_generator

class MazeSolverApp:
    def __init__(self, root):
        self.root = root
        self.render = Render(root, self)
        self.timer_running = False
        self.start_time = None
        self.elapsed_time = 0
        self.timer_id = None
        self.maze = None
        self.solving = False
        self.autogenerated_seed = None  # Track the last autogenerated seed

        # Bind keys to quit the application
        self.root.bind('<Escape>', self.quit_application)
        self.root.bind('q', self.quit_application)

    def quit_application(self, _=None):
        """Cleanly quit the application."""
        print("Quitting the application...")
        self.root.quit()  # This will close the Tkinter loop
        self.root.destroy()  # This will clean up and destroy the window
        print("Application exited.")

    def start_timer(self):
        if not self.timer_running:
            self.timer_running = True
            self.start_time = time.time() - self.elapsed_time
            self.update_timer()

    def stop_timer(self):
        if self.timer_running:
            self.timer_running = False
            if self.timer_id:
                self.root.after_cancel(self.timer_id)

    def reset_timer(self):
        self.stop_timer()
        self.elapsed_time = 0
        self.render.update_timer_label("00:00")

    def update_timer(self):
        if self.timer_running:
            self.elapsed_time = time.time() - self.start_time
            minutes = int(self.elapsed_time // 60)
            seconds = int(self.elapsed_time % 60)
            self.render.update_timer_label(f"{minutes:02d}:{seconds:02d}")
            self.timer_id = self.root.after(1000, self.update_timer)

    def generate_maze(self):
        params = self.render.get_maze_parameters()
        if params is None:
            return  # Invalid parameters, abort

        self.reset_timer()

        # Get additional parameters from the new sliders
        dead_ends = int(self.render.dead_ends_scale.get())
        branching_factor = int(self.render.branching_factor_scale.get())

        # Generate maze in a separate thread to avoid freezing the UI
        threading.Thread(target=self._generate_maze_thread, args=(params, dead_ends, branching_factor)).start()

    def _generate_maze_thread(self, params, dead_ends, branching_factor):
        try:
            # Check if the seed input is empty or if it's the autogenerated seed
            seed_input = params['seed']
            if seed_input is None or str(seed_input) == self.autogenerated_seed:
                seed = None  # Force the generator to create a new random seed
            else:
                seed = seed_input  # Use the user-provided seed

            # Get the values from the new sliders
            dead_ends = int(self.render.dead_ends_scale.get())
            branching_factor = int(self.render.branching_factor_scale.get())

            # Call generate_maze with the seed
            maze, used_seed = generate_maze(
                rows=params['rows'],
                cols=params['cols'],
                generation_algorithm=params['generation_algorithm'],
                seed=seed,
                wall_density=params['wall_density'],
                dead_ends=dead_ends,            # Pass the dead ends parameter
                branching_factor=branching_factor  # Pass the branching factor parameter
            )
            
            self.maze = maze

            # If the seed was autogenerated (seed=None), update the seed field
            if seed is None:
                self.autogenerated_seed = str(used_seed)  # Track the new autogenerated seed
                self.render.seed_entry.delete(0, tk.END)
                self.render.seed_entry.insert(0, str(used_seed))  # Insert the generated seed
            else:
                self.autogenerated_seed = None  # Reset if a user-provided seed is used

            # Update the UI in the main thread
            self.root.after(0, self.render.draw_maze, self.maze)
            # self.root.after(0, self.start_timer)
            print(f"Maze generated with seed: {used_seed}")
            
        except Exception as e:
            print(f"Error generating maze: {e}")
            messagebox.showerror("Error", f"An error occurred while generating the maze:\n{e}")

    def solve_maze(self):
        if self.maze is None or self.maze.size == 0:  # Adjust the maze check to avoid ambiguity
            messagebox.showwarning("No Maze", "Please generate a maze first.")
            return
        if self.solving:
            messagebox.showinfo("Solving", "Maze is already being solved.")
            return

        self.solving = True
        self.reset_timer()
        self.start_time = time.time()
        self.start_timer()
        self.steps = 0

        # Get selected algorithm
        algorithm = self.render.update_algorithm_selection()

        # Initialize the solver generator
        if algorithm == "BFS":
            self.solver_generator = bfs_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2))
        elif algorithm == "DFS":
            self.solver_generator = dfs_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2))
        elif algorithm == "A*":
            self.solver_generator = a_star_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2))
        else:
            messagebox.showerror("Error", f"Unknown algorithm: {algorithm}")
            self.solving = False
            self.stop_timer()
            return

        # Start the solving process
        self.root.after(0, self._process_solver_step)

    def _process_solver_step(self):
        try:
            action, cell, steps = next(self.solver_generator)
            self.steps = steps

            if action == 'visit':
                self.render.mark_visited(cell)
            elif action == 'enqueue':
                self.render.mark_frontier(cell)
            elif action == 'path':
                self.render.mark_path(cell)
            elif action == 'done':
                self.solving = False
                self.stop_timer()
                self.show_summary()
                return

            # Schedule the next step
            self.root.after(10, self._process_solver_step)  # Adjust delay as needed for visualization speed
        except StopIteration:
            self.solving = False
            self.stop_timer()
            self.show_summary()

    def show_summary(self):
        elapsed_time = time.time() - self.start_time if self.start_time else 0
        params = self.render.get_maze_parameters() or {}
        algorithm = self.render.update_algorithm_selection() or "Unknown"
        summary = (
            f"Algorithm: {algorithm}\n"
            f"Time Taken: {elapsed_time:.2f} seconds\n"
            f"Steps: {self.steps or 0}\n"
            f"Maze Size: {params.get('rows', '?')}x{params.get('cols', '?')}\n"
            f"Generation Algorithm: {params.get('generation_algorithm', 'Unknown')}\n"
            f"Wall Density: {params.get('wall_density', 'Unknown')}\n"
            f"Seed: {params.get('seed', 'None')}"
        )
        messagebox.showinfo("Maze Solved!", summary)

    def stop_solving(self):
        if self.solving:
            # Currently, our solving algorithms are not interruptible.
            # Implementing a stoppable solving process would require more complex threading control.
            messagebox.showinfo("Stop Solving", "Stopping the solver is not implemented yet.")
        else:
            messagebox.showinfo("Not Solving", "No solving process is currently running.")

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Maze Solver")
    app = MazeSolverApp(root)

    try:
        root.mainloop()  # Start the Tkinter event loop
    except KeyboardInterrupt:
        print("\nKeyboardInterrupt detected. Exiting the application cleanly...")
        app.quit_application()  # Call the quit method to clean up
