/*
 * AMOURANTHRTX Native Engine · HTML5+CSS subset + SPV compute · NO BROWSER
 * Desktop/x/RTXLayer/engine · ours · X11 GL · linear BGS→BGF
 *
 *   ./RTXLayer/rtx engine
 */
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#include <GL/glx.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <dirent.h>

#ifndef GL_SHADER_STORAGE_BUFFER
#define GL_SHADER_STORAGE_BUFFER 0x90D2
#endif
#ifndef GL_SHADER_STORAGE_BARRIER_BIT
#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000
#endif
#ifndef GL_COMPUTE_SHADER
#define GL_COMPUTE_SHADER 0x91B9
#endif
#ifndef GL_UNIFORM_BUFFER
#define GL_UNIFORM_BUFFER 0x8A11
#endif

#define MAX_NODES 256
#define MAX_RULES 128
#define MAX_PROP 16
#define CW 640
#define CH 360

typedef struct {
  char name[32];
  char value[64];
} Prop;
typedef struct {
  char sel[64];
  Prop p[MAX_PROP];
  int np;
} Rule;
typedef struct Node {
  char tag[32];
  char id[64];
  char cls[128];
  char text[256];
  char data_on[4];
  int x, y, w, h;
  int visible;
  unsigned bg, fg, border;
  float opacity;
  int raised;
  int is_btn;
  int is_canvas;
  int parent;
  int child;
  int next;
} Node;

static Rule rules[MAX_RULES];
static int nrules;
static Node nodes[MAX_NODES];
static int nnodes;
static int root_idx = -1;

static int show_bgf = 1, show_sdf = 1, show_spv = 1;
static int frame_id;
static volatile int running = 1;

#define MAX_SPV 32
typedef struct { char name[128]; char path[512]; char *src; } SpvPlug;
static SpvPlug plugs[MAX_SPV];
static int nplugs;
static int plug_i;
static char spvs_dir[512] = "";
static GLuint cprog_dyn = 0;


static char *read_file(const char *path) {
  FILE *f = fopen(path, "rb");
  if (!f) return NULL;
  fseek(f, 0, SEEK_END);
  long n = ftell(f);
  fseek(f, 0, SEEK_SET);
  char *b = (char *)malloc((size_t)n + 1);
  if (!b) {
    fclose(f);
    return NULL;
  }
  if (fread(b, 1, (size_t)n, f) != (size_t)n) {
    free(b);
    fclose(f);
    return NULL;
  }
  b[n] = 0;
  fclose(f);
  return b;
}

static unsigned parse_hex_color(const char *s) {
  if (!s || s[0] != '#') return 0xff0a0c0b;
  unsigned v = 0;
  if (strlen(s) >= 7) {
    sscanf(s + 1, "%06x", &v);
    return 0xff000000u | v;
  }
  return 0xff0a0c0b;
}

static void trim(char *s) {
  char *e;
  while (*s && isspace((unsigned char)*s)) s++;
  if (s != s) memmove(s - (s - s), s, strlen(s) + 1);
  /* fix: work on original */
}

static void str_trim_inplace(char *s) {
  size_t n = strlen(s);
  while (n && isspace((unsigned char)s[n - 1])) s[--n] = 0;
  size_t i = 0;
  while (s[i] && isspace((unsigned char)s[i])) i++;
  if (i) memmove(s, s + i, strlen(s + i) + 1);
}

/* ---- CSS subset ---- */
static void parse_css(const char *src) {
  const char *p = src;
  nrules = 0;
  while (*p && nrules < MAX_RULES) {
    while (*p && (isspace((unsigned char)*p) || *p == '/')) {
      if (p[0] == '/' && p[1] == '*') {
        p += 2;
        while (*p && !(p[0] == '*' && p[1] == '/')) p++;
        if (*p) p += 2;
      } else
        p++;
    }
    if (!*p) break;
    char sel[64];
    int si = 0;
    while (*p && *p != '{' && si < 63) {
      if (!isspace((unsigned char)*p)) sel[si++] = *p;
      p++;
    }
    sel[si] = 0;
    if (*p != '{') break;
    p++;
    Rule *r = &rules[nrules++];
    memset(r, 0, sizeof *r);
    strncpy(r->sel, sel, 63);
    while (*p && *p != '}' && r->np < MAX_PROP) {
      while (*p && isspace((unsigned char)*p)) p++;
      char name[32], val[64];
      int ni = 0, vi = 0;
      while (*p && *p != ':' && ni < 31) {
        if (!isspace((unsigned char)*p)) name[ni++] = *p;
        p++;
      }
      name[ni] = 0;
      if (*p == ':') p++;
      while (*p && isspace((unsigned char)*p)) p++;
      while (*p && *p != ';' && *p != '}' && vi < 63) {
        val[vi++] = *p++;
      }
      val[vi] = 0;
      str_trim_inplace(val);
      if (name[0]) {
        strncpy(r->p[r->np].name, name, 31);
        strncpy(r->p[r->np].value, val, 63);
        r->np++;
      }
      if (*p == ';') p++;
    }
    if (*p == '}') p++;
  }
}

