#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import traceback
from pathlib import Path

import cairo
import gi

gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("GdkPixbuf", "2.0")
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Pango


APP_ID = "br.com.escolacelita.StartMenu"
DATA_DIR = Path.home() / ".local" / "share" / "celita-menu"
USAGE_FILE = DATA_DIR / "usage.json"
THEME_FILE = Path.home() / ".config" / "celita" / "theme"

DEFAULT_APPS = [
    (["brave-browser.desktop", "brave-browser-stable.desktop", "google-chrome.desktop", "firefox-esr.desktop"], "Navegador"),
    (["celita-explorer.desktop", "thunar.desktop"], "Explorador"),
    (["libreoffice-startcenter.desktop"], "LibreOffice"),
    (["libreoffice-writer.desktop"], "Writer"),
    (["libreoffice-calc.desktop"], "Calc"),
    (["libreoffice-impress.desktop"], "Impress"),
    (["com.github.maoschanz.drawing.desktop", "org.kde.kolourpaint.desktop"], "Desenho"),
    (["org.gnome.Evince.desktop", "evince.desktop"], "Documentos PDF"),
    (["galculator.desktop", "org.gnome.Calculator.desktop"], "Calculadora"),
    (["org.xfce.mousepad.desktop", "mousepad.desktop"], "Bloco de Notas"),
    (["org.kde.gcompris.desktop"], "GCompris"),
    (["brainparty.desktop"], "Desafios de Lógica"),
]

CATEGORIES = [
    ("Início", "go-home-symbolic", None),
    ("Todos os aplicativos", "view-grid-symbolic", "all"),
    ("Internet", "web-browser-symbolic", "Network;WebBrowser"),
    ("Escritório", "x-office-document-symbolic", "Office"),
    ("Educação", "applications-education-symbolic", "Education"),
    ("Multimídia", "applications-multimedia-symbolic", "AudioVideo;Graphics"),
    ("Jogos", "applications-games-symbolic", "Game"),
    ("Acessórios", "applications-utilities-symbolic", "Utility;TextEditor"),
    ("Sistema", "preferences-system-symbolic", "System;Settings"),
]


def load_usage():
    try:
        value = json.loads(USAGE_FILE.read_text(encoding="utf-8"))
        return value if isinstance(value, dict) else {}
    except (OSError, ValueError):
        return {}


def save_usage(data):
    try:
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        temporary = USAGE_FILE.with_suffix(".tmp")
        temporary.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
        temporary.replace(USAGE_FILE)
    except OSError:
        pass


def dark_theme_enabled():
    try:
        saved = THEME_FILE.read_text(encoding="utf-8").strip().lower()
        if saved in ("light", "dark"):
            return saved == "dark"
    except OSError:
        pass
    theme_name = Gtk.Settings.get_default().get_property("gtk-theme-name") or ""
    return "dark" in theme_name.lower()


def desktop_app(desktop_id):
    try:
        return Gio.DesktopAppInfo.new(desktop_id)
    except (GLib.Error, TypeError):
        return None


def icon_widget(icon, size):
    if icon:
        image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.DIALOG)
        image.set_pixel_size(size)
        return image
    image = Gtk.Image.new_from_icon_name("application-x-executable", Gtk.IconSize.DIALOG)
    image.set_pixel_size(size)
    return image


