/*
 * wii_wm.c  –  Wii-style X11 window manager / launcher
 *
 * State machine:
 *
 *   MODE_MENU  ──[Enter]──►  MODE_LAUNCH  ──[Start]──►  MODE_APP
 *      ▲                         │                          │
 *      │         [Esc/WiiMenu]◄──┘            [Super key]──┘
 *      └─────────────────────────────────────────────────────
 *
 *  MODE_MENU    : Wii channel grid
 *  MODE_LAUNCH  : Green preview + "Wii Menu" / "Start" pills
 *  MODE_APP     : App is running fullscreen; overlay is hidden
 *  MODE_QUIT    : Exit wii_wm
 */

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/wait.h>

#include "desktop_parser.h"
#include "renderer.h"
#include "launch_screen.h"
#include "launcher.h"
#include "audio.h"

/* ── WM state ─────────────────────────────────────────────────────────────── */

typedef enum {
    MODE_MENU,
    MODE_LAUNCH,   /* launch-confirm screen                                   */
    MODE_APP,      /* app is running; overlay hidden                          */
    MODE_QUIT
} WMMode;

typedef struct {
    Display     *dpy;
    int          screen;
    Window       root;
    Window       overlay;
    int          sw, sh;

    AppList      apps;
    RenderState  rs;
    LaunchState  ls;

    WMMode       mode;
    int          is_wm;

    /* Currently running app */
    pid_t        app_pid;       /* grandchild PID (best-effort)              */
    Window       app_win;       /* X window of launched app (if detectable)  */

    struct timespec last_frame;
} WMState;

/* ── error handler ───────────────────────────────────────────────────────── */

static int wm_error_handler(Display *dpy, XErrorEvent *e)
{
    (void)dpy;
    if (e->error_code == BadAccess)
        fprintf(stderr, "wii_wm: another WM detected – overlay mode\n");
    return 0;
}

static int try_become_wm(Display *dpy, Window root)
{
    XSetErrorHandler(wm_error_handler);
    XSelectInput(dpy, root,
        SubstructureRedirectMask | SubstructureNotifyMask |
        ButtonPressMask           | KeyPressMask          |
        PropertyChangeMask);
    XSync(dpy, False);
    XSetErrorHandler(NULL);
    return 1;
}

/* ── overlay window ──────────────────────────────────────────────────────── */

static Window create_overlay(Display *dpy, int screen, int sw, int sh)
{
    Window root = RootWindow(dpy, screen);

    XSetWindowAttributes swa;
    swa.override_redirect = True;
    swa.event_mask        = KeyPressMask    |
                            ButtonPressMask  |
                            ExposureMask     |
                            FocusChangeMask  |
                            StructureNotifyMask;
    swa.background_pixel  = WhitePixel(dpy, screen);
    swa.border_pixel      = BlackPixel(dpy, screen);

    Window win = XCreateWindow(
        dpy, root, 0, 0, sw, sh, 0,
        DefaultDepth(dpy, screen),
        InputOutput,
        DefaultVisual(dpy, screen),
        CWOverrideRedirect | CWEventMask | CWBackPixel | CWBorderPixel,
        &swa);

    XWMHints *hints = XAllocWMHints();
    if (hints) {
        hints->input         = True;
        hints->initial_state = NormalState;
        hints->flags         = InputHint | StateHint;
        XSetWMHints(dpy, win, hints);
        XFree(hints);
    }

    XStoreName(dpy, win, "Wii WM");
    XMapRaised(dpy, win);
    XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
    XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime);

    return win;
}

/* ── show / hide overlay ─────────────────────────────────────────────────── */

static void show_overlay(WMState *wm)
{
    XMapRaised(wm->dpy, wm->overlay);
    XSetInputFocus(wm->dpy, wm->overlay, RevertToPointerRoot, CurrentTime);
    XGrabKeyboard(wm->dpy, wm->overlay, True,
                  GrabModeAsync, GrabModeAsync, CurrentTime);
    XFlush(wm->dpy);
}