static const char *css_get(const Node *n, const char *prop) {
  const char *best = NULL;
  for (int i = 0; i < nrules; i++) {
    Rule *r = &rules[i];
    int match = 0;
    if (r->sel[0] == '#' && n->id[0] && strcmp(r->sel + 1, n->id) == 0) match = 2;
    else if (r->sel[0] == '.' && n->cls[0] && strstr(n->cls, r->sel + 1)) match = 1;
    else if (strcmp(r->sel, n->tag) == 0) match = 1;
    else if (strcmp(r->sel, "*") == 0) match = 0;
    if (!match && r->sel[0] != '#' && r->sel[0] != '.') continue;
    if (r->sel[0] != '#' && r->sel[0] != '.' && strcmp(r->sel, n->tag) != 0 &&
        !(r->sel[0] == '.' && strstr(n->cls, r->sel + 1))) {
      if (!(r->sel[0] == '#' && n->id[0] && strcmp(r->sel + 1, n->id) == 0)) continue;
    }
    for (int j = 0; j < r->np; j++) {
      if (strcmp(r->p[j].name, prop) == 0) best = r->p[j].value;
    }
  }
  /* simpler second pass: any matching selector */
  best = NULL;
  int best_score = -1;
  for (int i = 0; i < nrules; i++) {
    Rule *r = &rules[i];
    int score = -1;
    if (r->sel[0] == '#' && n->id[0] && strncmp(r->sel + 1, n->id, 63) == 0) score = 3;
    else if (r->sel[0] == '.') {
      char tmp[128];
      strncpy(tmp, r->sel + 1, 127);
      /* multi-class in sel like .atom.bgf */
      char *save = NULL;
      char *tok = strtok_r(tmp, ".", &save);
      int ok = 1;
      while (tok) {
        if (!strstr(n->cls, tok)) ok = 0;
        tok = strtok_r(NULL, ".", &save);
      }
      /* single class */
      if (strchr(r->sel + 1, '.') == NULL) ok = strstr(n->cls, r->sel + 1) != NULL;
      if (ok) score = 2;
    } else if (strcmp(r->sel, n->tag) == 0)
      score = 1;
    if (score < 0) continue;
    for (int j = 0; j < r->np; j++) {
      if (strcmp(r->p[j].name, prop) == 0 && score >= best_score) {
        best_score = score;
        best = r->p[j].value;
      }
    }
  }
  return best;
}

/* ---- HTML subset ---- */
static int add_node(const char *tag, int parent) {
  if (nnodes >= MAX_NODES) return -1;
  Node *n = &nodes[nnodes];
  memset(n, 0, sizeof *n);
  strncpy(n->tag, tag, 31);
  n->parent = parent;
  n->child = -1;
  n->next = -1;
  n->visible = 1;
  n->opacity = 1.f;
  n->bg = 0xff0a0c0b;
  n->fg = 0xffe8e4df;
  n->border = 0xff1e2822;
  if (parent >= 0) {
    Node *p = &nodes[parent];
    if (p->child < 0)
      p->child = nnodes;
    else {
      int c = p->child;
      while (nodes[c].next >= 0) c = nodes[c].next;
      nodes[c].next = nnodes;
    }
  }
  return nnodes++;
}

static void parse_attrs(Node *n, const char *attrs) {
  const char *p = attrs;
  while (*p) {
    while (*p && isspace((unsigned char)*p)) p++;
    if (!*p) break;
    char key[32], val[128];
    int ki = 0, vi = 0;
    while (*p && *p != '=' && !isspace((unsigned char)*p) && ki < 31) key[ki++] = *p++;
    key[ki] = 0;
    while (*p && isspace((unsigned char)*p)) p++;
    if (*p == '=') p++;
    while (*p && isspace((unsigned char)*p)) p++;
    char q = 0;
    if (*p == '"' || *p == '\'') q = *p++;
    while (*p && ((q && *p != q) || (!q && !isspace((unsigned char)*p))) && vi < 127) val[vi++] = *p++;
    val[vi] = 0;
    if (q && *p == q) p++;
    if (strcmp(key, "id") == 0) strncpy(n->id, val, 63);
    else if (strcmp(key, "class") == 0) strncpy(n->cls, val, 127);
    else if (strcmp(key, "data-on") == 0) strncpy(n->data_on, val, 3);
  }
  if (strcmp(n->tag, "button") == 0) n->is_btn = 1;
  if (strcmp(n->tag, "canvas") == 0) n->is_canvas = 1;
  if (strstr(n->cls, "raised-sdf")) n->raised = 1;
}

static void parse_html(const char *src) {
  nnodes = 0;
  root_idx = -1;
  const char *p = src;
  int stack[64];
  int sp = 0;
  int cur = -1;
  while (*p) {
    if (*p == '<') {
      if (p[1] == '!' || (p[1] == '/' && p[2] == '!') ) {
        while (*p && *p != '>') p++;
        if (*p) p++;
        continue;
      }
      if (p[1] == '/') {
        p += 2;
        char tag[32];
        int ti = 0;
        while (*p && *p != '>' && ti < 31) tag[ti++] = (char)tolower((unsigned char)*p++);
        tag[ti] = 0;
        if (*p == '>') p++;
        if (sp > 0) {
          sp--;
          cur = sp > 0 ? stack[sp - 1] : -1;
        }
        continue;
      }
      p++;
      char tag[32];
      int ti = 0;
      while (*p && !isspace((unsigned char)*p) && *p != '>' && *p != '/' && ti < 31)
        tag[ti++] = (char)tolower((unsigned char)*p++);
      tag[ti] = 0;
      char attrs[512];
      int ai = 0;
      while (*p && *p != '>' && *p != '/' && ai < 511) attrs[ai++] = *p++;
      attrs[ai] = 0;
      int self = 0;
      if (*p == '/') {
        self = 1;
        p++;
      }
      if (*p == '>') p++;
      if (strcmp(tag, "meta") == 0 || strcmp(tag, "link") == 0 || strcmp(tag, "br") == 0 ||
          strcmp(tag, "img") == 0)
        self = 1;
      if (strcmp(tag, "html") == 0 || strcmp(tag, "head") == 0) {
        /* skip head content lightly: still create body tree only */
        if (strcmp(tag, "head") == 0) {
          while (*p) {
            if (p[0] == '<' && p[1] == '/' && strncasecmp(p + 2, "head", 4) == 0) break;
            p++;
          }
          continue;
        }
        continue;
      }
      int idx = add_node(tag, cur);
      if (idx < 0) break;
      parse_attrs(&nodes[idx], attrs);
      if (root_idx < 0 && (strcmp(tag, "body") == 0 || strcmp(tag, "div") == 0)) root_idx = idx;
      if (!self && strcmp(tag, "canvas") != 0) {
        stack[sp++] = idx;
        cur = idx;
      }
      continue;
    }
    /* text */
    char text[256];
    int ti = 0;
    while (*p && *p != '<' && ti < 255) text[ti++] = *p++;
    text[ti] = 0;
    str_trim_inplace(text);
    if (text[0] && cur >= 0) {
      if (nodes[cur].text[0]) {
        strncat(nodes[cur].text, " ", 255 - strlen(nodes[cur].text));
      }
      strncat(nodes[cur].text, text, 255 - strlen(nodes[cur].text));
    }
  }
  if (root_idx < 0 && nnodes) root_idx = 0;
}

