import tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
import requests
import threading
import io
import os
import vlc  # Ensure you have python-vlc installed
from pynput import mouse  # For mouse listening
import time  # Added for time handling


class VideoPlayer:
    def __init__(self, canvas, video_url, vlc_instance, play_button, seek_var=None, seek_scale=None, main_app=None, index=0):
        """
        Initializes the VideoPlayer.

        :param canvas: Tkinter Canvas where the video will be displayed.
        :param video_url: URL of the video to be played.
        :param vlc_instance: Shared VLC Instance object.
        :param play_button: Button widget to control play/pause.
        :param seek_var: DoubleVar linked to the seek Scale.
        :param seek_scale: Scale widget for seeking.
        :param main_app: Reference to the main ImageApp.
        :param index: Index of this video in the main_app's video_players list.
        """
        self.canvas = canvas
        self.video_url = video_url
        self.vlc_instance = vlc_instance
        self.play_button = play_button
        self.seek_var = seek_var
        self.seek_scale = seek_scale
        self.main_app = main_app
        self.index = index

        self.player = self.vlc_instance.media_player_new()
        self.media = self.vlc_instance.media_new(self.video_url) if self.video_url else None
        if self.media:
            self.player.set_media(self.media)
        self.playing = False
        self.fullscreen_player = None

        # Bind destroy event to cleanup
        self.canvas.bind("<Destroy>", self.on_destroy)

        # Configure seek scale
        if self.seek_scale and self.seek_var:
            self.seek_scale.configure(from_=0, to=100)
            self.seek_scale.bind("<ButtonPress-1>", self.on_seek_start)
            self.seek_scale.bind("<ButtonRelease-1>", self.on_seek_end)

    def on_seek_start(self, event):
        """Called when the user starts interacting with the seek bar."""
        self.seeking = True

    def on_seek_end(self, event):
        """Called when the user finishes interacting with the seek bar."""
        if self.get_length() > 0:
            new_time = (self.seek_var.get() / 100) * self.get_length()
            self.player.set_time(int(new_time))
        self.seeking = False

    def on_destroy(self, event):
        """Called when the widget is destroyed. Performs cleanup."""
        self.stop_video()
        if self.media:
            self.player.release()
            self.media.release()

    def play_video(self):
        """Starts or resumes video playback."""
        try:
            if not self.playing and self.player:
                # Assign the window handle if not already assigned
                if os.name == "nt":
                    self.player.set_hwnd(self.canvas.winfo_id())
                elif os.name == "posix":
                    self.player.set_xwindow(self.canvas.winfo_id())
                else:
                    pass  # MacOS handling if needed

                self.player.play()
                self.playing = True
                self.play_button.config(text="■")  # Pause icon
                self.update_seek_bar()
        except Exception as e:
            print(f"Error in play_video: {e}")
            messagebox.showerror("Playback Error", f"An error occurred while playing the video: {e}")

    def pause_video(self):
        """Pauses video playback."""
        try:
            if self.playing and self.player:
                self.player.pause()
                self.playing = False
                self.play_button.config(text="▶")  # Play icon
        except Exception as e:
            print(f"Error in pause_video: {e}")
            messagebox.showerror("Playback Error", f"An error occurred while pausing the video: {e}")

    def toggle_play_pause(self):
        """Toggles between play and pause."""
        if self.playing:
            self.pause_video()
        else:
            self.play_video()

    def stop_video(self):
        """Stops video playback."""
        try:
            if self.playing and self.player:
                self.player.stop()
                self.playing = False
                self.play_button.config(text="▶")  # Play icon
            # If fullscreen is active, close it
            if self.fullscreen_player and self.fullscreen_player.fullscreen_window:
                self.fullscreen_player.exit_fullscreen()
                self.fullscreen_player = None
        except Exception as e:
            print(f"Error in stop_video: {e}")

    def get_length(self):
        """Returns the length of the video in milliseconds."""
        if self.player:
            return self.player.get_length()
        return 0

    def get_time(self):
        """Returns the current playback time in milliseconds."""
        if self.player:
            return self.player.get_time()
        return 0

    def update_seek_bar(self):
        """Updates the seek bar based on the current playback time."""
        if self.playing and not getattr(self, 'seeking', False) and self.seek_var:
            current_time = self.get_time()
            length = self.get_length()
            if length > 0:
                progress = (current_time / length) * 100
                self.seek_var.set(progress)
        # Schedule next update
        if self.seek_scale:
            self.after_id = self.seek_scale.after(500, self.update_seek_bar)

    def enter_fullscreen(self):
        """Enters fullscreen mode with additional controls."""
        try:
            # If already in fullscreen, do nothing
            if self.fullscreen_player:
                return

            # Create a new fullscreen window
            self.fullscreen_player = VideoPlayerFullscreen(self)

            # Pause the video in the original window to prevent duplicate playback
            self.pause_video()
        except Exception as e:
            print(f"Error in enter_fullscreen: {e}")
            messagebox.showerror("Fullscreen Error", f"An error occurred while entering fullscreen: {e}")

    def seek_video(self, milliseconds):
        """Seeks the video by the given milliseconds."""
        if self.player:
            current_time = self.get_time()
            new_time = current_time + milliseconds
            new_time = max(0, min(new_time, self.get_length()))
            self.player.set_time(int(new_time))


