// Supplementary code for OCF-2026-001, "Rendered readability of CKey designs on
// screens" (Open CKey Findings, 2026). Licence: CC BY 4.0, like the paper.
//
// A minimal screen-and-camera model around the reference CKey core
// (0.1.0-build.85): render a styled Profile 0 v1 symbol, place it on a screen
// page inside a room, apply whole-frame auto-exposure, blur and sensor noise,
// then find, fit and decode it with the reference decoder. Reports the read
// floor: the smallest width (camera px) that reads in >= 9 of 12 trials.
//
// Build (from a CKey core build tree):
//   g++ -O2 -std=c++20 -I <ckey>/include ocf-2026-001-simulation.cpp \
//       libckey_render.a libckey_scan.a libckey_codec.a -o sim
//
// Parameters used for each table in the paper:
//   Table 4 and 5 : ROOM in {0.03, 0.10, 0.25, 0.50, 0.90}, PAGE = 246, CROP = false (as below)
//   Table 3       : ROOM = 0.03, styles with background_edge = background (no vignette) vs a darker edge
//   Section 5.3   : CROP = true (decode only the symbol plus one third of its width around it)
//   Page sweep    : ROOM = 0.03, PAGE in {255, 246, 225, 190, 120, 40}
#include "ckey/ckey.h"
#include <cstdio>
#include <cstring>
#include <vector>
#include <random>
#include <cmath>
#include <algorithm>
#include <string>
static ckey_rgb rgb(unsigned h){return ckey_rgb{(uint8_t)(h>>16),(uint8_t)(h>>8),(uint8_t)h};}
struct Var{std::string name; ckey_style st;};
static double lin(double v){v/=255; return v<=0.04045? v/12.92: std::pow((v+0.055)/1.055,2.4);}
static void gblur(std::vector<double>&a,int W,int H,double s){ if(s<=0)return; int r=(int)std::ceil(3*s); std::vector<double>k(2*r+1); double t=0; for(int i=-r;i<=r;++i){k[i+r]=std::exp(-i*i/(2*s*s));t+=k[i+r];} for(auto&x:k)x/=t;
  std::vector<double> b(a.size()); for(int y=0;y<H;++y)for(int x=0;x<W;++x){double v=0;for(int i=-r;i<=r;++i){int xx=std::clamp(x+i,0,W-1);v+=k[i+r]*a[y*W+xx];}b[y*W+x]=v;}
  for(int y=0;y<H;++y)for(int x=0;x<W;++x){double v=0;for(int i=-r;i<=r;++i){int yy=std::clamp(y+i,0,H-1);v+=k[i+r]*b[yy*W+x];}a[y*W+x]=v;} }
