#include "xwin.hpp"
#include <X11/XKBlib.h>
#if USE_PPM
#include <linux/limits.h>
#endif


XFrame::XFrame(res_t res, rgb_t* buf) : res(res), buf(buf), dirty{{0, 0}, {res.w - 1, res.h - 1}} {}


#if USE_PPM
/** @brief Dump frame as PPM image for debugging purposes; blocking. */
static void dump_ppm(const XFrame::rgb_t* buf, const res_t& res, unsigned ctr) {
    char fn[PATH_MAX];
    snprintf(fn, PATH_MAX, "frame-%u.ppm", ctr);

    int fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        LOG_ERRNO("open(%s)", fn);
        return;
    }

    if (dprintf(fd, "P6\n%d %d\n%d\n", res.w, res.h, 255) <= 0 ||
        write(fd, buf, (size_t)(res.w * res.h * 3)) != (ssize_t)(res.w * res.h * 3)) {
        LOG("cannot write to %s", fn);
    }

    close(fd);
}
#endif


XImg::XImg(res_t res, res_t off, x_ctx_t& x)
    : buf(create_buf(res)),
      off(off),
      x(x),
      gctx(DefaultGC(x.display, DefaultScreen(x.display))),
      xim(create_image(res, x, buf)),
      frame(res, buf.data) {
    XSync(x.display, 0);
}


XImg::buf_t XImg::create_buf(res_t res) {
#if USE_X_SHM
    buf_t buf{};
    buf.shm.shmid = shmget(IPC_PRIVATE, sizeof(XFrame::rgb_t) * (size_t)(res.w * res.h), IPC_CREAT | 0777);
    buf.shm.shmaddr = (char*)shmat(buf.shm.shmid, 0, 0);
    buf.shm.readOnly = 1;
    buf.data = (XFrame::rgb_t*)buf.shm.shmaddr;
    return buf;
#else
    return {(XFrame::rgb_t*)calloc((size_t)res.w * (size_t)res.h, sizeof(XFrame::rgb_t))};
#endif
}


XImage* XImg::create_image(res_t res, x_ctx_t& x, buf_t& buf) {
    std::lock_guard lock(x.mtx);

#if !USE_X_SHM
    return XCreateImage(x.display, x.visual, 24, ZPixmap, 0, (char*)buf.data, (unsigned)res.w, (unsigned)res.h,
                        sizeof(XFrame::rgb_t) * 8, res.w * (int)sizeof(XFrame::rgb_t));
#else
    // https://www.x.org/releases/X11R7.5/doc/Xext/mit-shm.html
    assert(XShmQueryExtension(x.display));
    XImage* xim =
        XShmCreateImage(x.display, x.visual, 24, ZPixmap, (char*)buf.data, &buf.shm, (unsigned)res.w, (unsigned)res.h);
    assert(xim);
    assert(xim->bytes_per_line == res.w * (int)sizeof(XFrame::rgb_t));
    if (XShmAttach(x.display, &buf.shm) == 0) {
        assert(false);
    }
    return xim;
#endif
}


XFrame& XImg::get() {
    return frame;
}


void XImg::draw() {
    const std::lock_guard<std::mutex> lock(x.mtx);
#if !USE_X_SHM
    XPutImage(x.display, x.window, gctx, xim, 0, 0, off.w, off.h, (unsigned)xim->width, (unsigned)xim->height);
    XFlush(x.display);
#else
    XShmPutImage(x.display, x.window, gctx, xim, 0, 0, off.w, off.h, (unsigned)xim->width, (unsigned)xim->height, 0);
    XFlush(x.display); // XSync(display, 0);
#endif
}


XImg::~XImg() {
    const std::lock_guard<std::mutex> lock(x.mtx);
#if !USE_X_SHM
    XDestroyImage(xim); // also frees buf
#else
    XShmDetach(x.display, &buf.shm);
    XDestroyImage(xim);
    shmdt(buf.shm.shmaddr);
    shmctl(buf.shm.shmid, IPC_RMID, 0);
#endif
}


FdReadSelector::FdReadSelector(int fd, time_t timeout_ms) : fd(fd), timeout(timeout_ms) {
    assert(timeout_ms > 0);
}


bool FdReadSelector::wait() {
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    tv = {timeout / 1000, (timeout % 1000) * 1000}; // micro

    const int num_fd = select(fd + 1, &fds, NULL, NULL, &tv);
    if (num_fd > 0) {
        return true;
    } else if (num_fd == 0) {
        return false;
    } else {
        LOG_ERRNO("X11 select()");
        return false;
    }
}