class VideoPlayerFullscreen:
    def __init__(self, video_player):
        """
        Initializes the fullscreen video player.

        :param video_player: Instance of VideoPlayer.
        """
        self.video_player = video_player
        self.vlc_instance = video_player.vlc_instance
        self.main_app = video_player.main_app

        # Create a new Toplevel window
        self.fullscreen_window = tk.Toplevel()
        self.fullscreen_window.attributes("-fullscreen", True)
        self.fullscreen_window.configure(bg='black')

        # Bind escape key to exit fullscreen
        self.fullscreen_window.bind("<Escape>", self.exit_fullscreen)

        # Bind window close event to ensure proper exit
        self.fullscreen_window.protocol("WM_DELETE_WINDOW", self.exit_fullscreen)

        # Create a canvas in the fullscreen window
        self.fullscreen_canvas = tk.Canvas(self.fullscreen_window, bg='black', highlightthickness=0)
        self.fullscreen_canvas.pack(fill="both", expand=True)

        # Reassign the VLC player to the fullscreen canvas
        if os.name == "nt":
            handle = self.fullscreen_canvas.winfo_id()
            self.video_player.player.set_hwnd(handle)
        elif os.name == "posix":
            handle = self.fullscreen_canvas.winfo_id()
            self.video_player.player.set_xwindow(handle)
        else:
            pass  # MacOS handling if needed

        # Remove the video image from the original canvas to prevent overlay
        self.video_player.canvas.delete("all")

        # Ensure the GUI updates before playing
        self.fullscreen_window.update_idletasks()

        # Start playing
        self.video_player.player.play()
        self.video_player.playing = True
        self.video_player.play_button.config(text="■")  # Pause icon

        # Create control frame
        control_frame = ttk.Frame(self.fullscreen_window)
        control_frame.place(relx=0.5, rely=0.95, anchor='center')  # Positioned at bottom center

        # Previous Button
        prev_button = ttk.Button(
            control_frame,
            text="⏮️ Previous",
            command=self.main_app.previous_video_fullscreen,
            style='Ctrl.TButton'
        )
        prev_button.grid(row=0, column=0, padx=5)

        # Play/Pause Button
        self.play_pause_fullscreen = ttk.Button(
            control_frame,
            text="❚❚",
            command=self.video_player.toggle_play_pause,
            style='Ctrl.TButton'
        )
        self.play_pause_fullscreen.grid(row=0, column=1, padx=5)

        # Next Button
        next_button = ttk.Button(
            control_frame,
            text="Next ⏭️",
            command=self.main_app.next_video_fullscreen,
            style='Ctrl.TButton'
        )
        next_button.grid(row=0, column=2, padx=5)

        # Seek Backward Button
        seek_back_button = ttk.Button(
            control_frame,
            text="⏪ 10s",
            command=lambda: self.video_player.seek_video(-10000),
            style='Ctrl.TButton'
        )
        seek_back_button.grid(row=0, column=3, padx=5)

        # Seek Forward Button
        seek_forward_button = ttk.Button(
            control_frame,
            text="10s ⏩",
            command=lambda: self.video_player.seek_video(10000),
            style='Ctrl.TButton'
        )
        seek_forward_button.grid(row=0, column=4, padx=5)

        # Exit Fullscreen Button
        exit_fullscreen_button = ttk.Button(
            control_frame,
            text="Exit Fullscreen ⛌",
            command=self.exit_fullscreen,
            style='Ctrl.TButton'
        )
        exit_fullscreen_button.grid(row=0, column=5, padx=5)

        # Start updating the seek bar
        if self.video_player.seek_scale and self.video_player.seek_var:
            self.video_player.update_seek_bar()

    def exit_fullscreen(self, event=None):
        """Exits fullscreen mode and returns the video to the original canvas."""
        try:
            # Stop the VLC player before reassigning
            self.video_player.player.stop()

            # Reassign the VLC player back to the original canvas
            if os.name == "nt":
                handle = self.video_player.canvas.winfo_id()
                self.video_player.player.set_hwnd(handle)
            elif os.name == "posix":
                handle = self.video_player.canvas.winfo_id()
                self.video_player.player.set_xwindow(handle)
            else:
                pass  # MacOS handling if needed

            # Restart the video playback on the original canvas
            self.video_player.play_video()

            # Destroy the fullscreen window
            self.fullscreen_window.destroy()
            self.fullscreen_window = None
            self.video_player.fullscreen_player = None

            # Inform the main app that fullscreen has been exited
            if self.video_player.main_app.current_fullscreen_vp == self.video_player:
                self.video_player.main_app.current_fullscreen_vp = None

            # Ensure the GUI updates before playing
            self.video_player.canvas.update_idletasks()

            # Start updating the seek bar
            if self.video_player.seek_scale and self.video_player.seek_var:
                self.video_player.update_seek_bar()
        except Exception as e:
            print(f"Error in exit_fullscreen: {e}")
            messagebox.showerror("Fullscreen Error", f"An error occurred while exiting fullscreen: {e}")