static void hide_overlay(WMState *wm)
{
    XUngrabKeyboard(wm->dpy, CurrentTime);
    XUnmapWindow(wm->dpy, wm->overlay);
    XFlush(wm->dpy);
}

/* ── mode transitions ────────────────────────────────────────────────────── */

static void enter_menu(WMState *wm)
{
    wm->mode = MODE_MENU;
    audio_resume();         /* un-pause when returning home */
    show_overlay(wm);
    renderer_draw(&wm->rs);
}

static void enter_launch(WMState *wm)
{
    DesktopEntry *e = renderer_focused_entry(&wm->rs);
    if (!e) return;
    wm->mode = MODE_LAUNCH;
    launch_screen_set_entry(&wm->ls, e);
    show_overlay(wm);
    launch_screen_draw(&wm->ls);
}

static void enter_app(WMState *wm)
{
    DesktopEntry *e = renderer_focused_entry(&wm->rs);
    if (!e) { enter_menu(wm); return; }

    fprintf(stderr, "wii_wm: launching '%s' → %s\n", e->name, e->exec);
    wm->app_pid = launcher_launch(e);
    wm->app_win = None;
    wm->mode    = MODE_APP;
    audio_pause();          /* pause music while app is running */
    hide_overlay(wm);
    /* Give the app ~200ms to create its window before we start polling */
    usleep(200000);
}

/* ── WM redirect passthrough ─────────────────────────────────────────────── */

static void handle_wm_event(WMState *wm, XEvent *ev)
{
    switch (ev->type) {
    case MapRequest: {
        Window child = ev->xmaprequest.window;
        XMapWindow(wm->dpy, child);
        /* Track the most recently mapped window as the "app window" */
        if (wm->mode == MODE_APP && wm->app_win == None)
            wm->app_win = child;
        /* Raise it and give it focus */
        XRaiseWindow(wm->dpy, child);
        XSetInputFocus(wm->dpy, child, RevertToPointerRoot, CurrentTime);
        break;
    }
    case ConfigureRequest: {
        XWindowChanges wc;
        wc.x            = ev->xconfigurerequest.x;
        wc.y            = ev->xconfigurerequest.y;
        wc.width        = ev->xconfigurerequest.width;
        wc.height       = ev->xconfigurerequest.height;
        wc.border_width = ev->xconfigurerequest.border_width;
        wc.sibling      = ev->xconfigurerequest.above;
        wc.stack_mode   = ev->xconfigurerequest.detail;
        XConfigureWindow(wm->dpy, ev->xconfigurerequest.window,
                         ev->xconfigurerequest.value_mask, &wc);
        break;
    }
    default:
        break;
    }
}

/* ── key handlers per mode ───────────────────────────────────────────────── */

static void key_menu(WMState *wm, KeySym ks)
{
    switch (ks) {
    case XK_Left:  case XK_h: renderer_move_focus(&wm->rs, -1,  0); break;
    case XK_Right: case XK_l: renderer_move_focus(&wm->rs, +1,  0); break;
    case XK_Up:    case XK_k: renderer_move_focus(&wm->rs,  0, -1); break;
    case XK_Down:  case XK_j: renderer_move_focus(&wm->rs,  0, +1); break;

    case XK_Page_Up:
        for (int i = 0; i < COLS*ROWS; i++) renderer_move_focus(&wm->rs, 0, -1);
        break;
    case XK_Page_Down:
        for (int i = 0; i < COLS*ROWS; i++) renderer_move_focus(&wm->rs, 0, +1);
        break;

    case XK_Return:
    case XK_KP_Enter:
        enter_launch(wm);
        break;

    case XK_q: case XK_Q:
    case XK_Escape:
        wm->mode = MODE_QUIT;
        break;

    default: break;
    }
}