void XKeyEvents::set(key_t key, msec_t ts, bool pressed) {
    const std::lock_guard<std::mutex> lock(mtx);
    key_state_t& state = keys[key];

    if (state.start > 0 && !pressed) { // newly released
        state.value += std::max((msec_t)1, ts - state.start);
        state.start = 0;
    } else if (state.start == 0 && pressed) { // newly pressed
        state.start = ts;
        dirty = true; // became dirty if not already before
    }
}


void XKeyEvents::notify() {
    const std::lock_guard<std::mutex> lock(mtx);
    if (dirty) {
        dirty = false;
        cond.notify_all();
    }
}


bool XKeyEvents::pop_movement(float sensitivity, vertex_t& movement) {
    const std::lock_guard<std::mutex> lock(mtx);
    const msec_t now = Timer::now();

    dirty = false;
    bool noop = true;
    for (auto& state : keys) {
        if (state.start > 0) {
            state.value += std::max((msec_t)1, now - state.start);
            state.start = now;
            noop = false;
        } else if (state.value > 0) {
            noop = false;
        }
    }
    if (noop) {
        return false;
    }

    const vertex_t offset{
        ((fcoord_t)std::exchange(keys[key_t::KEY_LEFT].value, 0) * -sensitivity) +
            ((fcoord_t)std::exchange(keys[key_t::KEY_RIGHT].value, 0) * sensitivity),
        ((fcoord_t)std::exchange(keys[key_t::KEY_DOWN].value, 0) * -sensitivity) +
            ((fcoord_t)std::exchange(keys[key_t::KEY_UP].value, 0) * sensitivity),
        0.0F,
    };
    const fcoord_t max_len = std::max(std::fabs(offset.x), std::fabs(offset.y));
    movement = max_len > MICRO_COORD ? offset.normed() * max_len : offset; // prevent diagonals being faster

    return movement.sqlen() > MICRO_COORD * MICRO_COORD;
}


bool XKeyEvents::pop_key(key_t key) {
    if (keys[key].start > 0 || keys[key].value > 0) {
        keys[key].start = 0;
        keys[key].value = 0;
        return true;
    }
    return false;
}


void XKeyEvents::pop_action(action_t& action) {
    const std::lock_guard<std::mutex> lock(mtx);
    action.action = pop_key(KEY_ACT);
    action.restart = pop_key(KEY_RESTART);
    action.regen = pop_key(KEY_REGEN);
    action.quit = pop_key(KEY_QUIT);
}


bool XKeyEvents::is_dirty() const {
    return std::any_of(keys.begin(), keys.end(),
                       [](const auto& state) { return (state.start > 0 || state.value > 0); });
}

void XKeyEvents::wait() {
    std::unique_lock<std::mutex> lock(mtx);
#if USE_X
    while (!is_dirty()) {
        cond.wait(lock);
    }
#endif
}


XInput::XInput(x_ctx_t& x) : x(x), xfd(x.fd, 1000), shutdown(false), thread([this]() { worker(); }) {}


XInput::~XInput() {
    shutdown = true;
    thread.join();
}


XKeyEvents& XInput::get_keys() {
    return events;
}