def download_video(video_url, code, button):
    """Downloads a video asynchronously and updates the UI upon completion."""
    if not video_url:
        print(f"Video URL not found (Code: {code}).")
        return

    def download():
        try:
            response = requests.get(video_url, stream=True)
            response.raise_for_status()

            # Create 'videos' directory if it doesn't exist
            if not os.path.exists('videos'):
                os.makedirs('videos')

            # Create filename using code
            file_name = f"{code}.mp4"
            save_path = os.path.join('videos', file_name)

            with open(save_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            print(f"Video successfully downloaded: {save_path}")

            # Update download button to green and disable it
            button.after(0, lambda: button.config(style='Green.TButton', text="Downloaded", state="disabled"))
        except Exception as e:
            print(f"Error downloading video (Code: {code}): {e}")
            button.after(0, lambda: messagebox.showerror("Download Error", f"Could not download video: {e}"))

    # Start download in a separate daemon thread
    threading.Thread(target=download, daemon=True).start()


def fetch_data(callback):
    """Fetches video data from Instagram and invokes the callback with the data."""
    headers = {
        'user-agent': 'Instagram 146.0.0.27.125 (iPhone12,1; iOS 13_3; en_US; en-US; scale=2.00; 1656x3584; 190542906)'
    }
    data = {
        'target_user_id': '55408673825',
        'page_size': '12'
    }
    try:
        response = requests.post('https://i.instagram.com/api/v1/clips/user/', headers=headers, data=data)
        response.raise_for_status()  # Catch HTTP errors
        response_data = response.json()

        max_id = response_data['paging_info'].get('max_id')
        print("Max ID:", max_id)

        items = []
        for i, item in enumerate(response_data.get("items", [])):
            try:
                media = item["media"]
                image_versions = media["image_versions2"]
                candidates = image_versions.get("candidates", [])

                # Take the first image candidate
                if isinstance(candidates, list) and candidates:
                    image_url = candidates[0].get("url")
                else:
                    image_url = None

                # Get the video URL
                video_versions = media.get("video_versions", [])
                video_url = video_versions[0].get("url") if video_versions else None

                # Get the 'code' value
                code = media.get("code", f"video_{i}")

                if image_url:
                    items.append({
                        "image_url": image_url,
                        "video_url": video_url,
                        "code": code
                    })
            except (KeyError, IndexError, TypeError) as e:
                print(f"Error fetching data for item {i}: {e}")

        callback(items)

    except Exception as e:
        print("Error fetching data:", e)
        callback([])  # Return empty list on error


def load_images(items, frame, vlc_instance, main_app, columns=4, image_size=(600, 900)):
    """Downloads images and adds them to the GUI."""
    def download_and_display(index, item):
        image_url = item["image_url"]
        video_url = item["video_url"]
        code = item["code"]

        try:
            img_response = requests.get(image_url)
            img_response.raise_for_status()
            img_data = img_response.content
            image = Image.open(io.BytesIO(img_data))
            image = image.resize(image_size, Image.Resampling.LANCZOS)
            photo = ImageTk.PhotoImage(image)

            # Tkinter updates must occur in the main thread using 'after'
            def add_image():
                row = index // columns
                col = index % columns

                # Create Image Frame
                image_frame = ttk.Frame(frame, relief="raised", borderwidth=2)
                image_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
                frame.grid_rowconfigure(row, weight=1)
                frame.grid_columnconfigure(col, weight=1)

                # Create Canvas for Video and Buttons
                video_canvas = tk.Canvas(image_frame, width=image_size[0], height=image_size[1],
                                         highlightthickness=0, bg='black')
                video_canvas.pack(fill="both", expand=True)

                # Add Image to Canvas as a placeholder
                video_canvas.create_image(0, 0, anchor='nw', image=photo)
                video_canvas.image = photo  # Keep a reference

                # Create Frame for Buttons and place it at the top-right corner
                buttons_frame = ttk.Frame(image_frame)
                buttons_frame.place(relx=0.85, rely=0.01)

                # Create Seek Bar
                seek_frame = ttk.Frame(image_frame)
                seek_frame.pack(fill="x", padx=5, pady=5)

                seek_var = tk.DoubleVar()
                seek_scale = ttk.Scale(seek_frame, orient='horizontal', length=image_size[0],
                                       from_=0, to=100, variable=seek_var)
                seek_scale.pack(fill='x')

                # Create Play/Stop Button
                play_stop_button = ttk.Button(buttons_frame, text="▶", width=3)
                play_stop_button.pack(side="top", pady=2)

                # Create Download Button
                if video_url:
                    download_button = ttk.Button(buttons_frame, text="⬇️")
                    download_button.config(command=lambda b=download_button: download_video(video_url, code, b))
                else:
                    download_button = ttk.Button(buttons_frame, text="⬇️", state="disabled")
                download_button.pack(side="top", pady=2)

                # Create Fullscreen Button
                if video_url:
                    fullscreen_button = ttk.Button(buttons_frame, text="⛶", width=3)
                    # Initially disable the fullscreen button until VideoPlayer is created
                else:
                    fullscreen_button = ttk.Button(buttons_frame, text="⛶", state="disabled")
                fullscreen_button.pack(side="top", pady=2)

                # Instantiate VideoPlayer now with all widgets
                video_player = VideoPlayer(
                    video_canvas,
                    video_url,
                    vlc_instance,
                    play_stop_button,
                    seek_var,
                    seek_scale,
                    main_app,
                    index
                )

                if video_url:
                    # Configure Play/Stop Button
                    play_stop_button.config(command=lambda vp=video_player: vp.toggle_play_pause())

                    # Configure Fullscreen Button
                    fullscreen_button.config(command=lambda vp=video_player: vp.main_app.play_video_in_fullscreen(vp.index),
                                             state="normal")
                else:
                    # Disable buttons if no video
                    play_stop_button.config(state="disabled")

                # Assign the VideoPlayer to the correct index in video_players
                if index < len(main_app.video_players):
                    main_app.video_players[index] = video_player
                else:
                    main_app.video_players.append(video_player)

            frame.after(0, add_image)
        except Exception as e:
            print(f"Error downloading image ({image_url}): {e}")

    # Initialize video_players list with None to ensure correct indexing
    if not hasattr(main_app, 'video_players') or not isinstance(main_app.video_players, list):
        main_app.video_players = [None] * len(items)
    else:
        # Ensure the list is the correct size
        while len(main_app.video_players) < len(items):
            main_app.video_players.append(None)

    # Download and add images concurrently
    for idx, item in enumerate(items):
        threading.Thread(target=download_and_display, args=(idx, item), daemon=True).start()


class ImageApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Instagram Videos")
        self.state('zoomed')  # Maximize window (OS-specific)
        self.bind("<F11>", self.toggle_fullscreen)  # Toggle fullscreen with F11
        self.bind("<Escape>", self.exit_fullscreen)  # Exit fullscreen with Escape

        # Style Definitions
        self.style = ttk.Style()
        self.style.theme_use('clam')  # Use 'clam' theme for better styling
        self.style.configure('Green.TButton', foreground='white', background='#4CAF50',
                             font=('Helvetica', 10, 'bold'))
        self.style.map('Green.TButton',
                       foreground=[('active', 'white')],
                       background=[('active', '#45a049')])

        self.style.configure('Ctrl.TButton', foreground='white', background='#2196F3',
                             font=('Helvetica', 10, 'bold'))
        self.style.map('Ctrl.TButton',
                       foreground=[('active', 'white')],
                       background=[('active', '#1E88E5')])

        # Shared VLC instance
        self.vlc_instance = vlc.Instance()

        # List to keep track of VideoPlayer instances
        self.video_players = []

        # Track the currently active fullscreen VideoPlayer
        self.current_fullscreen_vp = None

        # Scrollable Frame Setup
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True)

        canvas = tk.Canvas(container, bg='gray20')
        scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview)
        self.scrollable_frame = ttk.Frame(canvas)

        canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set, bg='gray20')

        self.scrollable_canvas = canvas  # Make canvas accessible

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

        # Initialize debounce variables for mouse scrolling
        self.last_scroll_time = 0
        self.scroll_delay = 100  # milliseconds

        # Start mouse wheel listener using Pynput
        self.start_mouse_listener(canvas)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

        # Initial loading message
        self.loading_label = ttk.Label(
            self.scrollable_frame,
            text="Loading images...",
            font=("Arial", 16),
            foreground='white',
            background='gray20'
        )
        self.loading_label.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

        # Start data fetching in a separate thread
        threading.Thread(target=self.start_fetching, daemon=True).start()

        # Handle GUI closure
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

    def start_mouse_listener(self, canvas):
        """Starts a global mouse wheel listener using Pynput."""
        def on_scroll(x, y, dx, dy):
            current_time = time.time() * 1000  # Current time in ms
            if current_time - self.last_scroll_time < self.scroll_delay:
                return  # Debounce: ignore if within delay
            self.last_scroll_time = current_time

            # dy: Vertical scroll (positive: down, negative: up)
            if dy > 0:
                direction = -1  # Scroll down
            elif dy < 0:
                direction = 1  # Scroll up
            else:
                direction = 0

            if direction != 0:
                # Must update Tkinter in the main thread
                canvas.after(0, lambda: canvas.yview_scroll(direction, "units"))

        def listen():
            with mouse.Listener(on_scroll=on_scroll) as listener:
                listener.join()

        # Start the mouse listener in a separate daemon thread
        listener_thread = threading.Thread(target=listen, daemon=True)
        listener_thread.start()

    def toggle_fullscreen(self, event=None):
        """Toggles fullscreen mode for the entire application."""
        try:
            is_fullscreen = self.attributes("-fullscreen")
            self.attributes("-fullscreen", not is_fullscreen)
        except Exception as e:
            print(f"Error in toggle_fullscreen: {e}")
            messagebox.showerror("Error", f"An error occurred while toggling fullscreen: {e}")

    def exit_fullscreen(self, event=None):
        """Exits fullscreen mode for the entire application."""
        try:
            self.attributes("-fullscreen", False)
            # Additionally, if any fullscreen video is open, exit it
            if self.current_fullscreen_vp:
                self.current_fullscreen_vp.fullscreen_player.exit_fullscreen()
                self.current_fullscreen_vp = None
        except Exception as e:
            print(f"Error in exit_fullscreen: {e}")
            messagebox.showerror("Error", f"An error occurred while exiting fullscreen: {e}")

    def start_fetching(self):
        """Starts fetching data."""
        fetch_data(self.on_data_fetched)

    def on_data_fetched(self, items):
        """Handles data after fetching."""
        # Remove loading message
        self.scrollable_frame.after(0, self.loading_label.destroy)

        if not items:
            # Inform the user if no videos were found or data couldn't be fetched
            info_label = ttk.Label(
                self.scrollable_frame,
                text="No videos found or data could not be fetched.",
                font=("Arial", 14),
                foreground='white',
                background='gray20'
            )
            info_label.grid(row=1, column=0, padx=10, pady=10)
            return

        # Initialize video_players list with None to ensure correct indexing
        self.video_players = [None] * len(items)

        # Load and display images
        load_images(
            items,
            self.scrollable_frame,
            self.vlc_instance,
            self,
            columns=4,
            image_size=(600, 900)  # Preserved image_size
        )

    def on_closing(self):
        """Stops all VideoPlayers and then destroys the main window."""
        try:
            # Stop all VideoPlayer instances
            for vp in self.video_players:
                if vp:  # Check if VideoPlayer exists at this index
                    vp.stop_video()

            # Exit fullscreen if any VideoPlayer is in fullscreen
            if self.current_fullscreen_vp:
                self.current_fullscreen_vp.fullscreen_player.exit_fullscreen()
        except Exception as e:
            print(f"Error during closing: {e}")
        finally:
            # Destroy the main window
            self.destroy()

    def next_video_fullscreen(self):
        """Plays the next video in fullscreen."""
        current_vp = self.get_current_fullscreen_vp()
        if current_vp is not None:
            next_index = (current_vp.index + 1) % len(self.video_players)
            self.play_video_in_fullscreen(next_index)

    def previous_video_fullscreen(self):
        """Plays the previous video in fullscreen."""
        current_vp = self.get_current_fullscreen_vp()
        if current_vp is not None:
            prev_index = (current_vp.index - 1) % len(self.video_players)
            self.play_video_in_fullscreen(prev_index)

    def get_current_fullscreen_vp(self):
        """Returns the VideoPlayer instance that is currently in fullscreen."""
        return self.current_fullscreen_vp

    def play_video_in_fullscreen(self, index):
        """Plays the video at the specified index in fullscreen."""
        if 0 <= index < len(self.video_players):
            vp = self.video_players[index]
            if vp and vp.video_url:
                # If there's an active fullscreen video, exit its fullscreen
                if self.current_fullscreen_vp and self.current_fullscreen_vp.fullscreen_player:
                    self.current_fullscreen_vp.fullscreen_player.exit_fullscreen()

                # Set the new fullscreen video
                self.current_fullscreen_vp = vp
                vp.enter_fullscreen()


# Run the Application
if __name__ == "__main__":
    app = ImageApp()
    app.mainloop()