static void key_launch(WMState *wm, KeySym ks)
{
    switch (ks) {
    case XK_Left:  case XK_h: launch_screen_move(&wm->ls, -1); break;
    case XK_Right: case XK_l: launch_screen_move(&wm->ls, +1); break;

    case XK_Return:
    case XK_KP_Enter:
        if (wm->ls.focused_btn == LAUNCH_BTN_START)
            enter_app(wm);
        else
            enter_menu(wm);
        break;

    case XK_Escape:
    case XK_BackSpace:
        enter_menu(wm);
        break;

    default: break;
    }
}

/* In MODE_APP the overlay is hidden, so we normally don't receive key events.
   However we set up a passive grab on the Super key via XGrabKey on root,
   which fires even when the overlay is unmapped. */
static void key_app(WMState *wm, KeySym ks)
{
    switch (ks) {
    case XK_Super_L:
    case XK_Super_R:
        /* "Minimise" app → return to menu */
        enter_menu(wm);
        break;
    default: break;
    }
}

/* ── passive Super key grab (works even when overlay is hidden) ─────────── */

static void grab_super_key(WMState *wm)
{
    KeyCode super_l = XKeysymToKeycode(wm->dpy, XK_Super_L);
    KeyCode super_r = XKeysymToKeycode(wm->dpy, XK_Super_R);

    if (super_l)
        XGrabKey(wm->dpy, super_l, AnyModifier, wm->root,
                 True, GrabModeAsync, GrabModeAsync);
    if (super_r)
        XGrabKey(wm->dpy, super_r, AnyModifier, wm->root,
                 True, GrabModeAsync, GrabModeAsync);
}

/* ── timing ──────────────────────────────────────────────────────────────── */

static long ms_since(const struct timespec *prev)
{
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    return (long)(now.tv_sec  - prev->tv_sec)  * 1000L
         + (long)(now.tv_nsec - prev->tv_nsec) / 1000000L;
}

/* ── main ────────────────────────────────────────────────────────────────── */

