import argparse
import glob
import os
import shutil
import subprocess
import sys
from contextlib import chdir
from pathlib import Path

from cffi import FFI
from setuptools import Distribution
from setuptools.command.build_ext import build_ext

QW_PATH = (Path(__file__).parent / ".." / "qw").resolve()
WLROOTS_PATH = os.getenv("QTILE_WLROOTS_PATH", "/usr/include/wlroots-0.19")

PKG_CONFIG = os.environ.get("PKG_CONFIG", "pkg-config")
WAYLAND_SCANNER = os.environ.get("QTILE_WAYLAND_SCANNER", shutil.which("wayland-scanner"))
if not WAYLAND_SCANNER:
    print(
        "Didn't find a wayland-scanner executable in $PATH, and "
        "$QTILE_WAYLAND_SCANNER is not set. Trying to get the path to it from "
        "pkg-config",
        file=sys.stderr,
    )
    WAYLAND_SCANNER = subprocess.run(
        [PKG_CONFIG, "--variable=wayland_scanner", "wayland-scanner"],
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.strip()
WAYLAND_PROTOCOLS = subprocess.run(
    [PKG_CONFIG, "--variable=pkgdatadir", "wayland-protocols"],
    text=True,
    stdout=subprocess.PIPE,
).stdout.strip()

QW_PROTO_IN_PATH = (QW_PATH / ".." / "proto").resolve()

PROTOS = [
    [
        "wlr-layer-shell-unstable-v1-protocol.h",
        f"{QW_PROTO_IN_PATH}/wlr-layer-shell-unstable-v1.xml",
    ],
    [
        "wlr-output-power-management-unstable-v1-protocol.h",
        f"{QW_PROTO_IN_PATH}/wlr-output-power-management-unstable-v1.xml",
    ],
    ["xdg-shell-protocol.h", f"{WAYLAND_PROTOCOLS}/stable/xdg-shell/xdg-shell.xml"],
    [
        "pointer-constraints-unstable-v1-protocol.h",
        f"{WAYLAND_PROTOCOLS}/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml",
    ],
]

QW_PROTO_OUT_PATH = QW_PATH / "proto"
QW_PROTO_OUT_PATH.mkdir(exist_ok=True)

for proto in PROTOS:
    subprocess.run(
        [WAYLAND_SCANNER, "server-header", proto[1], QW_PROTO_OUT_PATH / proto[0]],
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.strip()


# Helper to check whether wlroots has been compiled with xwayland support
def wlroots_has_xwayland():
    config = Path(WLROOTS_PATH) / "wlr" / "config.h"
    return "WLR_HAS_XWAYLAND 1" in config.read_text()


CDEF = """
// logging
enum wlr_log_importance {
    WLR_SILENT,
    WLR_ERROR,
    WLR_INFO,
    WLR_DEBUG,
    ...
};
enum wlr_log_importance wlr_log_get_verbosity(void);
extern "Python" void log_cb(enum wlr_log_importance importance,
                                       const char *log_str);

enum wlr_input_device_type {
    WLR_INPUT_DEVICE_KEYBOARD,
    WLR_INPUT_DEVICE_POINTER,
    WLR_INPUT_DEVICE_TOUCH,
    WLR_INPUT_DEVICE_TABLET,
    WLR_INPUT_DEVICE_TABLET_PAD,
    WLR_INPUT_DEVICE_SWITCH,
};

enum libinput_config_accel_profile {
    LIBINPUT_CONFIG_ACCEL_PROFILE_NONE = 0,
    LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT = (1 << 0),
    LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE = (1 << 1),
};

enum libinput_config_click_method {
        LIBINPUT_CONFIG_CLICK_METHOD_NONE = 0,
        LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS = (1 << 0),
        LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER = (1 << 1),
};

enum libinput_config_tap_button_map {
        LIBINPUT_CONFIG_TAP_MAP_LRM,
        LIBINPUT_CONFIG_TAP_MAP_LMR,
};

enum libinput_config_scroll_method {
        LIBINPUT_CONFIG_SCROLL_NO_SCROLL = 0,
        LIBINPUT_CONFIG_SCROLL_2FG = (1 << 0),
        LIBINPUT_CONFIG_SCROLL_EDGE = (1 << 1),
        LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN = (1 << 2),
};

struct wlr_box {
	int x, y;
	int width, height;
};

struct wlr_cursor {
    struct wlr_cursor_state *state;
    double x, y;
    ...;
};

struct wlr_output {
	char *name;
	char *make;
	char *model;
	char *serial;
	...;
};

// main callbacks
typedef uint32_t xkb_keysym_t;
typedef struct _cairo_surface cairo_surface_t;
extern "Python" void view_activation_cb(struct qw_view *view, void *userdata);
extern "Python" int keyboard_key_cb(xkb_keysym_t, uint32_t, void *userdata);
extern "Python" void manage_view_cb(struct qw_view *view, void *userdata);
extern "Python" void unmanage_view_cb(struct qw_view *view, void *userdata);
extern "Python" void cursor_motion_cb(void *userdata);
extern "Python" int cursor_button_cb(int button, uint32_t mask, bool pressed, int x, int y, void *userdata);
extern "Python" void pointer_internal_event_cb(int wid, int sx, int sy, int event_type, void *userdata);
extern "Python" void on_screen_change_cb(void *userdata);
extern "Python" void on_screen_reserve_space_cb(struct qw_output *output, void *userdata);
extern "Python" void on_input_device_added_cb(void *userdata);
extern "Python" bool focus_current_window_cb(void *userdata);
extern "Python" void on_session_lock_cb(bool locked, void *userdata);
extern "Python" struct wlr_box get_current_output_dims_cb(void *userdata);
extern "Python" bool add_idle_inhibitor_cb(void *userdata, void *inhibitor, void *view, bool is_layer_surface, bool is_session_lock_surface);
extern "Python" bool remove_idle_inhibitor_cb(void *userdata, void *inhibitor);
extern "Python" bool check_inhibited_cb(void *userdata);
extern "Python" struct qw_qtile_config *get_qtile_config_cb(void *userdata);
extern "Python" void idle_state_change_cb(void *userdata, int seconds, bool is_idle);

extern "Python" int request_focus_cb(void *userdata);
extern "Python" int request_close_cb(void *userdata);
extern "Python" int request_fullscreen_cb(bool fullscreen, void *userdata);
extern "Python" int request_maximize_cb(bool maximize, void *userdata);
extern "Python" int request_minimize_cb(bool minimize, void *userdata);
extern "Python" void set_title_cb(char* title, void *userdata);
extern "Python" void set_app_id_cb(char* app_id, void *userdata);
"""

cdef_files = [
    "log.h",
    "session-lock.h",
    "server.h",
    "view.h",
    "util.h",
    "output.h",
    "internal-view.h",
    "cursor.h",
    "input-device.h",
    "keyboard.h",
]

XWAYLAND_ONLY_SOURCES = ["xwayland-view.c"]

for file in cdef_files:
    with open(QW_PATH / file) as f:
        in_private_data = False
        skip_no_xwayland_block = False
        for line in f.readlines():
            # cffi doesn't prepocess `#if` blocks so we need
            # to strip out any xwayland blocks ourselves
            stripped = line.strip()
            if stripped.startswith("#if"):
                if "WLR_HAS_XWAYLAND" in stripped and not wlroots_has_xwayland():
                    skip_no_xwayland_block = True
                continue
            elif stripped.startswith("#else") and skip_no_xwayland_block:
                skip_no_xwayland_block = False
                continue
            elif stripped.startswith("#endif") and skip_no_xwayland_block:
                skip_no_xwayland_block = False
                continue

            if skip_no_xwayland_block:
                continue

            if line.startswith("#"):
                continue
            if line.strip().lower().startswith("// private data"):
                in_private_data = True
                CDEF += "    ...;\n"
                continue
            if line.startswith("};") and in_private_data:
                in_private_data = False
            if in_private_data:
                continue
            CDEF += line

SOURCE = "\n".join(f'#include "{header}"' for header in cdef_files) + "\n"


def get_include_path(lib: str) -> str:
    return subprocess.run(
        ["pkg-config", "--variable=includedir", lib], text=True, stdout=subprocess.PIPE
    ).stdout.strip()


INCLUDE_DIRS = [
    os.getenv("QTILE_CAIRO_PATH", "/usr/include/cairo"),
    os.getenv("QTILE_PIXMAN_PATH", "/usr/include/pixman-1"),
    os.getenv("QTILE_LIBDRM_PATH", "/usr/include/libdrm"),
    WLROOTS_PATH,
    QW_PATH,
    QW_PROTO_OUT_PATH,
]
LIBRARIES = ["wlroots-0.19", "wayland-server", "input", "cairo"]

# SOURCE_FILES loads all .c files...
SOURCE_FILES = glob.glob(f"{QW_PATH}/*.c")

# ...but we need to exclude any xwayland files
if not wlroots_has_xwayland():
    SOURCE_FILES = [f for f in SOURCE_FILES if not any(x in f for x in XWAYLAND_ONLY_SOURCES)]

OBJECTS = [Path(src).parent / "build" / Path(src).with_suffix(".o").name for src in SOURCE_FILES]


MACROS: list[tuple[str, str | None]] = [("WLR_USE_UNSTABLE", None)]
if not wlroots_has_xwayland():
    MACROS.append(("WLR_HAS_XWAYLAND", "0"))


def build_objects(debug: bool = False, asan: bool = False) -> None:
    # We have to use relative paths here for output_dir to work as expected
    with chdir(QW_PATH):
        dist = Distribution()
        cmd = build_ext(dist)
        cmd.setup_shlib_compiler()

        extra_preargs = []
        if debug or asan:
            extra_preargs = ["-g3", "-Og"]
        else:
            extra_preargs = ["-O2"]
        extra_preargs.extend(["-fPIC", "-Wall", "-Wextra"])

        cmd.shlib_compiler.compile(
            [os.path.basename(path) for path in SOURCE_FILES],
            output_dir="build",
            macros=MACROS,
            include_dirs=INCLUDE_DIRS,
            extra_preargs=extra_preargs,
        )


def ffi_compile(verbose: bool = False, debug: bool = False, asan: bool = False) -> None:
    # The ffi source of "libqtile.backend.wayland._ffi" means that we'll compile the library file
    # at libqtile/backend/wayland/_ffi.so.
    # This is built at the path specified in tmpdir which is "." by default. So, if we're in a
    # virtual environment, libqtile might be at .venv/lib/python3.13/site-packages/ but if we run
    # the builder script, we'll get a new libqtile folder in th current directory that only has that
    # tree shown above and the libtile library won't find the `_ffi` library.
    # We therefore set the tmpdir to be the path to the folder containing libqtile so the library
    # is always created in the correct folder.
    # The compile command is nested in a function to ensure that the tmpdir value is not overwritten.
    build_objects(debug=debug, asan=asan)

    extra_compile_args = []
    extra_link_args = []
    if debug or asan:
        extra_compile_args.extend(["-g3", "-Og"])
    if asan:
        extra_compile_args.extend(
            ["-fsanitize=address,leak,undefined", "-fno-omit-frame-pointer"]
        )
        extra_link_args.extend(["-fsanitize=address,undefined"])

    ffi = FFI()
    ffi.set_source(
        "libqtile.backend.wayland._ffi",
        SOURCE,
        define_macros=MACROS,
        include_dirs=INCLUDE_DIRS,
        extra_objects=OBJECTS,
        libraries=LIBRARIES,
        extra_compile_args=extra_compile_args,
        extra_link_args=extra_link_args,
    )
    ffi.cdef(CDEF)
    ffi.compile(
        tmpdir=Path(__file__).parent.parent.parent.parent.parent.as_posix(), verbose=verbose
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Qtile wayland backend build script.")
    parser.add_argument("--debug", action="store_true", help="Add debugging symbols.")
    parser.add_argument(
        "--asan", action="store_true", help="Compile with address sanitisation support."
    )
    args = parser.parse_args()
    ffi_compile(debug=args.debug, asan=args.asan)