static void apply_styles(void) {
  for (int i = 0; i < nnodes; i++) {
    Node *n = &nodes[i];
    const char *bg = css_get(n, "background");
    if (!bg) bg = css_get(n, "background-color");
    if (bg && bg[0] == '#') n->bg = parse_hex_color(bg);
    const char *fg = css_get(n, "color");
    if (fg && fg[0] == '#') n->fg = parse_hex_color(fg);
    const char *bd = css_get(n, "border-color");
    if (bd && bd[0] == '#') n->border = parse_hex_color(bd);
    if (strstr(n->cls, "raised-sdf")) n->raised = 1;
    if (n->data_on[0] == '0') n->opacity = 0.4f;
  }
}

/* layout: known shell structure fullscreen */
static void layout_all(int sw, int sh) {
  for (int i = 0; i < nnodes; i++) {
    nodes[i].x = nodes[i].y = 0;
    nodes[i].w = nodes[i].h = 0;
  }
  /* find key nodes by id */
  int app = -1, stripe = -1, mast = -1, stage = -1, rail = -1, view = -1, foot = -1, canvas = -1;
  for (int i = 0; i < nnodes; i++) {
    if (strcmp(nodes[i].id, "app") == 0) app = i;
    if (strcmp(nodes[i].id, "stripe") == 0) stripe = i;
    if (strcmp(nodes[i].id, "mast") == 0) mast = i;
    if (strcmp(nodes[i].cls, "stage") == 0 || strstr(nodes[i].cls, "stage")) stage = i;
    if (strcmp(nodes[i].id, "rail") == 0) rail = i;
    if (strcmp(nodes[i].id, "viewport") == 0) view = i;
    if (strcmp(nodes[i].id, "foot") == 0) foot = i;
    if (strcmp(nodes[i].id, "gl") == 0) canvas = i;
  }
  if (app >= 0) {
    nodes[app].x = 0;
    nodes[app].y = 0;
    nodes[app].w = sw;
    nodes[app].h = sh;
  }
  int y = 0;
  if (stripe >= 0) {
    nodes[stripe].x = 0;
    nodes[stripe].y = 0;
    nodes[stripe].w = sw;
    nodes[stripe].h = 6;
    y = 6;
  }
  if (mast >= 0) {
    nodes[mast].x = 0;
    nodes[mast].y = y;
    nodes[mast].w = sw;
    nodes[mast].h = 48;
    y += 48;
    /* buttons in mast */
    int bx = sw - 14 - 3 * 70;
    for (int i = 0; i < nnodes; i++) {
      if (nodes[i].is_btn) {
        nodes[i].w = 64;
        nodes[i].h = 28;
        nodes[i].x = bx;
        nodes[i].y = nodes[mast].y + 10;
        bx += 70;
      }
    }
  }
  int foot_h = 24;
  int mid_h = sh - y - foot_h;
  if (mid_h < 100) mid_h = 100;
  if (stage >= 0) {
    nodes[stage].x = 0;
    nodes[stage].y = y;
    nodes[stage].w = sw;
    nodes[stage].h = mid_h;
  }
  int rail_w = 260;
  if (rail >= 0) {
    nodes[rail].x = 0;
    nodes[rail].y = y;
    nodes[rail].w = rail_w;
    nodes[rail].h = mid_h;
    int wy = y + 36;
    for (int i = 0; i < nnodes; i++) {
      if (strstr(nodes[i].cls, "widget") || strstr(nodes[i].cls, "socket") ||
          strstr(nodes[i].cls, "panel-h")) {
        if (nodes[i].parent == rail || nodes[rail].child >= 0) {
          /* place sequential in rail */
        }
      }
    }
    /* sequential children of rail */
    int c = nodes[rail].child;
    while (c >= 0) {
      nodes[c].x = 8;
      nodes[c].y = wy;
      nodes[c].w = rail_w - 16;
      nodes[c].h = strstr(nodes[c].cls, "panel-h") ? 18 : 32;
      wy += nodes[c].h + 6;
      c = nodes[c].next;
    }
  }
  if (view >= 0) {
    nodes[view].x = rail_w;
    nodes[view].y = y;
    nodes[view].w = sw - rail_w;
    nodes[view].h = mid_h;
  }
  if (canvas >= 0 && view >= 0) {
    nodes[canvas].x = nodes[view].x;
    nodes[canvas].y = nodes[view].y;
    nodes[canvas].w = nodes[view].w;
    nodes[canvas].h = nodes[view].h;
  }
  /* hud over canvas */
  for (int i = 0; i < nnodes; i++) {
    if (strcmp(nodes[i].id, "hud") == 0 && view >= 0) {
      nodes[i].x = nodes[view].x + 10;
      nodes[i].y = nodes[view].y + 10;
      nodes[i].w = nodes[view].w - 20;
      nodes[i].h = 80;
    }
  }
  if (foot >= 0) {
    nodes[foot].x = 0;
    nodes[foot].y = sh - foot_h;
    nodes[foot].w = sw;
    nodes[foot].h = foot_h;
  }
}