int main(int argc, char *argv[])
{
    (void)argc; (void)argv;

    Display *dpy = XOpenDisplay(NULL);
    if (!dpy) {
        fprintf(stderr, "wii_wm: cannot open display '%s'\n",
                XDisplayName(NULL));
        return 1;
    }

    int screen = DefaultScreen(dpy);
    // int sw     = DisplayWidth(dpy, screen);
    // int sh     = DisplayHeight(dpy, screen);

    int sw = 1680;
    int sh = 1050;
    
    printf("wii_wm: display %dx%d\n", sw, sh);

    launcher_install_sigchld();

    WMState wm;
    memset(&wm, 0, sizeof(wm));
    wm.dpy    = dpy;
    wm.screen = screen;
    wm.root   = RootWindow(dpy, screen);
    wm.sw     = sw;
    wm.sh     = sh;
    wm.mode   = MODE_MENU;
    wm.app_pid = -1;
    wm.app_win = None;

    printf("wii_wm: scanning .desktop files…\n");
    applist_init(&wm.apps);
    desktop_load_all(&wm.apps);
    printf("wii_wm: found %d applications\n", wm.apps.count);

    try_become_wm(dpy, wm.root);
    grab_super_key(&wm);

    wm.overlay = create_overlay(dpy, screen, sw, sh);

    renderer_init(&wm.rs, dpy, wm.overlay, screen, sw, sh, &wm.apps);
    launch_screen_init(&wm.ls, dpy, wm.overlay,
                       screen, sw, sh, wm.rs.footer_h);

    /* ── Audio ──────────────────────────────────────────────────────────── */
    audio_init();
    /* Look for music file next to the binary, then in common locations */
    {
        const char *candidates[] = {
            "wii_menu.mp3",
            "wii_menu.ogg",
            "wii_menu.wav",
            "wii_menu.flac",
            "/usr/share/wii-wm/wii_menu.mp3",
            "/usr/share/wii-wm/wii_menu.ogg",
            NULL
        };
        for (int ci = 0; candidates[ci]; ci++) {
            if (access(candidates[ci], R_OK) == 0) {
                audio_set_volume(75);
                audio_play(candidates[ci]);
                break;
            }
        }
    }

    clock_gettime(CLOCK_MONOTONIC, &wm.last_frame);
    renderer_draw(&wm.rs);

    int x11_fd = ConnectionNumber(dpy);

    while (wm.mode != MODE_QUIT) {
        /* Drain all pending events */
        while (XPending(dpy)) {
            XEvent ev;
            XNextEvent(dpy, &ev);

            /* ── Key events ── */
            if (ev.type == KeyPress) {
                KeySym ks = XLookupKeysym(&ev.xkey, 0);

                /* Super key from root passive grab → always go home */
                if (ks == XK_Super_L || ks == XK_Super_R) {
                    if (wm.mode == MODE_APP || wm.mode == MODE_LAUNCH)
                        enter_menu(&wm);
                } else if (ev.xkey.window == wm.overlay) {
                    if (wm.mode == MODE_MENU)
                        key_menu(&wm, ks);
                    else if (wm.mode == MODE_LAUNCH)
                        key_launch(&wm, ks);
                    else if (wm.mode == MODE_APP)
                        key_app(&wm, ks);
                }
                continue;
            }

            /* ── WM redirect events ── */
            if (ev.type == MapRequest || ev.type == ConfigureRequest) {
                handle_wm_event(&wm, &ev);
                /* If an app window mapped while we're in APP mode and
                   overlay is hidden, raise the new window. */
                if (wm.mode == MODE_APP && ev.type == MapRequest) {
                    Window child = ev.xmaprequest.window;
                    XRaiseWindow(dpy, child);
                    XSetInputFocus(dpy, child,
                                   RevertToPointerRoot, CurrentTime);
                }
                continue;
            }

            /* ── Expose: redraw if overlay is visible ── */
            if (ev.type == Expose && ev.xexpose.count == 0
                    && ev.xexpose.window == wm.overlay) {
                if (wm.mode == MODE_MENU)
                    renderer_draw(&wm.rs);
                else if (wm.mode == MODE_LAUNCH)
                    launch_screen_draw(&wm.ls);
                continue;
            }

            /* ── FocusOut on overlay while in menu/launch → regrab ── */
            if (ev.type == FocusOut && ev.xfocus.window == wm.overlay
                    && (wm.mode == MODE_MENU || wm.mode == MODE_LAUNCH)) {
                XSetInputFocus(dpy, wm.overlay,
                               RevertToPointerRoot, CurrentTime);
                continue;
            }
        }

        if (wm.mode == MODE_QUIT) break;

        /* Animate at ~60 fps */
        long elapsed = ms_since(&wm.last_frame);
        if (elapsed >= 16) {
            if (wm.mode == MODE_MENU) {
                renderer_tick(&wm.rs);
                renderer_draw(&wm.rs);
            } else if (wm.mode == MODE_LAUNCH) {
                launch_screen_tick(&wm.ls);
                launch_screen_draw(&wm.ls);
            }
            /* MODE_APP: nothing to draw, just wait for Super */
            clock_gettime(CLOCK_MONOTONIC, &wm.last_frame);
        } else {
            long wait_ms = 16 - elapsed;
            struct timeval tv = {
                .tv_sec  = wait_ms / 1000,
                .tv_usec = (wait_ms % 1000) * 1000
            };
            fd_set fds;
            FD_ZERO(&fds);
            FD_SET(x11_fd, &fds);
            select(x11_fd + 1, &fds, NULL, NULL, &tv);
        }
    }

    /* cleanup */
    audio_free();
    renderer_free(&wm.rs);
    launch_screen_free(&wm.ls);
    applist_free(&wm.apps);
    XUngrabKey(dpy, AnyKey, AnyModifier, wm.root);
    XUngrabKeyboard(dpy, CurrentTime);
    XDestroyWindow(dpy, wm.overlay);
    XCloseDisplay(dpy);
    printf("wii_wm: goodbye\n");
    return 0;
}