// returns true if decodes
static int PAGE=246; static double ROOM=0.03; static bool CROP=false;
static bool trial(const ckey_scalable_symbol&sym,const ckey_style&st,int S,std::mt19937&rng,double glare){
  // render code at 4x then area-downsample to S (sub-pixel phase random)
  int R=S*4; ckey_raster_options o; ckey_raster_options_default(&o); o.size_px=R; o.format=CKEY_PIXELS_RGBA8; o.materials=1;
  std::vector<uint8_t> px((size_t)R*R*4); size_t req; if(ckey_render_scalable_raster(&sym,&st,&o,px.data(),px.size(),&req)!=CKEY_OK) return false;
  int W=S*6,H=S*6; std::vector<double> L(W*H,ROOM); // room
  int sx=S*3/2, sy=S*3/2; // screen page 3S x 3S, cream page
  for(int y=sy;y<sy+3*S;++y)for(int x=sx;x<sx+3*S;++x) L[y*W+x]=lin(PAGE);
  std::uniform_real_distribution<double> ph(0,3.99); int ox=(int)ph(rng), oy=(int)ph(rng);
  int cx=sx+S, cy=sy+S;
  for(int y=0;y<S;++y)for(int x=0;x<S;++x){ double acc=0;int n=0; for(int dy=0;dy<4;++dy)for(int dx=0;dx<4;++dx){int X=std::min(R-1,x*4+dx+ox/4),Y=std::min(R-1,y*4+dy+oy/4); const uint8_t*p=&px[((size_t)Y*R+X)*4]; acc+=0.2126*lin(p[0])+0.7152*lin(p[1])+0.0722*lin(p[2]); ++n;} L[(cy+y)*W+cx+x]=acc/n; }
  // auto exposure: frame mean -> 0.18 (room dominates, so the screen goes bright)
  double mean=0; for(double v:L)mean+=v; mean/=L.size(); double gain=0.18/mean; for(double&v:L)v*=gain;
  // optics: veiling glare (light spread) + blur
  if(glare>0){ std::vector<double> g=L; gblur(g,W,H,S*0.15); for(size_t i=0;i<L.size();++i)L[i]=L[i]*(1-glare)+g[i]*glare; }
  gblur(L,W,H,0.6);
  std::normal_distribution<double> nd(0,1); std::vector<uint8_t> gray(W*H);
  for(size_t i=0;i<L.size();++i){ double v=std::min(1.0,L[i]); v=std::max(0.0, v+std::sqrt(v/800)*nd(rng)+0.004*nd(rng)); v=std::min(1.0,v); gray[i]=(uint8_t)std::lround(255*std::pow(v,1/2.2)); }
  const uint8_t* base=gray.data(); int CW=W, CH=H;
  if(CROP){ // what the app's bracket-area read sees: the code plus a margin, not the room
    int m=S/3; int x0=cx-m, y0=cy-m; base=gray.data()+y0*W+x0; CW=S+2*m; CH=S+2*m; }
  double q[48]; size_t n=ckey_find_borders_gray8(base,CW,CH,W,q,6);
  for(size_t i=0;i<n;++i){ double out[8]; ckey_refine_quad_gray8(base,CW,CH,W,q+i*8,CKEY_FIELD_SCALABLE,out);
    uint8_t pay[256]; size_t len=0; uint32_t ver=0,lay=0;
    if(ckey_decode_scalable_gray8(base,CW,CH,W,out,pay,sizeof pay,&len,&ver,&lay,nullptr)==CKEY_OK && len==20) return true; }
  return false;
}
int main(){
  ckey_scalable_symbol sym; const char*p="https://openckey.org";
  ckey_encode_scalable((const uint8_t*)p,strlen(p),0,CKEY_ECC_M,CKEY_LAYOUT_STANDARD,&sym,nullptr);
  auto base=[](){ckey_style s; ckey_style_default(&s); return s;};
  std::vector<Var> v;
  auto add=[&](std::string n, unsigned bg, unsigned edge, unsigned ink, ckey_module_shape m){ ckey_style s=base(); s.background=rgb(bg); s.background_edge=rgb(edge); s.ink=rgb(ink); s.ink_shade=rgb(ink); s.modules=m; v.push_back({n,s}); };
  add("Plain",                                   0xffffff,0xffffff,0x000000,CKEY_MODULE_ROUNDED);
  add("Midnight",                                0x0b1d3a,0x0b1d3a,0xf5f1e6,CKEY_MODULE_ROUNDED);
  add("Mid-grey bg, black ink",                  0x8a8a8a,0x8a8a8a,0x000000,CKEY_MODULE_ROUNDED);
  add("Mid-grey bg, white ink",                  0x6a6a6a,0x6a6a6a,0xffffff,CKEY_MODULE_ROUNDED);
  add("White, vignette to black edge",           0xffffff,0x000000,0x000000,CKEY_MODULE_ROUNDED);
  add("White, vignette to dark grey edge",       0xffffff,0x404040,0x000000,CKEY_MODULE_ROUNDED);
  add("Navy, vignette to light edge",            0x0b1d3a,0xd8d8d8,0xf5f1e6,CKEY_MODULE_ROUNDED);
  add("Light grey bg (#d0), black ink",          0xd0d0d0,0xd0d0d0,0x000000,CKEY_MODULE_ROUNDED);
  add("Dark grey bg (#40), white ink",           0x404040,0x404040,0xffffff,CKEY_MODULE_ROUNDED);
  CROP=false;
  const double rooms[5]={0.03,0.10,0.25,0.50,0.90};
  printf("%-38s","style \\ room"); for(double r: rooms) printf("  %5.2f",r); printf("   (smallest width that reads, px; '-' = none up to 130)\n");
  for(auto&var: v){ printf("%-38s",var.name.c_str());
    for(double room: rooms){ ROOM=room; int floor=-1;
      for(int S=40; S<=130; S+=6){ std::mt19937 rng(S*7+1); int ok=0; for(int t=0;t<12;++t) ok+=trial(sym,var.st,S,rng,0.0); if(ok>=9){floor=S;break;} }
      if(floor>0) printf("  %5d",floor); else printf("      -"); fflush(stdout);}
    printf("\n"); }
}