class StartMenuWindow(Gtk.ApplicationWindow):
    def __init__(self, application):
        super().__init__(application=application)
        self.application = application
        self.usage = load_usage()
        self.apps = self.collect_apps()
        self.current_view = "home"
        self.allow_focus_close = False
        self.set_title("Menu Iniciar")
        self.set_decorated(False)
        self.set_resizable(False)
        self.set_skip_taskbar_hint(True)
        self.set_skip_pager_hint(True)
        self.set_keep_above(True)
        self.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU)
        self.set_app_paintable(True)
        self.set_name("celita-start-window")

        screen = self.get_screen()
        visual = screen.get_rgba_visual()
        if visual and screen.is_composited():
            self.set_visual(visual)

        self.connect("draw", self.on_draw)
        self.connect("key-press-event", self.on_key_press)
        self.connect("delete-event", self.on_delete)
        self.connect("focus-out-event", self.on_focus_out)

        self.provider = Gtk.CssProvider()
        self.provider.load_from_data(self.css().encode("utf-8"))
        Gtk.StyleContext.add_provider_for_screen(
            screen, self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        self.shell = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        self.shell.get_style_context().add_class("menu-shell")
        self.add(self.shell)
        self.build_left_pane()
        self.build_right_pane()
        self.render_home()

    @staticmethod
    def css():
        if dark_theme_enabled():
            return r"""
            #celita-start-window { background-color: transparent; }
            .menu-shell {
                border-radius: 12px;
                box-shadow: 0 12px 34px rgba(0, 0, 0, 0.48);
                background-color: rgba(30, 32, 35, 0.99);
            }
            .menu-left {
                background-color: rgba(37, 40, 44, 0.98);
                border-radius: 12px 0 0 12px;
                color: #f0f2f4;
                border: 1px solid rgba(255,255,255,0.10);
                border-right: 0;
            }
            .menu-right {
                background-color: rgba(29, 31, 34, 0.99);
                border-radius: 0 12px 12px 0;
                color: #f1f3f5;
                border: 1px solid rgba(255,255,255,0.09);
                border-left: 0;
            }
            .menu-title, .user-name, .section-title, .view-header { color: #f4f5f7; }
            .menu-title { font-weight: 700; font-size: 15px; }
            .user-name { font-weight: 700; font-size: 16px; }
            .user-subtitle { font-size: 11px; color: #a9afb7; }
            .section-title, .view-header { font-weight: 700; font-size: 17px; }
            .category-button, .power-button, .theme-button, .all-apps-button {
                border: 0; box-shadow: none; background-image: none;
                background-color: transparent; border-radius: 7px; color: #e6e9ed;
            }
            .category-button { padding: 8px 11px; }
            .category-button:hover, .category-button:focus,
            .power-button:hover, .theme-button:hover, .all-apps-button:hover {
                background-color: rgba(255,255,255,0.09);
            }
            .category-button.active { background-color: rgba(255,255,255,0.12); color: #62b7f0; }
            .search-entry {
                min-height: 44px; border-radius: 8px; border: 1px solid #45494f;
                background-color: #292c30; color: #f3f4f5; caret-color: #f3f4f5;
                box-shadow: none; padding: 0 13px; font-size: 14px;
            }
            .app-tile, .folder-tile, .recent-tile {
                border: 1px solid transparent; box-shadow: none; background-image: none;
                background-color: transparent; border-radius: 8px; padding: 8px 5px;
            }
            .app-tile:hover, .folder-tile:hover, .recent-tile:hover {
                border-color: #45494f; background-color: #292c30;
            }
            .app-name { font-size: 11px; color: #c6cbd1; }
            .folder-name { font-weight: 600; font-size: 11px; color: #e1e4e8; }
            .folder-detail, .recent-detail { font-size: 9px; color: #959ba4; }
            .divider { background-color: rgba(255,255,255,0.13); min-height: 1px; }
            .list-app-button {
                border: 0; box-shadow: none; background-image: none;
                background-color: transparent; border-radius: 7px; padding: 8px;
            }
            .list-app-button:hover { background-color: #292c30; }
            .list-app-name { font-weight: 600; font-size: 12px; color: #e2e5e8; }
            .list-app-description { font-size: 10px; color: #9da3ab; }
            scrollbar slider { min-width: 6px; min-height: 34px; border-radius: 6px; background: #5a6068; }
            """
        return r"""
        #celita-start-window { background-color: transparent; }
        .menu-shell {
            border-radius: 12px;
            box-shadow: 0 12px 34px rgba(26, 39, 58, 0.30);
            background-color: rgba(255, 255, 255, 0.985);
        }
        .menu-left {
            background-color: rgba(238, 241, 247, 0.90);
            border-radius: 12px 0 0 12px;
            color: #252a33;
            border: 1px solid rgba(255,255,255,0.58);
            border-right: 0;
        }
        .menu-right {
            background-color: rgba(255, 255, 255, 0.985);
            border-radius: 0 12px 12px 0;
            color: #20242b;
            border: 1px solid rgba(218,222,230,0.92);
            border-left: 0;
        }
        .menu-title { font-weight: 700; font-size: 15px; color: #20242b; }
        .user-name { font-weight: 700; font-size: 16px; color: #252a33; }
        .user-subtitle { font-size: 11px; color: #7c828c; }
        .section-title { font-weight: 700; font-size: 17px; color: #22262e; }
        .category-button, .power-button, .theme-button, .all-apps-button {
            border: 0;
            box-shadow: none;
            background-image: none;
            background-color: transparent;
            border-radius: 7px;
            color: #31363f;
        }
        .category-button { padding: 8px 11px; }
        .category-button:hover, .category-button:focus,
        .power-button:hover, .theme-button:hover, .all-apps-button:hover {
            background-color: rgba(255,255,255,0.68);
        }
        .category-button.active { background-color: rgba(255,255,255,0.90); color: #0878d1; }
        .search-entry {
            min-height: 44px;
            border-radius: 8px;
            border: 1px solid #eceaf1;
            background-color: #f5f2f8;
            box-shadow: none;
            padding: 0 13px;
            font-size: 14px;
        }
        .app-tile, .folder-tile, .recent-tile {
            border: 1px solid transparent;
            box-shadow: none;
            background-image: none;
            background-color: transparent;
            border-radius: 8px;
            padding: 8px 5px;
        }
        .app-tile:hover, .folder-tile:hover, .recent-tile:hover {
            border-color: #e7e9ee;
            background-color: #f6f7f9;
        }
        .app-name { font-size: 11px; color: #5f646d; }
        .folder-name { font-weight: 600; font-size: 11px; color: #30353c; }
        .folder-detail, .recent-detail { font-size: 9px; color: #979ca5; }
        .divider { background-color: rgba(130,136,146,0.22); min-height: 1px; }
        .view-header { font-weight: 700; font-size: 17px; color: #242830; }
        .list-app-button {
            border: 0; box-shadow: none; background-image: none;
            background-color: transparent; border-radius: 7px; padding: 8px;
        }
        .list-app-button:hover { background-color: #f4f5f7; }
        .list-app-name { font-weight: 600; font-size: 12px; color: #30343c; }
        .list-app-description { font-size: 10px; color: #8a8f98; }
        scrollbar slider { min-width: 6px; min-height: 34px; border-radius: 6px; background: #c9cdd3; }
        """

    @staticmethod
    def collect_apps():
        apps = []
        seen = set()
        for app in Gio.AppInfo.get_all():
            try:
                if not app.should_show():
                    continue
                desktop_id = app.get_id() or app.get_executable() or app.get_name()
                if not desktop_id or desktop_id in seen:
                    continue
                seen.add(desktop_id)
                apps.append(app)
            except (AttributeError, GLib.Error):
                continue
        apps.sort(key=lambda item: (item.get_display_name() or item.get_name()).casefold())
        return apps

    def build_left_pane(self):
        left_frame = Gtk.EventBox()
        left_frame.set_size_request(244, -1)
        left_frame.get_style_context().add_class("menu-left")
        self.shell.pack_start(left_frame, False, False, 0)

        left = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        left.set_border_width(18)
        left_frame.add(left)

        title = Gtk.Label(label="Menu Iniciar", xalign=0)
        title.get_style_context().add_class("menu-title")
        left.pack_start(title, False, False, 5)

        user_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        user_box.set_margin_top(20)
        user_box.set_margin_bottom(13)
        avatar = self.avatar_widget()
        user_box.pack_start(avatar, False, False, 0)
        labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1)
        username = os.environ.get("USER", "Usuário")
        display_name = "Professor" if username == "professor" else "Aluno" if username == "aluno" else username.title()
        name = Gtk.Label(label=display_name, xalign=0)
        name.get_style_context().add_class("user-name")
        subtitle = Gtk.Label(label="Escola Celita", xalign=0)
        subtitle.get_style_context().add_class("user-subtitle")
        labels.pack_start(name, False, False, 0)
        labels.pack_start(subtitle, False, False, 0)
        user_box.pack_start(labels, True, True, 0)
        left.pack_start(user_box, False, False, 0)

        self.category_buttons = []
        for index, (label, icon_name, token) in enumerate(CATEGORIES):
            button = Gtk.Button()
            button.set_relief(Gtk.ReliefStyle.NONE)
            button.get_style_context().add_class("category-button")
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=11)
            icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
            icon.set_pixel_size(17)
            row.pack_start(icon, False, False, 0)
            row.pack_start(Gtk.Label(label=label, xalign=0), True, True, 0)
            button.add(row)
            button.connect("clicked", self.on_category, token, label)
            left.pack_start(button, False, False, 0)
            self.category_buttons.append(button)
            if index == 0:
                button.get_style_context().add_class("active")

        left.pack_start(Gtk.Box(), True, True, 0)
        separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        separator.get_style_context().add_class("divider")
        left.pack_start(separator, False, False, 8)

        theme = Gtk.Button()
        theme.set_relief(Gtk.ReliefStyle.NONE)
        theme.get_style_context().add_class("theme-button")
        theme_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        dark = dark_theme_enabled()
        theme_icon = "weather-clear-night-symbolic" if dark else "weather-clear-symbolic"
        theme_name = "Escuro" if dark else "Claro"
        self.theme_icon = Gtk.Image.new_from_icon_name(theme_icon, Gtk.IconSize.MENU)
        self.theme_label = Gtk.Label(label=f"Tema: {theme_name}", xalign=0)
        theme_row.pack_start(self.theme_icon, False, False, 0)
        theme_row.pack_start(self.theme_label, True, True, 0)
        theme.add(theme_row)
        theme.connect("clicked", self.toggle_theme)
        left.pack_start(theme, False, False, 0)

        power = Gtk.Button()
        power.set_relief(Gtk.ReliefStyle.NONE)
        power.get_style_context().add_class("power-button")
        power_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        power_row.pack_start(Gtk.Image.new_from_icon_name("system-shutdown-symbolic", Gtk.IconSize.MENU), False, False, 0)
        power_row.pack_start(Gtk.Label(label="Energia", xalign=0), True, True, 0)
        power_row.pack_end(Gtk.Image.new_from_icon_name("pan-end-symbolic", Gtk.IconSize.MENU), False, False, 0)
        power.add(power_row)
        power.connect("clicked", self.show_power_menu)
        left.pack_end(power, False, False, 0)
        self.power_button = power

    def toggle_theme(self, _button):
        try:
            subprocess.run(
                ["/usr/local/bin/celita-theme", "alternar"],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            self.refresh_theme()
            self.hide()
        except (OSError, subprocess.CalledProcessError) as error:
            self.show_error("Não foi possível alterar o tema", str(error))

    def refresh_theme(self):
        dark = dark_theme_enabled()
        self.provider.load_from_data(self.css().encode("utf-8"))
        self.theme_icon.set_from_icon_name(
            "weather-clear-night-symbolic" if dark else "weather-clear-symbolic",
            Gtk.IconSize.MENU,
        )
        self.theme_label.set_text("Tema: Escuro" if dark else "Tema: Claro")

    def avatar_widget(self):
        face = Path.home() / ".face"
        if face.is_file():
            try:
                picture = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(face), 42, 42, True)
                image = Gtk.Image.new_from_pixbuf(picture)
                return image
            except (GLib.Error, OSError):
                pass
        image = Gtk.Image.new_from_icon_name("avatar-default", Gtk.IconSize.DIALOG)
        image.set_pixel_size(42)
        return image

    def build_right_pane(self):
        self.right = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.right.set_size_request(520, -1)
        self.right.get_style_context().add_class("menu-right")
        self.shell.pack_start(self.right, True, True, 0)

        header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        header.set_margin_top(22)
        header.set_margin_start(42)
        header.set_margin_end(34)
        header.set_margin_bottom(16)
        self.search = Gtk.SearchEntry()
        self.search.set_placeholder_text("Pesquisar aplicativos e arquivos")
        self.search.get_style_context().add_class("search-entry")
        self.search.connect("search-changed", self.on_search_changed)
        header.pack_start(self.search, True, True, 0)
        self.right.pack_start(header, False, False, 0)

        self.scroll = Gtk.ScrolledWindow()
        self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        self.scroll.set_shadow_type(Gtk.ShadowType.NONE)
        self.content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.content.set_margin_start(42)
        self.content.set_margin_end(34)
        self.content.set_margin_bottom(10)
        self.scroll.add(self.content)
        self.right.pack_start(self.scroll, True, True, 0)

        bottom_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        bottom_separator.get_style_context().add_class("divider")
        self.right.pack_start(bottom_separator, False, False, 0)
        self.all_apps_button = Gtk.Button()
        self.all_apps_button.set_relief(Gtk.ReliefStyle.NONE)
        self.all_apps_button.get_style_context().add_class("all-apps-button")
        bottom = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        bottom.pack_start(Gtk.Image.new_from_icon_name("view-list-symbolic", Gtk.IconSize.MENU), False, False, 0)
        self.all_apps_label = Gtk.Label(label="Todos os aplicativos", xalign=0)
        bottom.pack_start(self.all_apps_label, True, True, 0)
        self.all_apps_button.add(bottom)
        self.all_apps_button.set_margin_start(34)
        self.all_apps_button.set_margin_end(26)
        self.all_apps_button.set_margin_top(7)
        self.all_apps_button.set_margin_bottom(7)
        self.all_apps_button.connect("clicked", self.toggle_all_apps)
        self.right.pack_end(self.all_apps_button, False, False, 0)

    def clear_content(self):
        for child in self.content.get_children():
            self.content.remove(child)

    def header_label(self, text):
        label = Gtk.Label(label=text, xalign=0)
        label.get_style_context().add_class("section-title")
        label.set_margin_bottom(9)
        return label

    def default_app_entries(self):
        entries = []
        seen = set()
        for priority, (ids, label) in enumerate(DEFAULT_APPS):
            app = next((desktop_app(value) for value in ids if desktop_app(value)), None)
            if not app:
                continue
            app_id = app.get_id() or app.get_executable()
            if app_id in seen:
                continue
            seen.add(app_id)
            score = int(self.usage.get(app_id, 0))
            entries.append((score, -priority, app, label))

        for app in self.apps:
            app_id = app.get_id() or app.get_executable()
            if app_id in seen or not self.usage.get(app_id):
                continue
            entries.append((int(self.usage[app_id]), -999, app, app.get_display_name()))

        entries.sort(key=lambda row: (row[0], row[1]), reverse=True)
        return [(app, label) for _, _, app, label in entries[:12]]

    def render_home(self):
        self.current_view = "home"
        self.clear_content()
        self.set_active_category(0)
        self.all_apps_label.set_text("Todos os aplicativos")
        self.content.pack_start(self.header_label("Aplicativos mais usados"), False, False, 0)

        grid = Gtk.Grid(column_spacing=8, row_spacing=4)
        grid.set_column_homogeneous(True)
        for index, (app, label) in enumerate(self.default_app_entries()):
            grid.attach(self.app_tile(app, label), index % 4, index // 4, 1, 1)
        self.content.pack_start(grid, False, False, 0)

        divider = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        divider.get_style_context().add_class("divider")
        divider.set_margin_top(10)
        divider.set_margin_bottom(12)
        self.content.pack_start(divider, False, False, 0)
        self.content.pack_start(self.header_label("Acesso rápido"), False, False, 0)

        quick = Gtk.Grid(column_spacing=8, row_spacing=7)
        quick.set_column_homogeneous(True)
        folders = [
            ("Área de Trabalho", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP), "user-desktop"),
            ("Documentos", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS), "folder-documents"),
            ("Downloads", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD), "folder-download"),
            ("Imagens", GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES), "folder-pictures"),
        ]
        for index, (label, path, icon_name) in enumerate(folders):
            if path:
                quick.attach(self.folder_tile(label, Path(path), icon_name), index, 0, 1, 1)

        recent_count = 0
        for item in Gtk.RecentManager.get_default().get_items():
            if recent_count >= 4:
                break
            try:
                uri = item.get_uri()
                if not uri.startswith("file://") or not item.exists():
                    continue
                quick.attach(self.recent_tile(item), recent_count, 1, 1, 1)
                recent_count += 1
            except GLib.Error:
                continue

        self.content.pack_start(quick, False, False, 0)
        self.content.show_all()

    def app_tile(self, app, custom_label=None):
        button = Gtk.Button()
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.get_style_context().add_class("app-tile")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
        box.pack_start(icon_widget(app.get_icon(), 42), False, False, 0)
        text = custom_label or app.get_display_name() or app.get_name()
        label = Gtk.Label(label=text)
        label.set_ellipsize(Pango.EllipsizeMode.END)
        label.set_max_width_chars(14)
        label.get_style_context().add_class("app-name")
        box.pack_start(label, False, False, 0)
        button.add(box)
        button.set_tooltip_text(app.get_description() or text)
        button.connect("clicked", lambda _button: self.launch_app(app))
        return button

    def folder_tile(self, label, path, icon_name):
        button = Gtk.Button()
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.get_style_context().add_class("folder-tile")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
        image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
        image.set_pixel_size(35)
        box.pack_start(image, False, False, 0)
        name = Gtk.Label(label=label)
        name.get_style_context().add_class("folder-name")
        box.pack_start(name, False, False, 0)
        detail = Gtk.Label(label="Pasta pessoal")
        detail.get_style_context().add_class("folder-detail")
        box.pack_start(detail, False, False, 0)
        button.add(box)
        button.connect("clicked", lambda _button: self.open_uri(path.as_uri()))
        return button

    def recent_tile(self, item):
        button = Gtk.Button()
        button.set_relief(Gtk.ReliefStyle.NONE)
        button.get_style_context().add_class("recent-tile")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
        icon = item.get_gicon()
        box.pack_start(icon_widget(icon, 34), False, False, 0)
        label = Gtk.Label(label=item.get_display_name())
        label.set_ellipsize(Pango.EllipsizeMode.END)
        label.set_max_width_chars(13)
        label.get_style_context().add_class("folder-name")
        box.pack_start(label, False, False, 0)
        detail = Gtk.Label(label="Arquivo recente")
        detail.get_style_context().add_class("recent-detail")
        box.pack_start(detail, False, False, 0)
        button.add(box)
        button.connect("clicked", lambda _button: self.open_uri(item.get_uri()))
        return button

    def render_apps(self, title, category=None, query=""):
        self.current_view = "apps"
        self.clear_content()
        self.all_apps_label.set_text("Voltar ao início")
        self.content.pack_start(self.header_label(title), False, False, 0)
        query_folded = query.casefold().strip()
        tokens = [value for value in (category or "").split(";") if value]

        matches = []
        for app in self.apps:
            name = app.get_display_name() or app.get_name()
            description = app.get_description() or ""
            categories = app.get_string("Categories") if isinstance(app, Gio.DesktopAppInfo) else ""
            if query_folded and query_folded not in f"{name} {description}".casefold():
                continue
            if category and category != "all" and not any(token in (categories or "") for token in tokens):
                continue
            matches.append(app)

        if not matches:
            empty = Gtk.Label(label="Nenhum aplicativo encontrado.", xalign=0)
            empty.set_margin_top(20)
            self.content.pack_start(empty, False, False, 0)
        else:
            for app in matches:
                button = Gtk.Button()
                button.set_relief(Gtk.ReliefStyle.NONE)
                button.get_style_context().add_class("list-app-button")
                row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
                row.pack_start(icon_widget(app.get_icon(), 34), False, False, 0)
                labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1)
                name = Gtk.Label(label=app.get_display_name() or app.get_name(), xalign=0)
                name.get_style_context().add_class("list-app-name")
                labels.pack_start(name, False, False, 0)
                if app.get_description():
                    description = Gtk.Label(label=app.get_description(), xalign=0)
                    description.set_ellipsize(Pango.EllipsizeMode.END)
                    description.set_max_width_chars(52)
                    description.get_style_context().add_class("list-app-description")
                    labels.pack_start(description, False, False, 0)
                row.pack_start(labels, True, True, 0)
                button.add(row)
                button.connect("clicked", lambda _button, selected=app: self.launch_app(selected))
                self.content.pack_start(button, False, False, 1)
        self.content.show_all()

    def on_category(self, _button, token, label):
        self.search.set_text("")
        if token is None:
            self.render_home()
            return
        index = next((i for i, entry in enumerate(CATEGORIES) if entry[2] == token), 0)
        self.set_active_category(index)
        self.render_apps(label, token)

    def set_active_category(self, selected):
        for index, button in enumerate(self.category_buttons):
            context = button.get_style_context()
            if index == selected:
                context.add_class("active")
            else:
                context.remove_class("active")

    def on_search_changed(self, entry):
        query = entry.get_text().strip()
        if query:
            self.set_active_category(-1)
            self.render_apps(f'Resultados para “{query}”', "all", query)
        elif self.current_view == "apps":
            self.render_home()

    def toggle_all_apps(self, _button):
        self.search.set_text("")
        if self.current_view == "home":
            self.set_active_category(1)
            self.render_apps("Todos os aplicativos", "all")
        else:
            self.render_home()

    def launch_app(self, app):
        app_id = app.get_id() or app.get_executable() or app.get_name()
        try:
            app.launch([], None)
            self.usage[app_id] = int(self.usage.get(app_id, 0)) + 1
            save_usage(self.usage)
            self.hide()
        except GLib.Error as error:
            self.show_error("Não foi possível abrir o aplicativo", str(error))

    def open_uri(self, uri):
        try:
            Gio.AppInfo.launch_default_for_uri(uri, None)
            self.hide()
        except GLib.Error as error:
            self.show_error("Não foi possível abrir o item", str(error))

    def show_error(self, title, detail):
        dialog = Gtk.MessageDialog(
            transient_for=self,
            modal=True,
            message_type=Gtk.MessageType.ERROR,
            buttons=Gtk.ButtonsType.CLOSE,
            text=title,
        )
        dialog.format_secondary_text(detail)
        dialog.run()
        dialog.destroy()

    def show_power_menu(self, _button):
        popover = Gtk.Popover.new(self.power_button)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        box.set_border_width(8)
        actions = [
            ("Bloquear", "system-lock-screen-symbolic", ["/usr/local/bin/celita-lock-or-logout"] if Path("/usr/local/bin/celita-lock-or-logout").exists() else ["xflock4"]),
            ("Encerrar sessão", "system-log-out-symbolic", ["xfce4-session-logout", "--logout", "--fast"]),
            ("Reiniciar", "system-reboot-symbolic", ["xfce4-session-logout", "--reboot"]),
            ("Desligar", "system-shutdown-symbolic", ["xfce4-session-logout", "--halt"]),
        ]
        for label, icon_name, command in actions:
            button = Gtk.Button()
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=9)
            row.pack_start(Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU), False, False, 0)
            row.pack_start(Gtk.Label(label=label, xalign=0), True, True, 0)
            button.add(row)
            button.connect("clicked", self.run_session_action, command, popover)
            box.pack_start(button, False, False, 0)
        popover.add(box)
        self.power_popover = popover
        popover.show_all()

    def run_session_action(self, _button, command, popover):
        popover.hide()
        self.hide()
        try:
            subprocess.Popen(command, start_new_session=True)
        except OSError as error:
            self.show_error("Não foi possível executar a ação", str(error))

    def present_menu(self):
        self.refresh_theme()
        display = Gdk.Display.get_default()
        monitor = display.get_primary_monitor() or display.get_monitor(0)
        workarea = monitor.get_workarea()
        width = min(780, max(680, workarea.width - 24))
        height = min(650, max(560, workarea.height - 24))
        self.resize(width, height)
        self.move(workarea.x + 12, workarea.y + workarea.height - height - 12)
        self.show_all()
        self.present()
        self.search.grab_focus()
        # O clique no launcher transfere o foco do painel para o menu. Sem esta
        # pequena proteção o focus-out inicial pode esconder a janela antes de
        # ela chegar a ser desenhada.
        self.allow_focus_close = False
        GLib.timeout_add(700, self.enable_focus_close)

    def enable_focus_close(self):
        self.allow_focus_close = True
        return False

    def on_key_press(self, _widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.hide()
            return True
        return False

    def on_delete(self, *_args):
        self.hide()
        return True

    def on_focus_out(self, *_args):
        GLib.timeout_add(180, self.hide_if_unfocused)
        return False

    def hide_if_unfocused(self):
        if self.allow_focus_close and self.get_visible() and not self.is_active():
            popover = getattr(self, "power_popover", None)
            if not popover or not popover.get_visible():
                self.hide()
        return False

    def on_draw(self, _widget, context):
        context.set_operator(cairo.OPERATOR_SOURCE)
        context.set_source_rgba(0, 0, 0, 0)
        context.paint()
        context.set_operator(cairo.OPERATOR_OVER)
        return False


class StartMenuApplication(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.window = None

    def do_startup(self):
        Gtk.Application.do_startup(self)
        self.hold()

    def do_activate(self):
        if self.window is None:
            self.window = StartMenuWindow(self)
            self.window.present_menu()
        elif self.window.get_visible():
            self.window.hide()
        else:
            self.window.render_home()
            self.window.search.set_text("")
            self.window.present_menu()


if __name__ == "__main__":
    try:
        application = StartMenuApplication()
        raise SystemExit(application.run(sys.argv))
    except Exception:
        try:
            DATA_DIR.mkdir(parents=True, exist_ok=True)
            (DATA_DIR / "erro.log").write_text(traceback.format_exc(), encoding="utf-8")
        except OSError:
            pass
        raise