void XInput::worker() {
    XEvent ev;
    char key_vector_buf[32];
    char* key_vector = nullptr;

    KeySym symbols[256];
    {
        std::lock_guard<std::mutex> lock(x.mtx);
        for (int i = 0; i < 256; ++i) {
            symbols[i] = XkbKeycodeToKeysym(x.display, (KeyCode)i, 0, 0);
        }
    }

    while (!shutdown) {
        {
            std::unique_lock<std::mutex> lock(x.mtx);
            while (XPending(x.display) == 0) {
                lock.unlock();
                (void)xfd.wait();
                if (shutdown) {
                    return;
                }
                lock.lock();
            }

            XNextEvent(x.display, &ev);
            if (ev.type == KeymapNotify) {
                key_vector = ev.xkeymap.key_vector; // no need for memcpy
            } else if (ev.type == KeyPress || ev.type == KeyRelease) {
                XQueryKeymap(x.display, key_vector_buf);
                key_vector = key_vector_buf;
            } else {
                continue;
            }
        }

        const msec_t now = Timer::now();
        for (int i = 0; i < 32; ++i) {
            for (int b = 0; b < 8; ++b) {
                const bool pressed = ((key_vector[i] & (1 << b)) != 0);
                const KeyCode keycode = (KeyCode)((i * 8) + b);
                const KeySym keysym = symbols[keycode];
                switch (keysym) {
#if USE_WASD
                    case XK_w:
                        events.set(XKeyEvents::key_t::KEY_UP, now, pressed);
                        break;
                    case XK_a:
                        events.set(XKeyEvents::key_t::KEY_LEFT, now, pressed);
                        break;
                    case XK_s:
                        events.set(XKeyEvents::key_t::KEY_DOWN, now, pressed);
                        break;
                    case XK_d:
                        events.set(XKeyEvents::key_t::KEY_RIGHT, now, pressed);
                        break;
                    case XK_e:
                        events.set(XKeyEvents::key_t::KEY_ACT, now, pressed);
                        break;
#else
                    case XK_Up:
                        events.set(XKeyEvents::key_t::KEY_UP, now, pressed);
                        break;
                    case XK_Left:
                        events.set(XKeyEvents::key_t::KEY_LEFT, now, pressed);
                        break;
                    case XK_Down:
                        events.set(XKeyEvents::key_t::KEY_DOWN, now, pressed);
                        break;
                    case XK_Right:
                        events.set(XKeyEvents::key_t::KEY_RIGHT, now, pressed);
                        break;
                    case XK_space:
                        events.set(XKeyEvents::key_t::KEY_ACT, now, pressed);
                        break;
#endif
                    case XK_F5:
                        events.set(XKeyEvents::key_t::KEY_RESTART, now, pressed);
                        break;
                    case XK_F3:
                        events.set(XKeyEvents::key_t::KEY_REGEN, now, pressed);
                        break;
                    case XK_Escape:
                        events.set(XKeyEvents::key_t::KEY_QUIT, now, pressed);
                        break;
                    case NoSymbol:
                        break;
                }
            }
        }
        events.notify();
    }
}


XWin::XWin(res_t res, res_t off) {
#if USE_X
    x.display = XOpenDisplay(NULL);
    if (x.display == NULL) {
        assert(false);
        return;
    }

    x.fd = ConnectionNumber(x.display);
    x.visual = DefaultVisual(x.display, 0);
    if (x.visual->c_class != TrueColor) {
        XCloseDisplay(x.display);
        x.display = NULL;
        assert(false);
        return;
    }

    assert(res.w > 0 && res.h > 0);
    x.window = XCreateSimpleWindow(x.display, RootWindow(x.display, 0), 0, 0, (unsigned)(res.w + (off.w * 2)),
                                   (unsigned)(res.h + (off.h * 2)), 1, 0, 0);
    if (x.window == NULL) {
        XCloseDisplay(x.display);
        x.display = NULL;
        assert(false);
        return;
    }

    if (XStoreName(x.display, x.window, "maze-tracer") == 0 ||
        XSelectInput(x.display, x.window, KeymapStateMask | KeyPressMask | KeyReleaseMask | ExposureMask) == 0 ||
        XMapWindow(x.display, x.window) == 0) {
        XDestroyWindow(x.display, x.window);
        XCloseDisplay(x.display);
        x.window = 0;
        x.display = NULL;
        assert(false);
        return;
    }

    XSync(x.display, 0);
    ximg = std::make_unique<XImg>(res, off, x);
    xinput = std::make_unique<XInput>(x);
#else
    buf = std::make_unique<XFrame::rgb_t[]>((size_t)(res.w * res.h));
    ximg = std::make_unique<XFrame>(res, buf.get());
    xinput = std::make_unique<XKeyEvents>();
#endif
}


void XWin::draw() {
#if USE_X
    ximg->draw();
#elif USE_PPM
    static unsigned frame_counter = 0;
    dump_ppm(buf.get(), ximg->resolution(), ++frame_counter);
#endif
}


XFrame& XWin::get_frame() {
#if USE_X
    return ximg->get();
#else
    return *ximg;
#endif
}


XKeyEvents& XWin::get_keys() {
#if USE_X
    return xinput->get_keys();
#else
    return *xinput;
#endif
};


XWin::~XWin() {
#if USE_X
    ximg.reset();
    xinput.reset();
    XDestroyWindow(x.display, x.window);
    XCloseDisplay(x.display);
#endif
}