import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk
import requests
from bs4 import BeautifulSoup
import os
import threading
import concurrent.futures
from urllib.parse import urlparse, urljoin
import logging

# Set up logging
logging.basicConfig(level=logging.INFO, filename='scraper.log', filemode='w',
                    format='%(asctime)s - %(levelname)s - %(message)s')

class WebScraperGUI:
    def __init__(self, master):
        self.master = master
        master.title("Web Scraper")

        # Load the background image
        self.bg_image = self.load_background_image("background.png")  # Replace with your image file
        self.bg_photo = ImageTk.PhotoImage(self.bg_image)

        # Create a Label to hold the background image
        self.background_label = tk.Label(master, image=self.bg_photo)
        self.background_label.place(relwidth=1, relheight=1)  # Fill the entire window

        # Initialize variables
        self.url_var = tk.StringVar()
        self.file_type_var = tk.StringVar()
        self.save_dir_var = tk.StringVar()
        self.max_files_var = tk.IntVar(value=0)
        self.concurrent_downloads_var = tk.IntVar(value=5)  # Default to 5 concurrent downloads
        self.concurrent_subdir_var = tk.IntVar(value=5)  # Default to 5 concurrent subdirectory searches
        self.cancel_scraping = False

        self.setup_gui()

    def load_background_image(self, image_path):
        """Loads a background image for the GUI."""
        try:
            image = Image.open(image_path)
            return image.resize((600, 400), Image.LANCZOS)  # Resize to fit the window
        except Exception as e:
            logging.error(f"Error loading background image: {e}")
            messagebox.showerror("Error", "Failed to load background image.")
            return Image.new("RGB", (600, 400), "white")  # Fallback to a plain white image

    def setup_gui(self):
        """Sets up the GUI components."""
        # Configure grid weights
        self.master.grid_columnconfigure(0, weight=1)
        self.master.grid_columnconfigure(1, weight=1)
        self.master.grid_columnconfigure(2, weight=1)

        # Description labels with black background and white text
        label_style = {'bg': 'black', 'fg': 'white'}

        tk.Label(self.master, text="Enter URL:", **label_style).grid(row=0, column=0, sticky="w", padx=10, pady=5)
        self.url_entry = tk.Entry(self.master, textvariable=self.url_var, bg='black', fg='white')
        self.url_entry.grid(row=0, column=1, sticky="we", padx=10, pady=5)

        # Bind right-click context menu for URL entry
        self.url_entry.bind("<Button-3>", self.show_context_menu)

        tk.Label(self.master, text="File Type:", **label_style).grid(row=1, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(self.master, textvariable=self.file_type_var, bg='black', fg='white').grid(row=1, column=1, sticky="we", padx=10, pady=5)

        tk.Label(self.master, text="Save Directory:", **label_style).grid(row=2, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(self.master, textvariable=self.save_dir_var, bg='black', fg='white').grid(row=2, column=1, sticky="we", padx=10, pady=5)
        tk.Button(self.master, text="Browse", command=self.browse_save_dir, bg="#4CAF50", fg="white").grid(row=2, column=2, padx=10, pady=5)

        tk.Label(self.master, text="Max Files (0 for no limit):", **label_style).grid(row=3, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(self.master, textvariable=self.max_files_var, bg='black', fg='white').grid(row=3, column=1, sticky="we", padx=10, pady=5)

        tk.Label(self.master, text="Concurrent Downloads:", **label_style).grid(row=4, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(self.master, textvariable=self.concurrent_downloads_var, bg='black', fg='white').grid(row=4, column=1, sticky="we", padx=10, pady=5)

        tk.Label(self.master, text="Concurrent Subdir Searches:", **label_style).grid(row=5, column=0, sticky="w", padx=10, pady=5)
        tk.Entry(self.master, textvariable=self.concurrent_subdir_var, bg='black', fg='white').grid(row=5, column=1, sticky="we", padx=10, pady=5)

        tk.Button(self.master, text="Start Scraping", command=self.start_scraping, bg="#4CAF50", fg="white").grid(row=6, column=1, padx=10, pady=5)
        tk.Button(self.master, text="Cancel", command=self.cancel_scraping_process, bg="#f44336", fg="white").grid(row=6, column=2, padx=10, pady=5)
        tk.Button(self.master, text="Clear", command=self.clear_fields, bg="#2196F3", fg="white").grid(row=6, column=0, padx=10, pady=5)

        # Create a frame for the progress bar with a black background
        progress_frame = tk.Frame(self.master, bg='black')
        progress_frame.grid(row=7, column=0, columnspan=3, padx=10, pady=5, sticky="we")

        self.progress = ttk.Progressbar(progress_frame, length=0, mode='determinate')
        self.progress.pack(fill=tk.BOTH, expand=True)

        # Style the progress bar
        style = ttk.Style()
        style.configure("TProgressbar", background="green", troughcolor="black")

        self.status_label = tk.Label(self.master, text="", bg='white')
        self.status_label.grid(row=8, column=1, pady=5)

        # Create the output text area with black background and white text
        window_width = 600  # Example width
        text_width = int(window_width * 0.95)  # 95% of the window width
        self.output_text = tk.Text(self.master, height=10, width=text_width // 7, state=tk.DISABLED, bg='black', fg='white')  # Approximate width in text widget units
        self.output_text.grid(row=9, column=0, columnspan=3, padx=10, pady=5)

        # Bind paste event to URL entry
        self.url_entry.bind("<Control-v>", self.paste_url)

    def show_context_menu(self, event):
        """Shows the right-click context menu for copy/paste."""
        context_menu = tk.Menu(self.master, tearoff=0)
        context_menu.add_command(label="Copy", command=self.copy_url)
        context_menu.add_command(label="Paste", command=self.paste_url)
        context_menu.post(event.x_root, event.y_root)

    def copy_url(self):
        """Copies the URL to the clipboard."""
        self.master.clipboard_clear()
        self.master.clipboard_append(self.url_var.get())

    def paste_url(self, event=None):
        """Pastes URL from clipboard into the URL entry."""
        try:
            clipboard_content = self.master.clipboard_get()
            self.url_var.set(clipboard_content)
            return "break"
        except tk.TclError:
            pass

    def browse_save_dir(self):
        """Opens a dialog to select the save directory."""
        save_dir = filedialog.askdirectory()
        self.save_dir_var.set(save_dir)

    def clear_fields(self):
        """Clears all input fields."""
        self.url_var.set('')
        self.file_type_var.set('')
        self.save_dir_var.set('')
        self.max_files_var.set(0)
        self.concurrent_downloads_var.set(5)  # Reset to default
        self.concurrent_subdir_var.set(5)  # Reset to default
        self.output_text.configure(state=tk.NORMAL)
        self.output_text.delete(1.0, tk.END)
        self.output_text.configure(state=tk.DISABLED)
        self.progress.configure(value=0)
        self.status_label.config(text="")

    def start_scraping(self):
        """Starts the scraping process in a separate thread."""
        url = self.url_var.get().strip()
        if not url.startswith("http://") and not url.startswith("https://"):
            url = "https://" + url
        if ":" not in url:
            url += ":443"

        file_type = self.file_type_var.get().strip()
        if not file_type.startswith('.'):
            file_type = '.' + file_type

        save_dir = self.save_dir_var.get().strip()
        max_files = self.max_files_var.get()
        concurrent_downloads = self.concurrent_downloads_var.get()
        concurrent_subdirs = self.concurrent_subdir_var.get()

        if not url or not save_dir:
            messagebox.showerror("Error", "Please enter a valid URL and select a save directory.")
            return

        logging.info(f"Starting scraping with URL: {url}, File Type: {file_type}, Save Directory: {save_dir}, Max Files: {max_files}, Concurrent Downloads: {concurrent_downloads}, Concurrent Subdir Searches: {concurrent_subdirs}")
        self.update_output(f"Starting scraping with URL: {url}")

        self.status_label.config(text="Scraping in progress...")
        self.cancel_scraping = False
        self.scrape_thread = threading.Thread(target=self.scrape, args=(url, file_type, save_dir, max_files, concurrent_downloads, concurrent_subdirs))
        self.scrape_thread.start()

    def cancel_scraping_process(self):
        """Cancels the ongoing scraping process."""
        self.cancel_scraping = True
        self.update_output("Scraping canceled.")
        self.status_label.config(text="Scraping canceled.")

    def scrape(self, url, file_type, save_dir, max_files, concurrent_downloads, concurrent_subdirs):
        """Main scraping logic."""
        visited = set()
        to_visit = {url}
        base_domain = urlparse(url).netloc
        logging.info(f"Base domain extracted: {base_domain}")

        # Create a thread pool for downloading files
        with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_downloads) as executor:
            while to_visit and not self.cancel_scraping:
                current_url = to_visit.pop()
                if current_url in visited or not self.is_valid_url(current_url):
                    continue
                visited.add(current_url)

                headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
                    'Accept-Language': 'en-US,en;q=0.9',
                }

                try:
                    logging.info(f"Accessing: {current_url}")
                    self.update_output(f"Accessing: {current_url}")
                    response = requests.get(current_url, headers=headers)
                    response.raise_for_status()
                    soup = BeautifulSoup(response.text, 'html.parser')

                    # Find all links on the page
                    for link in soup.find_all('a', href=True):
                        href = link['href']
                        full_url = urljoin(current_url, href)

                        if urlparse(full_url).netloc == base_domain and full_url not in visited:
                            to_visit.add(full_url)

                            # If the link is a file, download it
                            if href.endswith(file_type):
                                executor.submit(self.download_file, full_url, self.get_file_path(full_url, save_dir))

                            # If the link is a directory, add it for concurrent searching
                            elif href.endswith('/'):
                                if len(to_visit) < max_files or max_files == 0:
                                    executor.submit(self.search_subdirectory, full_url, visited, to_visit, file_type, save_dir, max_files, concurrent_subdirs)

                except requests.RequestException as e:
                    logging.warning(f"Error accessing {current_url}: {e}")

            if self.cancel_scraping:
                logging.info("Scraping process was canceled by the user.")
            else:
                self.show_completion_message()

    def search_subdirectory(self, url, visited, to_visit, file_type, save_dir, max_files, concurrent_subdirs):
        """Searches for files in the subdirectory."""
        sub_visited = set()
        sub_to_visit = {url}

        with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_subdirs) as executor:
            while sub_to_visit and not self.cancel_scraping:
                current_url = sub_to_visit.pop()
                if current_url in sub_visited or not self.is_valid_url(current_url):
                    continue
                sub_visited.add(current_url)

                headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
                    'Accept-Language': 'en-US,en;q=0.9',
                }

                try:
                    logging.info(f"Accessing subdirectory: {current_url}")
                    response = requests.get(current_url, headers=headers)
                    response.raise_for_status()
                    soup = BeautifulSoup(response.text, 'html.parser')

                    # Find all links in the subdirectory
                    for link in soup.find_all('a', href=True):
                        href = link['href']
                        full_url = urljoin(current_url, href)

                        if urlparse(full_url).netloc == urlparse(url).netloc and full_url not in sub_visited:
                            sub_to_visit.add(full_url)

                            # If the link is a file, download it
                            if href.endswith(file_type):
                                executor.submit(self.download_file, full_url, self.get_file_path(full_url, save_dir))

                except requests.RequestException as e:
                    logging.warning(f"Error accessing {current_url}: {e}")

    def download_file(self, url, file_path):
        """Downloads a file from the given URL."""
        if self.cancel_scraping:
            return  # Exit early if cancellation is requested

        try:
            logging.info(f"Downloading file from: {url}")
            self.update_output(f"Downloading file from: {url}")

            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
                'Accept': 'application/octet-stream',
                'Accept-Language': 'en-US,en;q=0.9',
            }

            # Decode the URL to handle spaces and special characters
            decoded_url = requests.utils.unquote(url)

            response = requests.get(decoded_url, headers=headers)
            response.raise_for_status()

            logging.info(f"Saving to: {file_path}")
            os.makedirs(os.path.dirname(file_path), exist_ok=True)

            with open(file_path, 'wb') as file:
                file.write(response.content)

            logging.info(f"Downloaded: {file_path}")
            self.update_output(f"Downloaded: {file_path}")
        except Exception as e:
            logging.error(f"Failed to download {url}: {e}")
            self.update_output(f"Failed to download {url}: {e}")

    def get_file_path(self, url, save_dir):
        """Generates the file path for the downloaded file."""
        parsed_url = urlparse(url)
        path = os.path.join(save_dir, parsed_url.netloc, os.path.dirname(parsed_url.path.lstrip('/')))
        os.makedirs(path, exist_ok=True)
        return os.path.join(path, os.path.basename(requests.utils.unquote(url)))

    def is_valid_url(self, url):
        """Checks if the URL is valid."""
        parsed = urlparse(url)
        return (parsed.scheme in ["http", "https"] and 
                (parsed.port is None or parsed.port in {80, 443}))

    def update_output(self, message):
        """Updates the output text area with a new message."""
        self.master.after(0, lambda: self.output_text.configure(state=tk.NORMAL))
        self.master.after(0, lambda: self.output_text.insert(tk.END, message + "\n"))
        self.master.after(0, lambda: self.output_text.configure(state=tk.DISABLED))
        self.master.after(0, lambda: self.output_text.see(tk.END))

    def show_completion_message(self):
        """Displays a completion message when scraping is finished."""
        self.master.after(0, lambda: messagebox.showinfo("Scrape Complete", "Scraping finished."))
        self.status_label.config(text="Scraping complete.")

if __name__ == "__main__":
    root = tk.Tk()
    root.geometry("600x400")  # Adjust window size to fit the background image
    WebScraperGUI(root)
    root.mainloop()