/* tiny 5x7 font */
static const unsigned char FONT5[96][7] = {
#define R(a, b, c, d, e, f, g) \
  { a, b, c, d, e, f, g }
    /* space + partial ASCII — fill via generator below for A-Z 0-9 */
};

static void font_put(unsigned *buf, int bw, int bh, int x, int y, const char *s, unsigned col) {
  /* very simple: draw 6x8 blocks for each char as pattern from code */
  for (; *s; s++) {
    unsigned char ch = (unsigned char)*s;
    for (int row = 0; row < 7; row++) {
      unsigned char bits = (unsigned char)((ch * 17 + row * 3) & 0x1f);
      if (ch >= '0' && ch <= 'z') bits = (unsigned char)((ch + row * 7) & 0x1f);
      /* readable: full cells for glyphs using bit font for A-Z 0-9 */
      static const char *glyphs =
          "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ .:-_x|●";
      (void)glyphs;
      for (int colb = 0; colb < 5; colb++) {
        int on = 0;
        if (ch == ' ')
          on = 0;
        else if (ch == '●' || ch == '*')
          on = (row > 1 && row < 5 && colb > 0 && colb < 4);
        else
          on = ((ch + row + colb * 3) % 5) != 0 && row > 0 && row < 6;
        /* better fixed: use bitmask table for common */
        if (ch >= 'A' && ch <= 'Z') {
          unsigned short rows[7] = {0x0e, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11};
          if (ch == 'B') {
            unsigned short b[7] = {0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e};
            memcpy(rows, b, sizeof rows);
          }
          if (ch == 'G') {
            unsigned short b[7] = {0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f};
            memcpy(rows, b, sizeof rows);
          }
          if (ch == 'F') {
            unsigned short b[7] = {0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10};
            memcpy(rows, b, sizeof rows);
          }
          if (ch == 'S') {
            unsigned short b[7] = {0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e};
            memcpy(rows, b, sizeof rows);
          }
          if (ch == 'P') {
            unsigned short b[7] = {0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10};
            memcpy(rows, b, sizeof rows);
          }
          if (ch == 'V') {
            unsigned short b[7] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04};
            memcpy(rows, b, sizeof rows);
          }
          on = (rows[row] >> (4 - colb)) & 1;
        } else if (ch >= '0' && ch <= '9') {
          unsigned short rows[7] = {0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e};
          on = (rows[row] >> (4 - colb)) & 1;
        } else if (ch == ':' ) {
          on = (row == 2 || row == 4) && colb == 2;
        } else if (ch == '-' || ch == '_') {
          on = (row == 3);
        } else if (ch == '.') {
          on = (row == 5 && colb == 2);
        } else if (ch == 'x' || ch == 'X') {
          on = (colb == row || colb == 4 - row);
        } else if (ch == '|') {
          on = (colb == 2);
        } else if (ch == ' ') {
          on = 0;
        } else {
          on = ((row + colb + ch) & 1) && row > 0 && row < 6;
        }
        if (!on) continue;
        int px = x + colb;
        int py = y + row;
        if (px >= 0 && py >= 0 && px < bw && py < bh) buf[py * bw + px] = col;
      }
    }
    x += 6;
  }
}

/* GL present */
static const char *BLIT_VS =
    "#version 430\n"
    "out vec2 uv; void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); uv=p; "
    "gl_Position=vec4(p*2.0-1.0,0,1); }\n";
static const char *BLIT_FS =
    "#version 430\n"
    "in vec2 uv; out vec4 fc; uniform sampler2D t; void main(){ fc=texture(t, vec2(uv.x,1.0-uv.y)); }\n";

static const char *COMP_SRC =
    "#version 430\n"
    "layout(local_size_x=16,local_size_y=16) in;\n"
    "layout(std430,binding=0) buffer Field{int field[];};\n"
    "layout(std430,binding=1) buffer Free{int free_spv[];};\n"
    "layout(std140,binding=2) uniform Params{\n"
    " float res_x,res_y,time,seed,cam_z,radius,max_steps,show_bgf,show_sdf,show_spv,pad0,pad1;\n"
    "};\n"
    "int bgf(int a,int b){return (a-b)|1;}\n"
    "int spv3(int a,int b,int c){return (a^b^c)|1;}\n"
    "float cheby(vec3 p){vec3 a=abs(p);return max(a.x,max(a.y,a.z));}\n"
    "float map_sdf(vec3 p){float d0=cheby(p)-radius;float d1=cheby(p*1.35)-radius*0.55;return "
    "min(d0,d1);}\n"
    "void main(){\n"
    " int x=int(gl_GlobalInvocationID.x),y=int(gl_GlobalInvocationID.y);\n"
    " int w=int(res_x),h=int(res_y); if(w<1)w=1; if(h<1)h=1; if(x>=w||y>=h)return;\n"
    " int i=y*w+x;\n"
    " float u=(2.0*(float(x)+0.5)/float(w)-1.0)*(float(w)/float(h));\n"
    " float v=1.0-2.0*(float(y)+0.5)/float(h);\n"
    " vec3 ro=vec3(0.0,0.1,cam_z);\n"
    " vec3 rd=normalize(vec3(u,v,-1.55));\n"
    " float ang=time*0.22; float cs=cos(ang),sn=sin(ang); mat2 R=mat2(cs,-sn,sn,cs);\n"
    " rd.xz=R*rd.xz; ro.xz=R*ro.xz;\n"
    " float t=0.0; int steps=0; int ms=int(max_steps); if(ms<1)ms=1; if(ms>96)ms=96;\n"
    " vec3 hit=ro; int hitf=0;\n"
    " for(int s=0;s<96;s++){ if(s>=ms)break; steps=s+1; hit=ro+rd*t; float d=map_sdf(hit);\n"
    "  if(d<0.001){hitf=1;break;} t+=max(d,0.001); if(t>16.0)break; }\n"
    " int cheb_i=int(max(max(abs(hit.x),abs(hit.y)),abs(hit.z))*1000.0);\n"
    " int rad_i=int(radius*1000.0);\n"
    " field[i]=bgf(cheb_i,rad_i);\n"
    " int tick=int(time*30.0);\n"
    " int freev=spv3(x^int(seed),y^tick,tick); freev=spv3(freev,steps,int(time*1000.0))|1;\n"
    " if(hitf!=0) freev=spv3(freev,steps,7)|1; else freev=spv3(freev,1,tick)|1;\n"
    " free_spv[i]=freev;\n"
    "}\n";


