#pragma once
#include <X11/Xos.h>
#include <X11/Xutil.h> // -lX11
#if USE_X_SHM
#include <X11/extensions/XShm.h> // -lXext
#include <sys/ipc.h>
#include <sys/shm.h>
#endif
#include <assert.h>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <thread>
#include "vertex.hpp"


/** @brief X environment as created by @ref XWin. */
struct x_ctx_t {
    Display* display{};
    Visual* visual{};
    Window window{};
    std::mutex mtx{};
    int fd{-1};
};


/**
 * @brief Simple accessor for preallocated buffer (possibly SHM), written to by @ref Renderer.
 *
 * Pixel format suitable for directly using as X11 TrueColor image.
 * There should be only one instance and buffer in @ref XImg.
 */
class XFrame {
  public:
    struct PACKED rgb_t {
#if !USE_X && USE_PPM
        unsigned char r, g, b;
#else
        unsigned char b, g, r;
        unsigned char _unused{};
#endif
    };

  private:
    const res_t res;
    rgb_t* const buf;
    area_t dirty;

  public:
    XFrame(res_t, rgb_t*);

    INLINE constexpr rgb_t& at(dcoord_t x, dcoord_t y) {
        assert(0 <= x && x < res.w);
        assert(0 <= y && y < res.h);
        return buf[y * res.w + x];
    }

    INLINE const res_t& resolution() const {
        return res;
    }

    /** @brief As reused, track min/max of what needs to be overwritten. */
    area_t& dirty_box() {
        return dirty;
    }
};


/** @brief Creation and drawing primitives for X image with buffer in given window. */
class XImg {
  private:
    struct buf_t {
#if USE_X_SHM
        XShmSegmentInfo shm;
#endif
        XFrame::rgb_t* data;
    } buf;

    const res_t off;
    x_ctx_t& x;
    GC gctx;
    XImage* xim;
    XFrame frame;

    static buf_t create_buf(res_t);
    static XImage* create_image(res_t, x_ctx_t&, buf_t&);

  public:
    XImg(res_t, res_t, x_ctx_t&);
    ~XImg();

    XFrame& get();
    void draw();
};


/**
 * @brief Generic select() on a file descriptor, waiting for read event with timeout.
 *
 * X events can be waited or polled for, but there seems to be no blocking wait with a timeout.
 * So used on the X server connection.
 */
class FdReadSelector {
  private:
    const int fd;
    const time_t timeout;
    fd_set fds{};
    timeval tv{};

  public:
    FdReadSelector(int fd, time_t timeout_ms);
    bool wait();
};


/** @brief Called with current key press and release events, measure and pop the overall duration. */
class XKeyEvents {
  public:
    enum key_t { KEY_UP = 0, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_ACT, KEY_RESTART, KEY_REGEN, KEY_QUIT, KEY_NONE_ };
    struct action_t { ///< translated to @ref Renderer::input_t::action
        bool action;
        bool restart;
        bool regen;
        bool quit;
    };

  private:
    struct key_state_t {
        msec_t start{};
        msec_t value{};
    };

    std::mutex mtx{};
    std::condition_variable cond{};
    std::array<key_state_t, key_t::KEY_NONE_> keys{};
    bool dirty{};

    bool is_dirty() const;
    bool pop_key(key_t);

  public:
    void set(key_t, msec_t, bool); ///< key press or release at given timestamp
    void notify(); ///< possibly wakeup waiter after multiple @ref set() calls

    void wait(); ///< return or wait until 'dirty'
    bool pop_movement(float, vertex_t&); ///< translate and reset current key press durations
    void pop_action(action_t&); ///< check for and reset current one-time key presses
};


/** @brief Wait for X key events in a separate thread and notify @ref XKeyEvents. */
class XInput {
  private:
    x_ctx_t& x;
    FdReadSelector xfd;
    volatile bool shutdown;
    XKeyEvents events{};

    std::mutex mtx;
    std::thread thread;
    void worker();

  public:
    XInput(x_ctx_t&);
    ~XInput();

    XKeyEvents& get_keys();
};


/** @brief Initialize X window i/o - key presses and frame draws, respectively. */
class XWin {
  private:
#if USE_X
    x_ctx_t x{};
    std::unique_ptr<XImg> ximg{};
    std::unique_ptr<XInput> xinput{};
#else
    std::unique_ptr<XFrame::rgb_t[]> buf{};
    std::unique_ptr<XFrame> ximg{};
    std::unique_ptr<XKeyEvents> xinput{};
#endif

  public:
    XWin(res_t, res_t);
    ~XWin();

    void draw();
    XFrame& get_frame();
    XKeyEvents& get_keys();
};