import tkinter as tk
from tkinter import simpledialog
import subprocess
from time import strftime
import sys
import os

from core.app_manager import AppManager
from core.config import load_applications

class ToolTip:
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tooltip_window = None
        self.widget.bind("<Enter>", self.show_tooltip)
        self.widget.bind("<Leave>", self.hide_tooltip)

    def show_tooltip(self, event=None):
        if self.tooltip_window or not self.text:
            return
        x = self.widget.winfo_rootx()
        y = self.widget.winfo_rooty() - 25
        self.tooltip_window = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        label = tk.Label(tw, text=self.text, justify='left',
                         background="#ffffe0", relief='solid', borderwidth=1,
                         font=("Arial", "9", "normal"))
        label.pack(ipadx=2, ipady=1)

    def hide_tooltip(self, event=None):
        tw = self.tooltip_window
        self.tooltip_window = None
        if tw:
            tw.destroy()

class Desktop(tk.Frame):
    def __init__(self, parent, settings):
        super().__init__(parent)
        self.parent = parent
        self.settings = settings
        self.pack(fill="both", expand=True)
        
        self.app_manager = AppManager(self)
        self.apps = load_applications()
        
        self.toasts = []
        
        # Desktop area
        import sys, os
        sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
        from core.theme import THEMES
        theme_name = self.settings.get("theme", "GrimOS")
        self.theme_data = THEMES.get(theme_name, THEMES["GrimOS"])
        bg_color = self.theme_data.get("desktop_bg", "gray")
        
        # Taskbar
        self.taskbar = tk.Frame(self, height=40, bg=self.theme_data.get("taskbar_bg", "#333"))
        self.taskbar.pack(side="bottom", fill="x")
        self.taskbar.pack_propagate(False)
        
        self.desktop_frame = tk.Frame(self, bg=bg_color)
        self.desktop_frame.pack(fill="both", expand=True)
        
        self.load_desktop_icons()
        
        # Load icons
        self.icons = {}
        icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icons")
        icon_theme = self.theme_data.get("icon_theme", "grimos")
        icon_theme_dir = os.path.join(icon_dir, "themes", icon_theme)
        
        for iname in ['menu_poweroff', 'menu_reboot', 'menu_logoff', 'btn_apply', 'btn_refresh', 'btn_kill', 'btn_start', 'btn_wifi', 'btn_usb', 'btn_help', 'menu_grimoire', 'btn_audio', 'btn_trash']:
            ipath = os.path.join(icon_theme_dir, iname + ".png")
            if not os.path.exists(ipath):
                ipath = os.path.join(icon_dir, iname + ".png")
            if os.path.exists(ipath):
                self.icons[iname] = tk.PhotoImage(file=ipath)
                
        # Load app icons
        self.app_icons = {}
        for app in self.apps:
            icon_path = app.get("icon")
            if icon_path:
                full_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), icon_path)
                if os.path.exists(full_path):
                    self.app_icons[app.get("module")] = tk.PhotoImage(file=full_path)

        # Start button
        self.start_btn = tk.Button(self.taskbar, image=self.icons.get("btn_start"),
                                   bg=self.theme_data.get("start_btn_bg", "blue"), 
                                   fg=self.theme_data.get("start_btn_fg", "white"), 
                                   command=self.toggle_start_menu)
        self.start_btn.pack(side="left", padx=5, pady=5)
        ToolTip(self.start_btn, "Démarrer")
        
        # Taskbar window buttons container
        self.taskbar_windows = tk.Frame(self.taskbar, bg=self.theme_data.get("taskbar_bg", "#333"))
        self.taskbar_windows.pack(side="left", fill="both", expand=True, padx=5)
        self.taskbar_btns = {}
        
        # Clock
        self.clock_lbl = tk.Label(self.taskbar, bg=self.theme_data.get("taskbar_bg", "#333"), fg=self.theme_data.get("taskbar_fg", "white"), font=("Arial", 10))
        self.clock_lbl.pack(side="right", padx=10)
        self.update_clock()
        
        # System Monitor
        self.sysmon_canvas = tk.Canvas(self.taskbar, height=24, bg=self.theme_data.get("taskbar_bg", "#333"), highlightthickness=0)
        self.sysmon_canvas.pack(side="right", padx=10, pady=3)
        
        self.last_cpu_total = 0
        self.last_cpu_idle = 0
        self.update_sysmon()
        
        # Keyboard Layout
        self.kb_layout = "fr"
        self.kb_btn = tk.Button(self.taskbar, text="FR", bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), font=("Arial", 10, "bold"), command=self.toggle_keyboard)
        self.kb_btn.pack(side="right", padx=5, pady=5)
        self.apply_keyboard_layout()
        


        # Wi-Fi Module
        from core.wifi import WifiManager
        self.wifi_manager = WifiManager(self)
        self.sudo_pwd = None
        self.wifi_btn = tk.Button(self.taskbar, image=self.icons.get("btn_wifi"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_wifi_menu)
        self.wifi_btn.pack(side="right", padx=5, pady=5)
        ToolTip(self.wifi_btn, "Wi-Fi")
        
        # USB Module
        self.usb_btn = tk.Button(self.taskbar, image=self.icons.get("btn_usb"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_usb_menu)
        self.usb_btn.pack(side="right", padx=5, pady=5)
        ToolTip(self.usb_btn, "Périphériques USB")
        self.known_unmounted_usbs = set()
        self.after(3000, self.update_usb)
        
        # Audio Module
        from core.audio import AudioManager
        self.audio_manager = AudioManager(self)
        self.audio_btn = tk.Button(self.taskbar, image=self.icons.get("btn_audio"), bg=self.theme_data.get("panel_btn_bg", "#555"), fg=self.theme_data.get("taskbar_fg", "white"), command=self.show_audio_menu)
        self.audio_btn.pack(side="right", padx=5, pady=5)
        ToolTip(self.audio_btn, "Contrôle du volume")

        # Start Menu (hidden by default)
        self.start_menu = tk.Frame(self.desktop_frame, bg="white", relief="raised", bd=2)
        self.start_menu_visible = False
        self.build_start_menu()
        
        # Hide menu when clicking desktop
        self.desktop_frame.bind("<ButtonPress-1>", self.hide_start_menu)
        
        # Context menu (Right click)
        self.context_menu = tk.Menu(self, tearoff=0)
        self.context_menu.add_command(label="Paramètres", command=lambda: self.app_manager.launch_app({"name": "Paramètres", "module": "apps.parametres.app"}))
        self.context_menu.add_separator()
        self.context_menu.add_command(label="Redémarrer GrimOS", command=self.shutdown_grimos)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="Annuler", command=lambda: None)
        
        def show_context_menu(event):
            try:
                self.context_menu.tk_popup(event.x_root, event.y_root)
            finally:
                self.context_menu.grab_release()
                
        self.desktop_frame.bind("<Button-3>", show_context_menu)
        
        # Restore previous session if any
        self.app_manager.restore_session()
        
        # Start power management
        self.after(5000, self.check_power)
        
        # Start clipboard sync
        self.last_prim = ""
        self.after(1000, self.sync_clipboards)
        
        # Start IPC notifications watcher
        self.after(2000, self.check_notifications)

    def check_notifications(self):
        notify_file = "/tmp/grimos_notify"
        if os.path.exists(notify_file):
            try:
                with open(notify_file, "r", encoding="utf-8") as f:
                    msg = f.read().strip()
                os.remove(notify_file)
                if msg:
                    if msg.startswith("POPUP:"):
                        from tkinter import messagebox
                        messagebox.showinfo("Message Système", msg[6:])
                    else:
                        self.show_toast(msg)
            except Exception:
                pass
        self.after(1000, self.check_notifications)

    def sync_clipboards(self):
        try:
            prim = self.selection_get(selection="PRIMARY")
            if prim and prim != self.last_prim:
                self.clipboard_clear()
                self.clipboard_append(prim)
                self.last_prim = prim
        except:
            pass
        self.after(500, self.sync_clipboards)

    def check_power(self):
        try:
            is_ac = True
            if os.path.exists('/sys/class/power_supply/AC/online'):
                with open('/sys/class/power_supply/AC/online', 'r') as f:
                    val = f.read().strip()
                    if val == '0':
                        is_ac = False
            elif os.path.exists('/sys/class/power_supply/ACAD/online'):
                with open('/sys/class/power_supply/ACAD/online', 'r') as f:
                    val = f.read().strip()
                    if val == '0':
                        is_ac = False
                        
            if is_ac:
                subprocess.run(['xset', 's', 'off'], capture_output=True)
                subprocess.run(['xset', '-dpms'], capture_output=True)
            else:
                res = subprocess.run(['xprintidle'], capture_output=True, text=True)
                if res.returncode == 0:
                    idle_ms = int(res.stdout.strip())
                    if idle_ms > 180000: # 3 minutes
                        pwd = self.settings.get("sudo_pwd", "")
                        subprocess.run(['sudo', '-S', 'poweroff'], input=pwd+'\n', text=True, capture_output=True)
        except Exception:
            pass
        self.after(10000, self.check_power)

    def update_clock(self):
        time_string = strftime('%H:%M')
        self.clock_lbl.config(text=time_string)
        self.after(1000, self.update_clock)

    def update_sysmon(self):
        try:
            with open('/proc/meminfo', 'r') as f:
                lines = f.readlines()
            mem = {}
            for line in lines:
                parts = line.split()
                if len(parts) >= 2:
                    mem[parts[0].strip(':')] = int(parts[1])
            ram_pct = int(100 * (1 - mem.get('MemAvailable', mem.get('MemFree', 0)) / mem.get('MemTotal', 1)))
            swap_tot = mem.get('SwapTotal', 0)
            swap_pct = int(100 * (1 - mem.get('SwapFree', 0) / swap_tot)) if swap_tot > 0 else 0

            with open('/proc/stat', 'r') as f:
                cpu_line = f.readline().split()
            cpu_idle = int(cpu_line[4]) + int(cpu_line[5])
            cpu_total = sum(int(x) for x in cpu_line[1:8])
            cpu_diff = cpu_total - self.last_cpu_total
            idle_diff = cpu_idle - self.last_cpu_idle
            cpu_pct = int(100 * (1 - idle_diff / cpu_diff)) if cpu_diff > 0 else 0
            self.last_cpu_total = cpu_total
            self.last_cpu_idle = cpu_idle

            graph_width = self.settings.get("cpu_graph_width", 10)
            if not hasattr(self, 'cpu_history'):
                self.cpu_history = [0] * graph_width
                self.ram_history = [0] * graph_width
                self.swap_history = [0] * graph_width
            
            while len(self.cpu_history) > graph_width:
                self.cpu_history.pop(0)
                self.ram_history.pop(0)
                self.swap_history.pop(0)
            while len(self.cpu_history) < graph_width:
                self.cpu_history.insert(0, 0)
                self.ram_history.insert(0, 0)
                self.swap_history.insert(0, 0)
                
            self.cpu_history.pop(0)
            self.cpu_history.append(cpu_pct)
            self.ram_history.pop(0)
            self.ram_history.append(ram_pct)
            self.swap_history.pop(0)
            self.swap_history.append(swap_pct)

            temp = 0
            if os.path.exists('/sys/class/thermal/thermal_zone0/temp'):
                with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
                    temp = int(int(f.read().strip()) / 1000)
                    
            cap = None
            status = ""
            try:
                for bat_dir in os.listdir('/sys/class/power_supply'):
                    if bat_dir.startswith('BAT') or bat_dir.startswith('macsmc-battery'):
                        with open(f'/sys/class/power_supply/{bat_dir}/capacity', 'r') as f:
                            cap = int(f.read().strip())
                        
                        with open(f'/sys/class/power_supply/{bat_dir}/status', 'r') as f:
                            stat_txt = f.read().strip()
                            if stat_txt == "Charging": status = "⚡"
                            elif stat_txt == "Discharging": status = "🔋"
                            elif stat_txt == "Full": status = "🔌"
                        break
            except Exception:
                pass

            self.sysmon_canvas.delete("all")
            bar_w = 3
            graph_px_width = graph_width * bar_w
            graph_h = 20
            pad_y = 2
            current_x = 5
            fg_color = self.theme_data.get("taskbar_fg", "white")

            self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
            for i, val in enumerate(self.cpu_history):
                h = int((val / 100) * graph_h)
                if h > 0:
                    self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="green", outline="")
            
            current_x += graph_px_width + 5
            self.sysmon_canvas.create_text(current_x, pad_y + graph_h//2, text=f"{temp:02d}°C", fill=fg_color, anchor="w", font=("Arial", 9))
            current_x += 35
            
            self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
            for i, val in enumerate(self.ram_history):
                h = int((val / 100) * graph_h)
                if h > 0:
                    self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="blue", outline="")
                    
            current_x += graph_px_width + 5
            
            self.sysmon_canvas.create_rectangle(current_x, pad_y, current_x + graph_px_width, pad_y + graph_h, outline="black")
            for i, val in enumerate(self.swap_history):
                h = int((val / 100) * graph_h)
                if h > 0:
                    self.sysmon_canvas.create_rectangle(current_x + i * bar_w, pad_y + graph_h - h, current_x + (i + 1) * bar_w, pad_y + graph_h, fill="red", outline="")
                    
            current_x += graph_px_width + 10
            
            if cap is not None:
                bat_w = 20
                bat_h = 10
                bat_y = pad_y + (graph_h - bat_h) // 2
                self.sysmon_canvas.create_rectangle(current_x, bat_y, current_x + bat_w, bat_y + bat_h, outline=fg_color)
                self.sysmon_canvas.create_rectangle(current_x + bat_w, bat_y + 3, current_x + bat_w + 2, bat_y + bat_h - 3, fill=fg_color, outline="")
                
                fill_w = int((cap / 100) * (bat_w - 2))
                if fill_w > 0:
                    self.sysmon_canvas.create_rectangle(current_x + 1, bat_y + 1, current_x + 1 + fill_w, bat_y + bat_h - 1, fill="orange", outline="")
                    
                current_x += bat_w + 5
                self.sysmon_canvas.create_text(current_x, pad_y + graph_h//2, text=f"{status} {cap}%", fill=fg_color, anchor="w", font=("Arial", 9))
                current_x += 45
            
            self.sysmon_canvas.config(width=current_x + 5)
        except Exception:
            pass
        self.after(1000, self.update_sysmon)

    def update_usb(self):
        pwd = self.settings.get("sudo_pwd")
        if pwd:
            try:
                res = subprocess.run(['lsblk', '-J', '-o', 'NAME,RM,MOUNTPOINT'], text=True, capture_output=True)
                import json
                data = json.loads(res.stdout)
                
                current_unmounted = set()
                
                for bd in data.get('blockdevices', []):
                    if bd.get('rm') in [True, '1', 1]:
                        children = bd.get('children', [])
                        if children:
                            for child in children:
                                if not child.get('mountpoint'):
                                    current_unmounted.add(child.get('name'))
                        else:
                            if not bd.get('mountpoint'):
                                current_unmounted.add(bd.get('name'))
                                
                # Find new unmounted drives
                new_drives = current_unmounted - self.known_unmounted_usbs
                for d in new_drives:
                    mnt_dir = f"/tmp/usb_{d}"
                    os.makedirs(mnt_dir, exist_ok=True)
                    subprocess.run(['sudo', '-S', 'mount', f'/dev/{d}', mnt_dir], input=pwd+'\n', text=True, capture_output=True)
                    
                self.known_unmounted_usbs = current_unmounted
            except:
                pass
        self.after(3000, self.update_usb)

    def show_usb_menu(self):
        pwd = self.settings.get("sudo_pwd")
        if not pwd:
            from tkinter import messagebox
            messagebox.showerror("Erreur", "Mot de passe Sudo non configuré dans Paramètres.")
            return
            
        try:
            res = subprocess.run(['lsblk', '-J', '-o', 'NAME,RM,MOUNTPOINT,SIZE'], text=True, capture_output=True)
            import json
            data = json.loads(res.stdout)
            
            mounted_devices = []
            for bd in data.get('blockdevices', []):
                if bd.get('rm') in [True, '1', 1]:
                    children = bd.get('children', [])
                    if children:
                        for child in children:
                            if child.get('mountpoint'):
                                mounted_devices.append(child)
                    else:
                        if bd.get('mountpoint'):
                            mounted_devices.append(bd)
                            
            if not mounted_devices:
                from tkinter import messagebox
                messagebox.showinfo("USB", "Aucun périphérique USB monté actuellement.")
                return
                
            if hasattr(self, 'usb_popup') and self.usb_popup.winfo_exists():
                self.usb_popup.destroy()
                return
                
            self.usb_popup = tk.Toplevel(self)
            self.usb_popup.title("Éjecter USB")
            self.usb_popup.geometry("300x200")
            
            self.usb_popup.update_idletasks()
            x = self.winfo_width() - 310
            y = self.winfo_height() - 250
            self.usb_popup.geometry(f"+{x}+{y}")
            self.usb_popup.transient(self.winfo_toplevel())
            
            tk.Label(self.usb_popup, text="Cliquez pour éjecter :", font=("Arial", 10, "bold")).pack(pady=5)
            
            for dev in mounted_devices:
                name = dev.get('name')
                mountpoint = dev.get('mountpoint')
                size = dev.get('size')
                
                def unmount(n=name, m=mountpoint):
                    r = subprocess.run(['sudo', '-S', 'umount', f'/dev/{n}'], input=pwd+'\n', text=True, capture_output=True)
                    from tkinter import messagebox
                    if r.returncode == 0:
                        self.known_unmounted_usbs.add(n)
                        subprocess.run(['sudo', '-S', 'rmdir', m], input=pwd+'\n', text=True)
                        messagebox.showinfo("USB", f"Le périphérique {n} a été éjecté en toute sécurité.")
                        if self.usb_popup.winfo_exists():
                            self.usb_popup.destroy()
                    else:
                        messagebox.showerror("Erreur", r.stderr)
                        
                tk.Button(self.usb_popup, text=f"⏏️ /dev/{name} ({size})\n{mountpoint}", command=unmount).pack(fill="x", padx=5, pady=2)
                
        except Exception as e:
            from tkinter import messagebox
            messagebox.showerror("Erreur", str(e))

    def show_audio_menu(self):
        if hasattr(self, 'audio_popup') and self.audio_popup.winfo_exists():
            self.audio_popup.destroy()
            return
            
        self.audio_popup = tk.Toplevel(self)
        self.audio_popup.overrideredirect(True)
        self.audio_popup.configure(bg="#222", bd=1, relief="solid")
        
        # Positionnement au-dessus du bouton
        x = self.audio_btn.winfo_rootx()
        y = self.audio_btn.winfo_rooty() - 230
        self.audio_popup.geometry(f"120x220+{x-30}+{y}")
        
        # Titre
        tk.Label(self.audio_popup, text="Volume", font=("Arial", 10, "bold"), bg="#222", fg="white").pack(pady=5)
        
        # Slider
        current_vol = self.audio_manager.get_volume()
        
        def on_volume_change(val):
            self.audio_manager.set_volume(int(val))
            
        vol_scale = tk.Scale(self.audio_popup, from_=100, to=0, orient="vertical", bg="#222", fg="white", highlightthickness=0, command=on_volume_change)
        vol_scale.set(current_vol)
        vol_scale.pack(fill="y", expand=True)
        
        # Bouton de test
        btn_test = tk.Button(self.audio_popup, text="Tester le son", bg="#4CAF50", fg="white", relief="flat", command=self.audio_manager.test_audio)
        btn_test.pack(pady=5, padx=10, fill="x")
        
        # Binding pour fermer quand on clique ailleurs
        def check_focus(e):
            if str(e.widget) not in str(self.audio_popup):
                self.audio_popup.destroy()
                
        # self.audio_popup.bind("<FocusOut>", lambda e: self.audio_popup.destroy())
        self.audio_popup.focus_set()

    def show_wifi_menu(self):
        if not self.wifi_manager.iface:
            from tkinter import messagebox
            messagebox.showerror("Wi-Fi", "Aucune interface Wi-Fi détectée sur ce système.")
            return

        pwd = self.settings.get("sudo_pwd")
        if not pwd:
            from tkinter import messagebox
            messagebox.showerror("Wi-Fi", "Veuillez configurer votre mot de passe système (Sudo) dans l'application Paramètres.")
            return
            
        if hasattr(self, 'wifi_popup') and self.wifi_popup.winfo_exists():
            self.wifi_popup.destroy()
            return
            
        self.wifi_popup = tk.Toplevel(self)
        self.wifi_popup.title("Réseaux Wi-Fi")
        self.wifi_popup.geometry("300x450")
        
        self.wifi_popup.update_idletasks()
        x = self.winfo_width() - 310
        y = self.winfo_height() - 500
        self.wifi_popup.geometry(f"+{x}+{y}")
        self.wifi_popup.transient(self.winfo_toplevel())
        
        ssid_actuel, ip_actuelle = self.wifi_manager.get_current_status(pwd)
        if ssid_actuel:
            info_frame = tk.Frame(self.wifi_popup, bg="#e0f7fa", bd=1, relief="solid")
            info_frame.pack(fill="x", padx=5, pady=5)
            tk.Label(info_frame, text=f"Connecté : {ssid_actuel}", font=("Arial", 10, "bold"), bg="#e0f7fa", fg="#006064").pack()
            if ip_actuelle:
                tk.Label(info_frame, text=f"IP : {ip_actuelle}", font=("Arial", 9), bg="#e0f7fa", fg="#00838f").pack()
        
        lbl = tk.Label(self.wifi_popup, text="Recherche de réseaux en cours...", font=("Arial", 10, "italic"))
        lbl.pack(pady=10)
        
        listbox = tk.Listbox(self.wifi_popup, font=("Arial", 10))
        listbox.pack(fill="both", expand=True, padx=5, pady=5)
        
        btn_connect = tk.Button(self.wifi_popup, text="Se connecter", state="disabled")
        btn_connect.pack(fill="x", padx=5, pady=5)
        
        networks_data = []

        def on_select(event):
            if listbox.curselection():
                btn_connect.config(state="normal")
                
        listbox.bind('<<ListboxSelect>>', on_select)

        def do_connect():
            sel = listbox.curselection()
            if not sel: return
            ssid = networks_data[sel[0]]['ssid']
            psk = simpledialog.askstring("Wi-Fi", f"Clé de sécurité pour '{ssid}' (vide si réseau ouvert) :")
            if psk is not None:
                lbl.config(text="Connexion en cours...")
                def connect_thread():
                    success, msg = self.wifi_manager.connect_network(pwd, ssid, psk)
                    def callback():
                        from tkinter import messagebox
                        if success:
                            messagebox.showinfo("Wi-Fi", f"Connecté à {ssid}.")
                            self.wifi_popup.destroy()
                        else:
                            messagebox.showerror("Erreur", msg)
                            lbl.config(text="Erreur.")
                    self.after(0, callback)
                import threading
                threading.Thread(target=connect_thread, daemon=True).start()

        btn_connect.config(command=do_connect)

        def scan_thread():
            nets = self.wifi_manager.scan_networks(pwd)
            def callback():
                if not self.wifi_popup.winfo_exists(): return
                lbl.config(text=f"{len(nets)} réseaux détectés.")
                listbox.delete(0, tk.END)
                networks_data.clear()
                for net in nets:
                    networks_data.append(net)
                    listbox.insert(tk.END, f"{net['ssid']} ({net['signal']} dBm)")
            self.after(0, callback)
            
        import threading
        threading.Thread(target=scan_thread, daemon=True).start()

    def toggle_keyboard(self):
        if self.kb_layout == "fr":
            self.kb_layout = "us"
            self.kb_btn.config(text="US")
        else:
            self.kb_layout = "fr"
            self.kb_btn.config(text="FR")
        self.apply_keyboard_layout()

    def apply_keyboard_layout(self):
        try:
            subprocess.run(["setxkbmap", self.kb_layout])
        except Exception as e:
            print(f"Erreur clavier: {e}")

    def build_start_menu(self):
        for widget in self.start_menu.winfo_children():
            widget.destroy()
            
        tk.Label(self.start_menu, text="GrimOS", bg="lightgray", font=("Arial", 10, "bold")).pack(fill="x", pady=2)
        
        self.start_menu_container = tk.Frame(self.start_menu, bg="white")
        self.start_menu_container.pack(fill="both", expand=True)
        
        self.start_menu_canvas = tk.Canvas(self.start_menu_container, bg="white", highlightthickness=0)
        self.start_menu_scrollbar = tk.Scrollbar(self.start_menu_container, orient="vertical", command=self.start_menu_canvas.yview)
        
        self.start_menu_content = tk.Frame(self.start_menu_canvas, bg="white")
        
        self.start_menu_content.bind(
            "<Configure>",
            lambda e: self.start_menu_canvas.configure(scrollregion=self.start_menu_canvas.bbox("all"))
        )
        
        self.start_menu_canvas.create_window((0, 0), window=self.start_menu_content, anchor="nw", width=230)
        self.start_menu_canvas.configure(yscrollcommand=self.start_menu_scrollbar.set)
        
        def _on_mousewheel(event):
            if self.start_menu_visible:
                self.start_menu_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        self.start_menu.bind_all("<MouseWheel>", _on_mousewheel)
        self.start_menu.bind_all("<Button-4>", lambda e: self.start_menu_canvas.yview_scroll(-1, "units") if self.start_menu_visible else None)
        self.start_menu.bind_all("<Button-5>", lambda e: self.start_menu_canvas.yview_scroll(1, "units") if self.start_menu_visible else None)
        
        categories = {}
        for app in self.apps:
            cat = app.get("category", "Autres")
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(app)
            
        for cat in sorted(categories.keys()):
            tk.Label(self.start_menu_content, text=f"── {cat} ──", bg="white", fg="gray", font=("Arial", 9, "italic")).pack(fill="x", pady=(5,0))
            for app in categories[cat]:
                icon_img = self.app_icons.get(app.get("module"))
                btn = tk.Button(self.start_menu_content, text=" " + app.get("name"), image=icon_img, compound="left", relief="flat", anchor="w", 
                                command=lambda a=app: self.launch_from_menu(a))
                btn.pack(fill="x", padx=10, pady=1)
            
        tk.Frame(self.start_menu_content, height=2, bg="black").pack(fill="x", pady=2)
        
        def generate_grimoire():
            self.hide_start_menu()
            import os
            from tkinter import messagebox
            grimoire_path = os.path.expanduser("~/Grimoire_GrimOS.md")
            
            content = "# Le Grimoire GrimOS\n\n"
            content += "Bienvenue dans GrimOS (Graphical Runtime Integrated Minimal Operating System).\n"
            content += "Ce document est généré automatiquement et compile la documentation du système.\n\n"
            
            content += "## 1. Créer et intégrer une application\n"
            content += "Pour créer une application compatible GrimOS, créez un fichier `.py` contenant :\n"
            content += "```python\nimport tkinter as tk\n"
            content += "def start(window, app_manager=None, **kwargs):\n"
            content += "    # 'window' est un tk.Frame pré-créé par GrimOS.\n"
            content += "    tk.Label(window, text=\"Mon Appli\").pack()\n```\n\n"
            content += "**Note de compatibilité :** Ne créez JAMAIS de `tk.Tk()` dans la fonction `start()`, cela casserait l'affichage sans gestionnaire de fenêtres.\n\n"
            content += "Ensuite, pour ajouter l'application au Menu Démarrer, éditez le fichier `config/applications.json` dans le dossier de GrimOS :\n"
            content += "```json\n{\n  \"name\": \"Mon App\",\n  \"module\": \"chemin.vers.le.module\",\n  \"category\": \"Autres\"\n}\n```\n"
            content += "Relancez la session, et votre application apparaîtra !\n\n"
            
            content += "## 2. Aides des Applications Intégrées\n"
            content += "### Éditeur de Code\n"
            content += "Conçu pour le développement natif. Utilisez le bouton 'Nouveau GUI' pour générer un code complet (boilerplate) pour démarrer une application GrimOS, testable même en standalone via son propre bloc `__main__`.\n\n"
            
            content += "### Explorateur\n"
            content += "Double-cliquez pour naviguer. Clic-droit pour renommer ou supprimer. La case 'Fichiers cachés' permet d'afficher les éléments systèmes. Le bouton `>_ Terminal` permet d'ouvrir une invite de commande dans le dossier visité.\n\n"
            
            content += "### Terminal\n"
            content += "Environnement émulé. Commandes courantes supportées avec historique. La commande `sudo` est volontairement bloquée par sécurité (utilisez l'application Xterm pour l'administration système pure).\n"
            
            try:
                with open(grimoire_path, "w", encoding="utf-8") as f:
                    f.write(content)
                self.app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=grimoire_path)
            except Exception as e:
                messagebox.showerror("Erreur", f"Impossible de générer le Grimoire : {e}")

        btn_help = tk.Button(self.start_menu_content, text=" Générer le Grimoire", image=self.icons.get('menu_grimoire'), compound="left", relief="flat", anchor="w", fg="blue", command=generate_grimoire)
        btn_help.pack(fill="x", padx=5, pady=2)
        
        tk.Frame(self.start_menu_content, height=1, bg="lightgray").pack(fill="x", pady=2)
        
        btn_logoff = tk.Button(self.start_menu_content, text=" Fermer la session", image=self.icons.get('menu_logoff'), compound="left", relief="flat", anchor="w", fg="black", command=self.shutdown_grimos)
        btn_logoff.pack(fill="x", padx=5, pady=2)
        
        btn_reboot = tk.Button(self.start_menu_content, text=" Redémarrer", image=self.icons.get('menu_reboot'), compound="left", relief="flat", anchor="w", fg="orange", command=self.reboot_system)
        btn_reboot.pack(fill="x", padx=5, pady=2)
        
        btn_poweroff = tk.Button(self.start_menu_content, text=" Arrêter", image=self.icons.get('menu_poweroff'), compound="left", relief="flat", anchor="w", fg="red", command=self.poweroff_system)
        btn_poweroff.pack(fill="x", padx=5, pady=2)

    def toggle_start_menu(self):
        if self.start_menu_visible:
            self.hide_start_menu()
        else:
            self.start_menu_content.update_idletasks()
            req_h = self.start_menu_content.winfo_reqheight() + 30 # Label de titre et bordures
            max_h = self.desktop_frame.winfo_height() - 20
            
            if req_h > max_h:
                self.start_menu_scrollbar.pack(side="right", fill="y")
                self.start_menu_canvas.pack(side="left", fill="both", expand=True)
                final_h = max_h
            else:
                self.start_menu_scrollbar.pack_forget()
                self.start_menu_canvas.pack(side="left", fill="both", expand=True)
                final_h = req_h
                
            self.start_menu.place(x=5, y=self.desktop_frame.winfo_height() - final_h - 5, height=final_h, width=250)
            self.start_menu.lift()
            self.start_menu_visible = True

    def hide_start_menu(self, event=None):
        if self.start_menu_visible:
            self.start_menu.place_forget()
            self.start_menu_visible = False

    def launch_from_menu(self, app):
        self.hide_start_menu()
        self.app_manager.launch_app(app)

    def register_window(self, win):
        if win not in self.taskbar_btns:
            icon_img = self.app_icons.get(win.app_config.get("module"))
            tooltip_text = win.filepath if hasattr(win, "filepath") and win.filepath else win.title
            btn = tk.Button(self.taskbar_windows, image=icon_img, bg="#444", fg="white", 
                            command=lambda: self.toggle_window(win))
            btn.pack(side="left", padx=2, pady=5)
            self.taskbar_btns[win] = btn
            ToolTip(btn, tooltip_text)

    def update_taskbar_btn(self, win):
        if win in self.taskbar_btns:
            if not win.winfo_ismapped():
                # Fenêtre minimisée -> fond plus sombre ou différent pour l'indiquer
                self.taskbar_btns[win].config(bg="#888", relief="sunken")
            else:
                self.taskbar_btns[win].config(bg="#444", relief="raised")

    def toggle_window(self, win):
        if win.winfo_ismapped():
            win.minimize()
        else:
            win.restore()

    def remove_window(self, win):
        if win in self.taskbar_btns:
            self.taskbar_btns[win].destroy()
            del self.taskbar_btns[win]
        if win in self.app_manager.active_windows:
            self.app_manager.active_windows.remove(win)

    def save_and_quit(self):
        self.app_manager.save_session()
        self.parent.destroy()

    def shutdown_grimos(self):
        self.save_and_quit()
        
    def reboot_system(self):
        from tkinter import messagebox
        pwd = self.settings.get("sudo_pwd")
        if not pwd:
            messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système (Sudo) dans les Paramètres.")
            return
        if pwd is not None:
            self.app_manager.save_session()
            try:
                res = subprocess.run(['sudo', '-S', 'systemctl', 'reboot'], input=pwd + '\n', text=True, capture_output=True)
                if res.returncode != 0:
                    messagebox.showerror("Erreur", f"Échec de la commande:\n{res.stderr}")
            except Exception as e:
                print(f"Erreur reboot: {e}")
        
    def poweroff_system(self):
        from tkinter import messagebox
        pwd = self.settings.get("sudo_pwd")
        if not pwd:
            messagebox.showerror("Erreur", "Veuillez configurer votre mot de passe système (Sudo) dans les Paramètres.")
            return
        if pwd is not None:
            self.app_manager.save_session()
            try:
                res = subprocess.run(['sudo', '-S', 'systemctl', 'poweroff'], input=pwd + '\n', text=True, capture_output=True)
                if res.returncode != 0:
                    messagebox.showerror("Erreur", f"Échec de la commande:\n{res.stderr}")
            except Exception as e:
                print(f"Erreur poweroff: {e}")

    def show_toast(self, message):
        toast = tk.Label(self.desktop_frame, text=message, bg="#333", fg="white", font=("Arial", 10, "bold"), padx=15, pady=10, relief="solid", bd=1)
        
        y_pos = -10
        for t in self.toasts:
            self.desktop_frame.update_idletasks()
            y_pos -= t.winfo_reqheight() + 5
            
        toast.place(relx=1.0, rely=1.0, x=-10, y=y_pos, anchor="se")
        self.toasts.append(toast)
        
        def remove_toast():
            if toast in self.toasts:
                self.toasts.remove(toast)
            toast.destroy()
            
            current_y = -10
            for t in self.toasts:
                self.desktop_frame.update_idletasks()
                t.place(relx=1.0, rely=1.0, x=-10, y=current_y, anchor="se")
                current_y -= t.winfo_reqheight() + 5
                
        self.after(4000, remove_toast)

    def load_desktop_icons(self):
        import json
        desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "desktop.json")
        icon_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "icons")
        icon_theme = self.theme_data.get("icon_theme", "grimos")
        icon_theme_dir = os.path.join(icon_dir, "themes", icon_theme)
        
        if not os.path.exists(desktop_config_path):
            try:
                with open(desktop_config_path, "w") as f:
                    json.dump([], f)
            except:
                pass
        
        for w in self.desktop_frame.winfo_children():
            if getattr(w, "_is_desktop_icon", False):
                w.destroy()
                
        try:
            with open(desktop_config_path, "r") as f:
                icons = json.load(f)
                
            for icon_data in icons:
                name = icon_data.get("name", "Fichier")
                path = icon_data.get("path", "")
                x = icon_data.get("x", 50)
                y = icon_data.get("y", 50)
                
                img_name = "blocnotes.png"
                if os.path.isdir(path):
                    img_name = "explorateur.png"
                elif path.endswith(".py"):
                    img_name = "terminal.png"
                    
                img_path = os.path.join(icon_theme_dir, img_name)
                if not os.path.exists(img_path):
                    img_path = os.path.join(icon_dir, img_name)
                    
                try:
                    photo = tk.PhotoImage(file=img_path)
                except:
                    photo = None
                
                # Truncate long names
                disp_name = name if len(name) < 15 else name[:12]+"..."
                
                # Use theme colors for icon label
                fg_color = self.theme_data.get("desktop_fg", "white")
                
                btn = tk.Button(self.desktop_frame, text=disp_name, image=photo, compound="top", relief="flat", bg=self.desktop_frame.cget("bg"), fg=fg_color, justify="center", width=80, command=lambda p=path: self.launch_file(p))
                btn.image = photo # Keep reference
                btn._is_desktop_icon = True
                btn.place(x=x, y=y)
                
                def make_draggable_and_menu(widget, data, icon_list):
                    def start_drag(event):
                        widget._drag_start_x = event.x
                        widget._drag_start_y = event.y
                    def do_drag(event):
                        nx = widget.winfo_x() - widget._drag_start_x + event.x
                        ny = widget.winfo_y() - widget._drag_start_y + event.y
                        widget.place(x=nx, y=ny)
                    def stop_drag(event):
                        # Don't register drag if the widget was just clicked
                        if getattr(widget, "_is_menu_open", False): return
                        data["x"] = widget.winfo_x()
                        data["y"] = widget.winfo_y()
                        self.save_desktop_icons(icon_list)
                    
                    def show_menu(event):
                        menu = tk.Menu(self.desktop_frame, tearoff=0)
                        
                        def delete_shortcut():
                            if data in icon_list:
                                icon_list.remove(data)
                                self.save_desktop_icons(icon_list)
                                widget.destroy()
                                self.show_toast(f"Raccourci supprimé")
                                
                        menu.add_command(label=" Supprimer le raccourci", image=self.icons.get('btn_trash'), compound="left", command=delete_shortcut)
                        
                        widget._is_menu_open = True
                        try:
                            menu.tk_popup(event.x_root, event.y_root)
                        finally:
                            menu.grab_release()
                            widget._is_menu_open = False
                            
                    widget.bind("<Button-1>", start_drag)
                    widget.bind("<B1-Motion>", do_drag)
                    widget.bind("<ButtonRelease-1>", stop_drag)
                    widget.bind("<Button-3>", show_menu)
                    
                make_draggable_and_menu(btn, icon_data, icons)
        except Exception as e:
            print("Erreur desktop icons:", e)

    def save_desktop_icons(self, icons):
        import json
        desktop_config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "desktop.json")
        try:
            with open(desktop_config_path, "w") as f:
                json.dump(icons, f, indent=4)
        except:
            pass
            
    def launch_file(self, path):
        if not os.path.exists(path):
            self.show_toast(f"Fichier introuvable:\n{path}")
            return
            
        if os.path.isdir(path):
            self.app_manager.launch_app({"name": "Explorateur", "module": "apps.explorateur.app"}, filepath=path)
        elif path.endswith(".py"):
            self.app_manager.launch_app({"name": "Éditeur de Code", "module": "apps.editeur.app"}, filepath=path)
        elif path.endswith((".png", ".jpg", ".gif")):
            self.app_manager.launch_app({"name": "Visionneuse", "module": "apps.visionneuse.app"}, filepath=path)
        else:
            self.app_manager.launch_app({"name": "Bloc-notes", "module": "apps.blocnotes.app"}, filepath=path)