static void free_plugs(void) {
  for (int i = 0; i < nplugs; i++) { free(plugs[i].src); plugs[i].src = NULL; }
  nplugs = 0;
}

static int ends_with(const char *s, const char *suf) {
  size_t n = strlen(s), m = strlen(suf);
  return n >= m && strcmp(s + n - m, suf) == 0;
}

static void scan_spvs(const char *dir) {
  free_plugs();
  if (!dir || !dir[0]) return;
  DIR *d = opendir(dir);
  if (!d) { fprintf(stderr, "spvs: cannot open %s\n", dir); return; }
  struct dirent *e;
  while ((e = readdir(d)) && nplugs < MAX_SPV) {
    if (e->d_name[0] == '.') continue;
    if (!ends_with(e->d_name, ".comp")) continue;
    char path[512];
    snprintf(path, sizeof path, "%s/%s", dir, e->d_name);
    char *src = read_file(path);
    if (!src) continue;
    /* force GL 430 for runtime compile if file says 450 */
    if (strncmp(src, "#version 450", 12) == 0) {
      char *n = malloc(strlen(src) + 8);
      sprintf(n, "#version 430\n%s", src + 12);
      free(src);
      src = n;
    }
    strncpy(plugs[nplugs].name, e->d_name, 127);
    strncpy(plugs[nplugs].path, path, 511);
    plugs[nplugs].src = src;
    printf("SPV plug[%d] %s\n", nplugs, e->d_name);
    nplugs++;
  }
  closedir(d);
  if (nplugs == 0) fprintf(stderr, "spvs: no .comp in %s · using built-in\n", dir);
}

static GLuint compile_shader(GLenum type, const char *src) {
  GLuint s = glCreateShader(type);
  glShaderSource(s, 1, &src, NULL);
  glCompileShader(s);
  GLint ok = 0;
  glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
  if (!ok) {
    char log[2048];
    glGetShaderInfoLog(s, sizeof log, NULL, log);
    fprintf(stderr, "shader:\n%s\n", log);
    exit(1);
  }
  return s;
}
static GLuint link_prog(GLuint a, GLuint b) {
  GLuint p = glCreateProgram();
  glAttachShader(p, a);
  if (b) glAttachShader(p, b);
  glLinkProgram(p);
  GLint ok = 0;
  glGetProgramiv(p, GL_LINK_STATUS, &ok);
  if (!ok) {
    char log[2048];
    glGetProgramInfoLog(p, sizeof log, NULL, log);
    fprintf(stderr, "link:\n%s\n", log);
    exit(1);
  }
  return p;
}

static GLuint compile_compute_src(const char *src) {
  GLuint sh = compile_shader(GL_COMPUTE_SHADER, src);
  GLuint prog = link_prog(sh, 0);
  glDeleteShader(sh);
  return prog;
}

static void hotswap_plug(int idx, GLuint *out_prog) {
  if (nplugs <= 0) return;
  if (idx < 0) idx = 0;
  if (idx >= nplugs) idx = nplugs - 1;
  plug_i = idx;
  GLuint np = compile_compute_src(plugs[plug_i].src);
  if (*out_prog) glDeleteProgram(*out_prog);
  *out_prog = np;
  printf("hotswap SPV → [%d] %s\n", plug_i, plugs[plug_i].name);
}

static void field_free_to_rgba(const int *field, const int *freeb, int w, int h, unsigned char *rgba) {
  for (int i = 0; i < w * h; i++) {
    int d = field[i];
    int a = d < 0 ? -d : d;
    int band = a & 255;
    int fr = freeb[i];
    int spark = fr & 31;
    unsigned char r = 8, g = 10, b = 9, al = 255;
    int inside = d < 0;
    if (show_sdf) {
      if (inside) {
        r = 40;
        g = 24;
        b = 32;
      } else if (band < 6) {
        r = g = b = 230;
      } else if (band < 28) {
        r = 196;
        g = 120;
        b = 138;
      }
    }
    if (show_bgf) {
      int bands = ((a / 12) & 1);
      if (bands) {
        r = (unsigned char)(r * 0.5f + 249 * 0.5f);
        g = (unsigned char)(g * 0.5f + 248 * 0.5f);
        b = (unsigned char)(b * 0.5f + 113 * 0.5f);
      }
    }
    if (!show_sdf && !show_bgf) {
      r = g = b = 12;
    }
    if (show_spv && spark == 1) {
      r = 255;
      g = 42;
      b = 109;
    }
    if (show_spv && ((fr >> 8) & 7) == 3) {
      g = (unsigned char)(g + 40 > 255 ? 255 : g + 40);
    }
    rgba[i * 4 + 0] = r;
    rgba[i * 4 + 1] = g;
    rgba[i * 4 + 2] = b;
    rgba[i * 4 + 3] = al;
  }
}

static int hit_node(int mx, int my) {
  for (int i = nnodes - 1; i >= 0; i--) {
    Node *n = &nodes[i];
    if (!n->is_btn) continue;
    if (mx >= n->x && my >= n->y && mx < n->x + n->w && my < n->y + n->h) return i;
  }
  return -1;
}

int main(int argc, char **argv) {
  const char *root = ".";
  const char *sheet_arg = NULL; /* optional absolute/relative HTML sheet */
  for (int i = 1; i < argc; i++) {
    if (!strcmp(argv[i], "--root") && i + 1 < argc) root = argv[++i];
    else if (!strcmp(argv[i], "--sheet") && i + 1 < argc) sheet_arg = argv[++i];
    else if (!strcmp(argv[i], "--spvs") && i + 1 < argc) {
      strncpy(spvs_dir, argv[++i], sizeof spvs_dir - 1);
    }
  }
  char html_path[512], css_path[512], status_path[512];
  /* ISSUE4: prefer ESSIE single sheet SDF · fallback ui.html */
  if (sheet_arg && sheet_arg[0]) {
    snprintf(html_path, sizeof html_path, "%s", sheet_arg);
  } else {
    snprintf(html_path, sizeof html_path, "%s/sheets/essie_menu.html", root);
    if (access(html_path, R_OK) != 0)
      snprintf(html_path, sizeof html_path, "%s/ui.html", root);
  }
  snprintf(css_path, sizeof css_path, "%s/css/ui.css", root);
  snprintf(status_path, sizeof status_path, "%s/STATUS.txt", root);

  char *html = read_file(html_path);
  char *css = read_file(css_path);
  if (!html || !css) {
    fprintf(stderr, "engine: need %s and %s\n", html_path, css_path);
    return 1;
  }
  printf("engine sheet=%s\n", html_path);
  parse_css(css);
  parse_html(html);
  apply_styles();
  free(html);
  free(css);
  printf("AMOURANTHRTX Datacenter APP · nodes=%d rules=%d · fullscreen · no browser · no 127\n", nnodes, nrules);

  Display *dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fprintf(stderr, "no X11\n");
    return 1;
  }
  int screen = DefaultScreen(dpy);
  int sw = 1280, sh = 720;

  static int vis_attr[] = {GLX_RGBA, GLX_DOUBLEBUFFER, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8,
                           GLX_BLUE_SIZE, 8, GLX_DEPTH_SIZE, 16, None};
  XVisualInfo *vi = glXChooseVisual(dpy, screen, vis_attr);
  if (!vi) {
    fprintf(stderr, "no visual\n");
    return 1;
  }
  Colormap cmap = XCreateColormap(dpy, RootWindow(dpy, screen), vi->visual, AllocNone);
  XSetWindowAttributes swa;
  swa.colormap = cmap;
  swa.event_mask = ExposureMask | StructureNotifyMask | ButtonPressMask | KeyPressMask | PointerMotionMask;
  swa.background_pixel = BlackPixel(dpy, screen);
  Window win = XCreateWindow(dpy, RootWindow(dpy, screen), 80, 60, (unsigned)sw, (unsigned)sh, 0,
                             vi->depth, InputOutput, vi->visual, CWColormap | CWEventMask | CWBackPixel, &swa);
  XStoreName(dpy, win, "AMOURANTHRTX Engine · native HTML/CSS/SPV · no browser");
  Atom wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
  XSetWMProtocols(dpy, win, &wm_delete, 1);
  XMapWindow(dpy, win);
  /* fullscreen · no timeout · native app */
  {
    Atom wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
    Atom wm_fs = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
    XEvent xev;
    memset(&xev, 0, sizeof xev);
    xev.type = ClientMessage;
    xev.xclient.window = win;
    xev.xclient.message_type = wm_state;
    xev.xclient.format = 32;
    xev.xclient.data.l[0] = 1; /* _NET_WM_STATE_ADD */
    xev.xclient.data.l[1] = (long)wm_fs;
    xev.xclient.data.l[2] = 0;
    XSendEvent(dpy, RootWindow(dpy, screen), False,
               SubstructureRedirectMask | SubstructureNotifyMask, &xev);
    sw = DisplayWidth(dpy, screen);
    sh = DisplayHeight(dpy, screen);
    XMoveResizeWindow(dpy, win, 0, 0, (unsigned)sw, (unsigned)sh);
  }
  XFlush(dpy);

  GLXContext ctx = glXCreateContext(dpy, vi, NULL, GL_TRUE);
  glXMakeCurrent(dpy, win, ctx);
  printf("GL %s · %s\n", glGetString(GL_VERSION), glGetString(GL_RENDERER));

  GLuint cprog = 0;
  if (spvs_dir[0]) scan_spvs(spvs_dir);
  else {
    snprintf(spvs_dir, sizeof spvs_dir, "%s/../datacenter/spvs", root);
    scan_spvs(spvs_dir);
  }
  if (nplugs > 0) {
    cprog = compile_compute_src(plugs[0].src);
    plug_i = 0;
    printf("compute from SPV folder plug: %s\n", plugs[0].name);
  } else {
    GLuint cs = compile_shader(GL_COMPUTE_SHADER, COMP_SRC);
    cprog = link_prog(cs, 0);
    glDeleteShader(cs);
    printf("compute built-in fallback\n");
  }
  cprog_dyn = cprog;
  GLuint vs = compile_shader(GL_VERTEX_SHADER, BLIT_VS);
  GLuint fs = compile_shader(GL_FRAGMENT_SHADER, BLIT_FS);
  GLuint bprog = link_prog(vs, fs);
  glDeleteShader(vs);
  glDeleteShader(fs);

  size_t np = (size_t)CW * (size_t)CH;
  size_t bytes = np * sizeof(int);
  GLuint ssbo[2], ubo;
  glGenBuffers(2, ssbo);
  glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[0]);
  glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)bytes, NULL, GL_DYNAMIC_COPY);
  glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[1]);
  glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)bytes, NULL, GL_DYNAMIC_COPY);
  glGenBuffers(1, &ubo);
  glBindBuffer(GL_UNIFORM_BUFFER, ubo);
  glBufferData(GL_UNIFORM_BUFFER, 48, NULL, GL_DYNAMIC_DRAW);

  int *field = (int *)malloc(bytes);
  int *freeb = (int *)malloc(bytes);
  unsigned char *crgba = (unsigned char *)malloc(np * 4);
  unsigned *fb = (unsigned *)malloc((size_t)sw * (size_t)sh * sizeof(unsigned));
  if (!field || !freeb || !crgba || !fb) return 1;

  GLuint tex = 0, uitex = 0;
  glGenTextures(1, &tex);
  glBindTexture(GL_TEXTURE_2D, tex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, CW, CH, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  glGenTextures(1, &uitex);
  glBindTexture(GL_TEXTURE_2D, uitex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  struct timespec t0;
  clock_gettime(CLOCK_MONOTONIC, &t0);
  int last_free = 1;

  while (running) {
    while (XPending(dpy)) {
      XEvent ev;
      XNextEvent(dpy, &ev);
      if (ev.type == ClientMessage && (Atom)ev.xclient.data.l[0] == wm_delete) running = 0;
      if (ev.type == KeyPress) {
        KeySym ks = XLookupKeysym(&ev.xkey, 0);
        if (ks == XK_Escape || ks == XK_q) running = 0;
        if (ks >= XK_1 && ks <= XK_9) {
          int idx = (int)(ks - XK_1);
          if (idx < nplugs) hotswap_plug(idx, &cprog);
        }
        if (ks == XK_bracketright || ks == XK_n) {
          if (nplugs) hotswap_plug((plug_i + 1) % nplugs, &cprog);
        }
        if (ks == XK_bracketleft || ks == XK_p) {
          if (nplugs) hotswap_plug((plug_i - 1 + nplugs) % nplugs, &cprog);
        }
        if (ks == XK_r || ks == XK_F5) {
          scan_spvs(spvs_dir);
          if (nplugs) hotswap_plug(plug_i < nplugs ? plug_i : 0, &cprog);
        }
      }
      if (ev.type == ConfigureNotify) {
        sw = ev.xconfigure.width;
        sh = ev.xconfigure.height;
        free(fb);
        fb = (unsigned *)malloc((size_t)sw * (size_t)sh * sizeof(unsigned));
      }
      if (ev.type == ButtonPress && ev.xbutton.button == 1) {
        int hi = hit_node(ev.xbutton.x, ev.xbutton.y);
        if (hi >= 0) {
          Node *n = &nodes[hi];
          if (strcmp(n->id, "btn-bgf") == 0) {
            show_bgf = !show_bgf;
            snprintf(n->data_on, 4, "%d", show_bgf);
            n->opacity = show_bgf ? 1.f : 0.4f;
            printf("BGF %s\n", show_bgf ? "ON" : "off");
          } else if (strcmp(n->id, "btn-sdf") == 0) {
            show_sdf = !show_sdf;
            snprintf(n->data_on, 4, "%d", show_sdf);
            n->opacity = show_sdf ? 1.f : 0.4f;
            printf("SDF %s\n", show_sdf ? "ON" : "off");
          } else if (strcmp(n->id, "btn-spv") == 0) {
            show_spv = !show_spv;
            snprintf(n->data_on, 4, "%d", show_spv);
            n->opacity = show_spv ? 1.f : 0.4f;
            printf("SPV %s\n", show_spv ? "ON" : "off");
          } else if (strcmp(n->id, "btn-ezzie") == 0) {
            printf("ATOM EZZIE · free eor · rides BGF · Issue4\n");
          } else if (strcmp(n->id, "btn-phi") == 0) {
            printf("ATOM PHI · scale (d*s)|1 · Issue4\n");
          } else if (strcmp(n->id, "btn-thermo") == 0) {
            printf("ATOM THERMO · cool|warm|hot · Issue4\n");
          } else if (strcmp(n->id, "btn-stone") == 0) {
            printf("STONE SDF vault · wb stone list|store|recover · Issue4\n");
          } else if (strcmp(n->id, "btn-js") == 0) {
            printf("PLUG js_all · JS 100%% · seat via ESSIE\n");
          } else if (strcmp(n->id, "btn-fox") == 0) {
            printf("PLUG fox · The Fox train only\n");
          } else if (strcmp(n->id, "btn-essie") == 0) {
            printf("ESSIE hotswap · not EZZIE · Issue4\n");
          } else if (strcmp(n->id, "btn-kate") == 0) {
            printf("KATE SPV land · editor seat · no Konsole · term+engine\n");
          } else if (strcmp(n->id, "btn-stream") == 0) {
            printf("PLUG stream/mp4 · Host RTMP path\n");
          } else if (strcmp(n->id, "btn-term") == 0) {
            printf("PLUG term · ESSIE console · SPV land · not Konsole\n");
          }
        }
      }
    }

    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    float time =
        (float)(now.tv_sec - t0.tv_sec) + (float)(now.tv_nsec - t0.tv_nsec) * 1e-9f;
    frame_id++;

    /* SPV compute every frame */
    float params[12] = {(float)CW, (float)CH, time, 7.f, 2.2f, 0.55f, 64.f,
                        (float)show_bgf, (float)show_sdf, (float)show_spv, 0, 0};
    glBindBuffer(GL_UNIFORM_BUFFER, ubo);
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof params, params);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo[0]);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ssbo[1]);
    glBindBufferBase(GL_UNIFORM_BUFFER, 2, ubo);
    glUseProgram(cprog);
    GLuint blk = glGetUniformBlockIndex(cprog, "Params");
    if (blk != GL_INVALID_INDEX) glUniformBlockBinding(cprog, blk, 2);
    glDispatchCompute((CW + 15) / 16, (CH + 15) / 16, 1);
    glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[0]);
    glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)bytes, field);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo[1]);
    glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)bytes, freeb);
    field_free_to_rgba(field, freeb, CW, CH, crgba);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, CW, CH, GL_RGBA, GL_UNSIGNED_BYTE, crgba);

    int spv_sample = freeb[(CH / 2) * CW + (CW / 2)];
    int bgf_sample = field[(CH / 2) * CW + (CW / 2)];
    int sdf_sample = (int)((0.55f - 0.55f) * 1000); /* shell rad fixed; show measure delta */
    sdf_sample = bgf_sample; /* display field measure as related; SDF toggle is visual */
    (void)last_free;
    last_free = spv_sample;

    /* update widget text */
    for (int i = 0; i < nnodes; i++) {
      if (strcmp(nodes[i].id, "w-bgf") == 0)
        snprintf(nodes[i].text, 255, "BGF %d %s", bgf_sample, show_bgf ? "ON" : "off");
      if (strcmp(nodes[i].id, "w-sdf") == 0)
        snprintf(nodes[i].text, 255, "SDF %s", show_sdf ? "ON" : "off");
      if (strcmp(nodes[i].id, "w-spv") == 0)
        snprintf(nodes[i].text, 255, "SPV %08X", (unsigned)spv_sample);
      if (strcmp(nodes[i].id, "w-frame") == 0)
        snprintf(nodes[i].text, 255, "frame %d t=%.2f", frame_id, time);
      if (strcmp(nodes[i].id, "hud") == 0)
        snprintf(nodes[i].text, 255,
                 "DATACENTER APP · native · no 127 · no browser\nBGF=%d %s  SDF=%s  SPV=%08X\nframe=%d  plug=%s  [1-9] hotswap  r=rescan",
                 bgf_sample, show_bgf ? "ON" : "off", show_sdf ? "ON" : "off", (unsigned)spv_sample,
                 frame_id, nplugs ? plugs[plug_i].name : "builtin");
    }

    layout_all(sw, sh);

    /* CPU composite UI framebuffer */
    for (int i = 0; i < sw * sh; i++) fb[i] = 0xff0a0c0b;
    /* neon stripe */
    for (int x = 0; x < sw; x++) {
      unsigned col = (x % 18 < 3)    ? 0xffff2a6d
                     : (x % 18 < 6)  ? 0xff05f2a0
                     : (x % 18 < 9)  ? 0xff2de2e6
                                     : 0xff0a0c0b;
      for (int y = 0; y < 6; y++) fb[y * sw + x] = col;
    }
    for (int i = 0; i < nnodes; i++) {
      Node *n = &nodes[i];
      if (n->w <= 0 || n->h <= 0) continue;
      if (n->is_canvas) continue; /* GL viewport */
      unsigned bg = n->bg;
      if (n->is_btn) {
        if (strstr(n->cls, "bgf") && show_bgf) bg = 0xfff9f871;
        else if (strstr(n->cls, "sdf") && show_sdf) bg = 0xffc4788a;
        else if (strstr(n->cls, "spv") && show_spv) bg = 0xff05f2a0;
        else bg = 0xff0e1210;
      }
      for (int y = n->y; y < n->y + n->h && y < sh; y++) {
        if (y < 0) continue;
        for (int x = n->x; x < n->x + n->w && x < sw; x++) {
          if (x < 0) continue;
          int edge = (x == n->x || y == n->y || x == n->x + n->w - 1 || y == n->y + n->h - 1);
          fb[y * sw + x] = edge && n->raised ? n->border : bg;
        }
      }
      if (n->text[0]) font_put(fb, sw, sh, n->x + 6, n->y + 8, n->text, n->is_btn ? 0xff141208 : n->fg);
    }

    /* blit compute into canvas rect */
    int cx = 260, cy = 54, cw = sw - 260, ch = sh - 54 - 24;
    for (int i = 0; i < nnodes; i++)
      if (nodes[i].is_canvas) {
        cx = nodes[i].x;
        cy = nodes[i].y;
        cw = nodes[i].w;
        ch = nodes[i].h;
      }
    for (int y = 0; y < ch; y++) {
      int sy = y * CH / (ch ? ch : 1);
      if (sy >= CH) sy = CH - 1;
      for (int x = 0; x < cw; x++) {
        int sx = x * CW / (cw ? cw : 1);
        if (sx >= CW) sx = CW - 1;
        int si = (sy * CW + sx) * 4;
        unsigned col = 0xff000000u | (crgba[si] << 16) | (crgba[si + 1] << 8) | crgba[si + 2];
        int dx = cx + x, dy = cy + y;
        if (dx >= 0 && dy >= 0 && dx < sw && dy < sh) fb[dy * sw + dx] = col;
      }
    }
    /* hud text on top of canvas */
    for (int i = 0; i < nnodes; i++) {
      if (strcmp(nodes[i].id, "hud") == 0 && nodes[i].text[0]) {
        int hy = nodes[i].y;
        char *line = nodes[i].text;
        char buf[128];
        while (*line) {
          int li = 0;
          while (line[li] && line[li] != '\n' && li < 127) {
            buf[li] = line[li];
            li++;
          }
          buf[li] = 0;
          font_put(fb, sw, sh, nodes[i].x, hy, buf, 0xff05f2a0);
          hy += 10;
          line += li;
          if (*line == '\n') line++;
        }
      }
    }

    glViewport(0, 0, sw, sh);
    glBindTexture(GL_TEXTURE_2D, uitex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, sw, sh, 0, GL_BGRA, GL_UNSIGNED_BYTE, fb);
    glDisable(GL_DEPTH_TEST);
    glUseProgram(bprog);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, uitex);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glXSwapBuffers(dpy, win);
    usleep(16000);
  }

  printf("engine exit · frames=%d · STATUS %s\n", frame_id, status_path);
  free(field);
  free(freeb);
  free(crgba);
  free(fb);
  glXMakeCurrent(dpy, None, NULL);
  glXDestroyContext(dpy, ctx);
  XDestroyWindow(dpy, win);
  XCloseDisplay(dpy);
  return 0;
}
