// Requires: wpe-webkit-2.0 wpe-platform-2.0 wayland-client

#include <algorithm>
#include <cstring>
#include <fcntl.h>
#include <gio/gio.h>
#include <glib-unix.h>
#include <glib.h>
#include <jsc/jsc.h>
#include <memory>
#include <string>
#include <sys/mman.h>
#include <unistd.h>
#include <wpe/wayland/wpe-wayland.h>
#include <wpe/webkit.h>
#include <wpe/wpe-platform.h>

static const char *defaultTitle = "WPE Shell";
static constexpr int kChromeH = 16;

// ---- Chrome HTML (embedded) ----

static const char *chromeHTML = R"HTML(<!DOCTYPE html>
<html lang=en><head><meta charset="utf-8"><style>
*{gap:0;padding:0;margin:0;white-space:nowrap;background:white;color:#706b5a;overflow:hidden;outline:none;border:none;box-sizing:border-box}
::selection{background:#4a8a6244}
body{font:12px Open Sans;-webkit-user-select:none;display:flex;align-items:center;height:16px;flex-wrap:nowrap;padding:0 10px}
button,summary{min-width:14px;height:16px;display:flex;cursor:pointer;align-items:center;font:inherit;justify-content:center;flex-shrink:0}
input{max-width:700px;flex:1;font:inherit;height:16px;text-align:center;margin:0 auto}
details{display:flex;list-style:none;flex-wrap:nowrap}
details>div{display:flex;align-items:center}
#tabs{display:flex;align-items:center;min-width:0;flex:1;max-width:800px}
.tab{display:flex;align-items:center;cursor:pointer;max-width:200px;min-width:20px;height:16px}
.tab:not(.active) .x{opacity:0}
.tab .x{min-width:14px;display:flex;align-items:center;justify-content:center}
button:hover,summary:hover,input:focus,.tab.active .t,.tab .x:hover{opacity:1;text-decoration:underline}
.tab .x:not(:hover){text-decoration:none}
</style></head><body>
<details><summary>+</summary><div><div id="tabs"></div><button id="newtab" title="New Tab">+</button></div></details>
<button id="rld" title="Reload">+</button>
<input id="url"></input>
<button id="back" title="Back">+</button>
<button id="fwd" title="Forward">+</button>
<script>
const $=id=>document.getElementById(id);
const nav=(a,d)=>window.webkit.messageHandlers.nav.postMessage(d?JSON.stringify({action:a,data:d}):JSON.stringify({action:a}));
const inp=$('url');
function parse(t){t=t.trim();if(!t)return'';
  if(/^[a-z]+:\/\//i.test(t))return t;
  if(/^localhost(:\d+)?(\/.*)?$/.test(t)||/^\S+\.\S+/.test(t))return'http://'+t;
  return'https://duckduckgo.com/?q='+encodeURIComponent(t)}
function go(){const d=parse(inp.value);if(d){nav('navigate',d);inp.blur()}}
inp.onkeydown=e=>{if(e.key==='Enter')go()};
$('back').onclick=()=>nav('back');
$('fwd').onclick=()=>nav('forward');
$('rld').onclick=()=>nav('reload');
$('newtab').onclick=()=>nav('newTab');
window.chrome={
  setUrl:u=>inp.value=u||'',
  setTabs:(ts,id)=>{let tb=$('tabs');
    tb.innerHTML='';
    let drag=null;
    document.onmousemove=ev=>{
      if(!drag)return;
      let dx=ev.clientX-drag.startX, dy=ev.clientY-drag.startY;
      if(!drag.moved && (Math.abs(dx)+Math.abs(dy))>4){
        drag.moved=true;
        if(drag.el)drag.el.style.opacity='0.35';
        document.body.style.cursor='grabbing';
      }
      if(drag.moved){
        for(let n of tb.querySelectorAll('.tab')){
          let r=n.getBoundingClientRect();
          let inside=(ev.clientY>=r.top&&ev.clientY<=r.bottom&&ev.clientX>=r.left&&ev.clientX<=r.right);
          if(inside){
            let after=(ev.clientX-r.left)>r.width/2;
            n.style.boxShadow=after?'inset -2px 0 0 #d3d3d3':'inset 2px 0 0 #d3d3d3';
          } else {
            n.style.boxShadow='';
          }
        }
      }
    };
    document.onmouseup=ev=>{
      if(!drag)return;
      let wasMoved=drag.moved;
      let srcId=drag.srcId;
      if(drag.el)drag.el.style.opacity='1';
      document.body.style.cursor='';
      for(let n of tb.querySelectorAll('.tab'))n.style.boxShadow='';
      drag=null;
      if(!wasMoved)return;
      let target=null;
      for(let n of tb.querySelectorAll('.tab')){
        let r=n.getBoundingClientRect();
        if(ev.clientY>=r.top&&ev.clientY<=r.bottom&&ev.clientX>=r.left&&ev.clientX<=r.right){
          target=n;break;
        }
      }
      if(!target)return;
      let tid=parseInt(target.dataset.id);
      if(tid===srcId)return;
      let r=target.getBoundingClientRect();
      let after=(ev.clientX-r.left)>r.width/2;
      nav('reorderTab',srcId+','+tid+','+(after?'1':'0'));
    };
    for(const t of ts){
      let e=document.createElement('div');
      e.className='tab'+(t.id===id?' active':'');
      e.dataset.id=t.id;
      let s=document.createElement('span');s.className='t';s.textContent=t.title || 'loading...';
      let c=document.createElement('span');c.className='x';c.textContent='+';
      c.onmousedown=ev=>ev.stopPropagation();
      c.onclick=ev=>{ev.stopPropagation();nav('closeTab',t.id)};
      e.appendChild(s);e.appendChild(c);
      e.onclick=()=>{ if(!drag||!drag.moved) nav('switchTab',t.id); };
      e.onmousedown=ev=>{
        drag={srcId:t.id,el:e,startX:ev.clientX,startY:ev.clientY,moved:false};
      };
      tb.appendChild(e);
    }
  },
  focusUrl:()=>{inp.focus();inp.select()}
};
</script></body></html>
)HTML";

// ---- State ----

struct Tab {
  int id;
  WebKitWebView *wv;
  WPEView *wv2;
};

struct State {
  WPEToplevel *tl;
  WPEDisplay *disp;
  WebKitWebContext *ctx;
  WebKitNetworkSession *ns;
  WebKitSettings *set;
  WebKitUserContentManager *ucm;
  WebKitWebView *chrome;
  WebKitUserContentManager *cucm;
  GHashTable *tabs;
  GList *order;
  int nextId = 1, activeId = -1;
  gboolean chromeReady = FALSE;
  char *pUrl = nullptr, *pTabs = nullptr;
};

static State *g_st;
static GApplication *g_app;
static struct wl_buffer *g_dummyBuf = nullptr;
static int g_dw = 0, g_dh = 0;

// Forward decls
static gboolean fpProtect = FALSE;
static void layout(WPEToplevel *, WPEView *, WPEView *);
static void chromeRunJS(State *, const char *);
static void chromeSetUrl(State *, const char *);
static void chromeSetTabs(State *);
static Tab *newTab(State *, const char *, WebKitWebView *);
static void switchTab(State *, int);
static void closeTab(State *, int);
static gboolean onEvent(WPEView *, WPEEvent *, gpointer);

// ---- Toplevel white buffer ----

static void mapToplevel(WPEView *v) {
  auto *tl = wpe_view_get_toplevel(v);
  if (!tl || !WPE_IS_TOPLEVEL_WAYLAND(tl))
    return;
  auto *d = wpe_view_get_display(v);
  if (!WPE_IS_DISPLAY_WAYLAND(d))
    return;
  auto *shm = wpe_display_wayland_get_wl_shm(WPE_DISPLAY_WAYLAND(d));
  auto *surf = wpe_toplevel_wayland_get_wl_surface(WPE_TOPLEVEL_WAYLAND(tl));
  if (!shm || !surf)
    return;
  int w, h;
  wpe_toplevel_get_size(tl, &w, &h);
  if (w <= 0 || h <= 0) {
    w = 1400;
    h = 924;
  }
  if (g_dummyBuf && g_dw == w && g_dh == h) {
    wl_surface_attach(surf, g_dummyBuf, 0, 0);
    wl_surface_damage(surf, 0, 0, w, h);
    wl_surface_commit(surf);
    return;
  }
  if (g_dummyBuf)
    wl_buffer_destroy(g_dummyBuf);
  int stride = w * 4, size = stride * h;
  int fd = memfd_create("wpe-tl", MFD_CLOEXEC);
  if (fd < 0)
    return;
  ftruncate(fd, size);
  auto *data = (uint32_t *)mmap(nullptr, size, PROT_READ | PROT_WRITE,
                                MAP_SHARED, fd, 0);
  if (data == MAP_FAILED) {
    close(fd);
    return;
  }
  for (int i = 0; i < w * h; i++)
    data[i] = 0xFFFFFFFF;
  munmap(data, size);
  auto *pool = wl_shm_create_pool(shm, fd, size);
  g_dummyBuf =
      wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
  wl_shm_pool_destroy(pool);
  close(fd);
  g_dw = w;
  g_dh = h;
  wl_surface_attach(surf, g_dummyBuf, 0, 0);
  wl_surface_damage(surf, 0, 0, w, h);
  wl_surface_commit(surf);
}

// ---- Chrome nav ----

static char *jsonEsc(const char *s) {
  if (!s)
    return g_strdup("");
  GString *o = g_string_new(nullptr);
  for (const char *p = s; *p; p++) {
    auto c = (unsigned char)*p;
    if (c == '"' || c == '\\')
      g_string_append_c(o, '\\');
    if (c < 0x20)
      g_string_append_printf(o, "\\u%04x", c);
    else
      g_string_append_c(o, c);
  }
  return g_string_free(o, FALSE);
}

static void onNav(WebKitUserContentManager *, JSCValue *v, gpointer ud) {
  auto *st = (State *)ud;
  if (!v || !jsc_value_is_string(v))
    return;
  char *raw = jsc_value_to_string(v);
  if (!raw)
    return;
  std::unique_ptr<gchar, decltype(&g_free)> rg(raw, &g_free);
  // Parse action
  const char *a = strstr(raw, "\"action\"");
  if (!a)
    return;
  a = strchr(a + 7, ':');
  if (!a)
    return;
  a = strchr(a + 1, '"');
  if (!a)
    return;
  const char *ae = strchr(a + 1, '"');
  if (!ae)
    return;
  std::string action(a + 1, ae);
  // Parse data
  std::string data;
  const char *dk = strstr(raw, "\"data\"");
  if (dk) {
    const char *c = strchr(dk + 6, ':');
    if (c) {
      const char *p = c + 1;
      while (*p == ' ' || *p == '\t')
        p++;
      if (*p == '"') {
        const char *s = p + 1, *e = strchr(s, '"');
        if (e)
          data = std::string(s, e);
      } else {
        const char *s = p;
        while (*p && *p != ',' && *p != '}' && *p != ' ')
          p++;
        data = std::string(s, p);
      }
    }
  }

  if (action == "newTab") {
    newTab(st, "about:blank", nullptr);
    return;
  }
  if (action == "switchTab") {
    switchTab(st, atoi(data.c_str()));
    return;
  }
  if (action == "closeTab") {
    closeTab(st, atoi(data.c_str()));
    return;
  }
  if (action == "reorderTab") {
    char *ds = g_strdup(data.c_str());
    char *c1 = strchr(ds, ',');
    if (!c1) {
      g_free(ds);
      return;
    }
    *c1 = '\0';
    char *rest = c1 + 1;
    char *c2 = strchr(rest, ',');
    if (!c2) {
      g_free(ds);
      return;
    }
    *c2 = '\0';
    int did = atoi(ds), tid = atoi(rest);
    bool after = atoi(c2 + 1) != 0;
    g_free(ds);
    Tab *dt = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(did));
    Tab *tt = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(tid));
    if (!dt || !tt || dt == tt)
      return;
    st->order = g_list_remove(st->order, dt);
    GList *tn = g_list_find(st->order, tt);
    if (!tn)
      return;
    st->order = g_list_insert_before(st->order, after ? tn->next : tn, dt);
    chromeSetTabs(st);
    return;
  }
  Tab *at = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(st->activeId));
  if (!at)
    return;
  if (action == "back") {
    if (webkit_web_view_can_go_back(at->wv))
      webkit_web_view_go_back(at->wv);
    return;
  }
  if (action == "forward") {
    if (webkit_web_view_can_go_forward(at->wv))
      webkit_web_view_go_forward(at->wv);
    return;
  }
  if (action == "reload") {
    webkit_web_view_reload(at->wv);
    return;
  }
  if (action == "navigate") {
    GFile *f = g_file_new_for_commandline_arg(data.c_str());
    g_autofree char *uri = g_file_get_uri(f);
    g_object_unref(f);
    if (uri && *uri)
      webkit_web_view_load_uri(at->wv, uri);
    return;
  }
}

// ---- Chrome helpers ----

static void chromeRunJS(State *st, const char *s) {
  if (!st->chromeReady)
    return;
  webkit_web_view_evaluate_javascript(st->chrome, s, -1, nullptr, nullptr,
                                      nullptr, nullptr, nullptr);
}

static void chromeSetUrl(State *st, const char *url) {
  if (!url)
    url = "";
  g_autofree char *e = jsonEsc(url),
                  *sc = g_strdup_printf("window.chrome.setUrl('%s')", e);
  if (!st->chromeReady) {
    g_free(st->pUrl);
    st->pUrl = g_strdup(url);
    return;
  }
  chromeRunJS(st, sc);
}

static void chromeSetTabs(State *st) {
  GString *j = g_string_new("window.chrome.setTabs([");
  gboolean first = TRUE;
  for (GList *l = st->order; l; l = l->next) {
    auto *t = (Tab *)l->data;
    const char *title = webkit_web_view_get_title(t->wv);
    g_autofree char *fb = nullptr;
    if (!title || !*title) {
      const char *uri = webkit_web_view_get_uri(t->wv);
      if (uri && *uri) {
        fb = g_strdup(uri);
        title = fb;
      }
    }
    if (!title)
      title = "";
    g_autofree char *e = jsonEsc(title);
    if (!first)
      g_string_append_c(j, ',');
    g_string_append_printf(j, "{\"id\":%d,\"title\":\"%s\"}", t->id, e);
    first = FALSE;
  }
  g_string_append_printf(j, "],%d)", st->activeId);
  if (!st->chromeReady) {
    g_free(st->pTabs);
    st->pTabs = g_strdup(j->str);
    g_string_free(j, TRUE);
    return;
  }
  chromeRunJS(st, j->str);
  g_string_free(j, TRUE);
}

// ---- Content callbacks ----

static void onUri(WebKitWebView *wv, GParamSpec *, gpointer ud) {
  auto *st = (State *)ud;
  Tab *t = (Tab *)g_object_get_data(G_OBJECT(wv), "tab");
  if (!t || t->id != st->activeId)
    return;
  chromeSetUrl(st, webkit_web_view_get_uri(wv));
}

static void onTitle(WebKitWebView *, GParamSpec *, gpointer ud) {
  auto *st = (State *)ud;
  chromeSetTabs(st);
  Tab *a = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(st->activeId));
  if (a) {
    const char *t = webkit_web_view_get_title(a->wv);
    wpe_toplevel_set_title(st->tl, (t && *t) ? t : defaultTitle);
  }
}

// ---- Tab management ----

static Tab *newTab(State *st, const char *url, WebKitWebView *parent) {
  int id = st->nextId++;
  auto *t = new Tab();
  t->id = id;
  auto *pol = webkit_website_policies_new_with_policies(
      "enable-fingerprinting-protection", fpProtect, nullptr);
  if (parent)
    t->wv =
        WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "related-view",
                                     parent, "website-policies", pol, nullptr));
  else
    t->wv = WEBKIT_WEB_VIEW(g_object_new(
        WEBKIT_TYPE_WEB_VIEW, "web-context", st->ctx, "network-session", st->ns,
        "settings", st->set, "user-content-manager", st->ucm, "display",
        st->disp, "website-policies", pol, nullptr));
  g_object_unref(pol);
  t->wv2 = webkit_web_view_get_wpe_view(t->wv);
  g_object_set_data(G_OBJECT(t->wv), "tab", t);
  wpe_view_set_auto_resize_with_toplevel(t->wv2, FALSE);
  {
    int w, h;
    wpe_toplevel_get_size(st->tl, &w, &h);
    if (w > 0 && h > 0) {
      wpe_view_set_position_in_toplevel(t->wv2, 0, kChromeH);
      wpe_view_set_size(t->wv2, w, std::max(1, h - kChromeH));
    }
  }
  wpe_view_set_toplevel(t->wv2, st->tl);
  g_signal_connect(t->wv, "notify::uri", G_CALLBACK(onUri), st);
  g_signal_connect(t->wv, "notify::title", G_CALLBACK(onTitle), st);
  g_signal_connect(t->wv2, "event", G_CALLBACK(onEvent), st);
  g_signal_connect(
      t->wv, "web-process-terminated",
      G_CALLBACK(
          +[](WebKitWebView *v, WebKitWebProcessTerminationReason r, gpointer) {
            if (r == WEBKIT_WEB_PROCESS_CRASHED)
              g_timeout_add_seconds(
                  1,
                  [](gpointer d) -> gboolean {
                    webkit_web_view_reload(WEBKIT_WEB_VIEW(d));
                    return G_SOURCE_REMOVE;
                  },
                  v);
          }),
      nullptr);
  // decide-policy: new window / middle-click → new tab
  g_signal_connect(
      t->wv, "decide-policy",
      G_CALLBACK(+[](WebKitWebView *, WebKitPolicyDecision *d,
                     WebKitPolicyDecisionType type, gpointer ud) -> gboolean {
        auto *st = (State *)ud;
        if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION ||
            type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
          auto *nd = WEBKIT_NAVIGATION_POLICY_DECISION(d);
          auto *act =
              webkit_navigation_policy_decision_get_navigation_action(nd);
          unsigned btn = webkit_navigation_action_get_mouse_button(act);
          bool mid = (btn == 2);
          bool nw = (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION);
          if (mid || nw) {
            auto *req = webkit_navigation_action_get_request(act);
            const char *u = req ? webkit_uri_request_get_uri(req) : nullptr;
            if (u && *u) {
              newTab(st, u, nullptr);
              webkit_policy_decision_ignore(d);
              return TRUE;
            }
          }
        }
        webkit_policy_decision_use(d);
        return TRUE;
      }),
      st);
  // create signal: window.open → new tab
  g_signal_connect(
      t->wv, "create",
      G_CALLBACK(+[](WebKitWebView *parent, WebKitNavigationAction *act,
                     gpointer ud) -> WebKitWebView * {
        auto *st = (State *)ud;
        auto *req = webkit_navigation_action_get_request(act);
        const char *u = req ? webkit_uri_request_get_uri(req) : nullptr;
        Tab *nt = newTab(st, u, parent);
        return nt ? nt->wv : nullptr;
      }),
      st);
  // context-menu: right-click → copy link
  g_signal_connect(
      t->wv, "context-menu",
      G_CALLBACK(+[](WebKitWebView *v, WebKitContextMenu *,
                     WebKitHitTestResult *ht, gpointer) -> gboolean {
        if (webkit_hit_test_result_context_is_link(ht)) {
          const char *u = webkit_hit_test_result_get_link_uri(ht);
          if (u && *u) {
            auto *d = wpe_view_get_display(webkit_web_view_get_wpe_view(v));
            if (d) {
              auto *cl = wpe_display_get_clipboard(d);
              if (cl) {
                auto *c = wpe_clipboard_content_new();
                wpe_clipboard_content_set_text(c, u);
                wpe_clipboard_set_content(cl, c);
                wpe_clipboard_content_unref(c);
              }
            }
          }
          return TRUE;
        }
        return FALSE;
      }),
      nullptr);
  wpe_view_set_visible(t->wv2, FALSE);
  g_hash_table_insert(st->tabs, GINT_TO_POINTER(id), t);
  st->order = g_list_append(st->order, t);
  if (url && !parent) {
    GFile *f = g_file_new_for_commandline_arg(url);
    g_autofree char *uri = g_file_get_uri(f);
    g_object_unref(f);
    if (uri && *uri)
      webkit_web_view_load_uri(t->wv, uri);
  }
  switchTab(st, id);
  return t;
}

static void switchTab(State *st, int id) {
  Tab *nt = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(id));
  if (!nt)
    return;
  Tab *ot = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(st->activeId));
  if (ot && ot != nt)
    wpe_view_set_visible(ot->wv2, FALSE);
  st->activeId = id;
  auto *cw = webkit_web_view_get_wpe_view(st->chrome);
  layout(st->tl, cw, nt->wv2);
  wpe_view_set_visible(nt->wv2, TRUE);
  chromeSetUrl(st, webkit_web_view_get_uri(nt->wv));
  chromeSetTabs(st);
  const char *t = webkit_web_view_get_title(nt->wv);
  wpe_toplevel_set_title(st->tl, (t && *t) ? t : defaultTitle);
}

static void closeTab(State *st, int id) {
  Tab *t = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(id));
  if (!t)
    return;
  bool wasActive = (id == st->activeId);
  g_hash_table_steal(st->tabs, GINT_TO_POINTER(id));
  st->order = g_list_remove(st->order, t);
  wpe_view_set_visible(t->wv2, FALSE);
  if (wasActive) {
    if (st->order) {
      switchTab(st, ((Tab *)st->order->data)->id);
    } else {
      st->activeId = -1;
    }
  }
  g_signal_handlers_disconnect_by_data(t->wv, st);
  g_object_set_data(G_OBJECT(t->wv), "tab", nullptr);
  g_object_unref(t->wv);
  delete t;
  if (g_hash_table_size(st->tabs) == 0) {
    g_application_quit(g_app);
    return;
  }
  chromeSetTabs(st);
}

// ---- Layout ----

static void layout(WPEToplevel *tl, WPEView *cv, WPEView *ctv) {
  int w, h;
  wpe_toplevel_get_size(tl, &w, &h);
  if (w <= 0 || h <= 0)
    return;
  if (cv) {
    wpe_view_set_position_in_toplevel(cv, 0, 0);
    wpe_view_set_size(cv, w, kChromeH);
  }
  if (ctv) {
    wpe_view_set_position_in_toplevel(ctv, 0, kChromeH);
    wpe_view_set_size(ctv, w, std::max(1, h - kChromeH));
  }
}

// ---- Keyboard shortcuts ----

static gboolean onEvent(WPEView *, WPEEvent *ev, gpointer ud) {
  if (wpe_event_get_event_type(ev) != WPE_EVENT_KEYBOARD_KEY_DOWN)
    return FALSE;
  auto *st = (State *)ud;
  auto m = wpe_event_get_modifiers(ev);
  auto k = wpe_event_keyboard_get_keyval(ev);
  if (!(m & WPE_MODIFIER_KEYBOARD_CONTROL))
    return FALSE;
  Tab *a = (Tab *)g_hash_table_lookup(st->tabs, GINT_TO_POINTER(st->activeId));
  if (!a)
    return FALSE;
  if ((m & WPE_MODIFIER_KEYBOARD_SHIFT) && k == WPE_KEY_I) {
    webkit_web_view_toggle_inspector(a->wv);
    return TRUE;
  }
  if (k == WPE_KEY_R) {
    webkit_web_view_reload(a->wv);
    return TRUE;
  }
  if (k == WPE_KEY_T) {
    newTab(st, "about:blank", nullptr);
    return TRUE;
  }
  if (k == WPE_KEY_W) {
    closeTab(st, st->activeId);
    return TRUE;
  }
  if (k == WPE_KEY_Q) {
    g_application_quit(g_app);
    return TRUE;
  }
  if (k == WPE_KEY_L) {
    chromeRunJS(st, "window.chrome.focusUrl()");
    return TRUE;
  }
  return FALSE;
}

// ---- CLI variables (need to be before functions that use them) ----

static gboolean privateMode, ignoreTLS, windowMax, windowFull, cookiesDb,
    smooth = FALSE, spatialNav = FALSE;
static const char **uriArgs, **ignoreHosts, **filters, **scriptsStart,
    **scriptsEnd;
static const char *cookiesFile, *cookiesPolicy, *proxy, *profileDir, *tz,
    *features;
static guint winW = 0, winH = 0;
static gboolean printVer;

// ---- Settings / network ----

static WebKitNetworkSession *createNS() {
  auto *ns =
      privateMode  ? webkit_network_session_new_ephemeral()
      : profileDir ? ([] {
          g_autofree char *d = g_build_filename(profileDir, "data", nullptr),
                          *c = g_build_filename(profileDir, "cache", nullptr);
          return webkit_network_session_new(d, c);
        })()
                   : webkit_network_session_new(nullptr, nullptr);
  webkit_network_session_set_itp_enabled(ns, FALSE);
  if (proxy) {
    auto *p = webkit_network_proxy_settings_new(proxy, ignoreHosts);
    webkit_network_session_set_proxy_settings(
        ns, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, p);
    webkit_network_proxy_settings_free(p);
  }
  if (ignoreTLS)
    webkit_network_session_set_tls_errors_policy(
        ns, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
  auto *cm = webkit_network_session_get_cookie_manager(ns);
  if (cookiesPolicy) {
    auto *ec = static_cast<GEnumClass *>(
        g_type_class_ref(WEBKIT_TYPE_COOKIE_ACCEPT_POLICY));
    auto *ev = g_enum_get_value_by_nick(ec, cookiesPolicy);
    if (ev)
      webkit_cookie_manager_set_accept_policy(
          cm, static_cast<WebKitCookieAcceptPolicy>(ev->value));
    g_type_class_unref(ec);
  }
  if (cookiesFile && !webkit_network_session_is_ephemeral(ns)) {
    auto st = g_str_has_suffix(cookiesFile, ".txt")
                  ? WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT
                  : WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE;
    webkit_cookie_manager_set_persistent_storage(cm, cookiesFile, st);
  }
  if (cookiesDb)
    g_signal_connect(cm, "changed", G_CALLBACK(+[](WebKitCookieManager *) {
                       g_print("[cookies] changed\n");
                     }),
                     nullptr);
  return ns;
}

static WebKitSettings *createSet() {
  auto *s = webkit_settings_new();
  webkit_settings_set_enable_javascript(s, TRUE);
  webkit_settings_set_enable_smooth_scrolling(s, smooth);
  webkit_settings_set_enable_webgl(s, FALSE);
  webkit_settings_set_enable_media_stream(s, FALSE);
  webkit_settings_set_enable_spatial_navigation(s, spatialNav);
  webkit_settings_set_javascript_can_open_windows_automatically(s, TRUE);
  webkit_settings_set_enable_developer_extras(s, TRUE);
  if (features) {
    g_autoptr(WebKitFeatureList) fl = webkit_settings_get_all_features();
    g_auto(GStrv) items = g_strsplit(features, ",", -1);
    for (gsize i = 0; items[i]; i++) {
      char *item = g_strchomp(items[i]);
      gboolean en = TRUE;
      if (*item == '!' || *item == '-') {
        en = FALSE;
        item++;
      } else if (*item == '+')
        item++;
      if (!*item)
        continue;
      if (auto *f = webkit_feature_list_find(fl, item))
        webkit_settings_set_feature_enabled(s, f, en);
    }
  }
  return s;
}

// ---- User content (filters + scripts) ----

struct FilterSaveData {
  GMainLoop *loop = nullptr;
  WebKitUserContentFilter *filter = nullptr;
  GError *err = nullptr;
};
static void filterSavedCb(WebKitUserContentFilterStore *s, GAsyncResult *r,
                          FilterSaveData *d) {
  d->filter = webkit_user_content_filter_store_save_finish(s, r, &d->err);
  g_main_loop_quit(d->loop);
}
static void filterLoadedCb(WebKitUserContentFilterStore *s, GAsyncResult *r,
                           FilterSaveData *d) {
  d->filter = webkit_user_content_filter_store_load_finish(s, r, &d->err);
  g_main_loop_quit(d->loop);
}

static void addUserScript(WebKitUserContentManager *ucm, const char *path,
                          WebKitUserScriptInjectionTime t) {
  g_autoptr(GFile) f = g_file_new_for_commandline_arg(path);
  g_autofree char *src = nullptr;
  g_autoptr(GError) e = nullptr;
  if (g_file_load_contents(f, nullptr, &src, nullptr, nullptr, &e))
    webkit_user_content_manager_add_script(
        ucm, webkit_user_script_new(src, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
                                    t, nullptr, nullptr));
  else
    g_printerr("User script '%s': %s\n", path, e ? e->message : "?");
}

static WebKitUserContentManager *createUCM() {
  g_autoptr(WebKitUserContentManager) ucm = nullptr;
  if (filters) {
    g_autofree gchar *path = g_build_filename(
        g_get_user_cache_dir(), g_get_prgname(), "filters", nullptr);
    g_autoptr(WebKitUserContentFilterStore) store =
        webkit_user_content_filter_store_new(path);
    for (int i = 0; filters[i]; i++) {
      const char *fp = filters[i];
      g_autoptr(GFile) ff = g_file_new_for_commandline_arg(fp);
      g_autofree gchar *bn = g_path_get_basename(fp);
      g_autofree gchar *fn = g_str_has_suffix(bn, ".json")
                                 ? g_strndup(bn, strlen(bn) - 5)
                                 : g_strdup(bn);
      // Try loading from cache first
      FilterSaveData ld;
      webkit_user_content_filter_store_load(
          store, fn, nullptr,
          reinterpret_cast<GAsyncReadyCallback>(filterLoadedCb), &ld);
      ld.loop = g_main_loop_new(nullptr, FALSE);
      g_main_loop_run(ld.loop);
      if (ld.filter) {
        if (!ucm)
          ucm = webkit_user_content_manager_new();
        webkit_user_content_manager_add_filter(ucm, ld.filter);
        g_print("[content-filter] cached: %s (as '%s')\n", fp, fn);
        g_clear_pointer(&ld.filter, webkit_user_content_filter_unref);
        g_main_loop_unref(ld.loop);
        continue;
      }
      g_clear_pointer(&ld.err, g_error_free);
      g_main_loop_unref(ld.loop);
      // Not cached → compile from file
      FilterSaveData sd;
      webkit_user_content_filter_store_save_from_file(
          store, fn, ff, nullptr,
          reinterpret_cast<GAsyncReadyCallback>(filterSavedCb), &sd);
      sd.loop = g_main_loop_new(nullptr, FALSE);
      g_main_loop_run(sd.loop);
      if (sd.filter) {
        if (!ucm)
          ucm = webkit_user_content_manager_new();
        webkit_user_content_manager_add_filter(ucm, sd.filter);
        g_print("[content-filter] compiled: %s (as '%s')\n", fp, fn);
      } else
        g_printerr("Cannot save filter '%s': %s\n", fp,
                   sd.err ? sd.err->message : "(unknown)");
      g_clear_pointer(&sd.err, g_error_free);
      g_clear_pointer(&sd.filter, webkit_user_content_filter_unref);
      g_main_loop_unref(sd.loop);
    }
  }
  if (scriptsStart || scriptsEnd) {
    if (!ucm)
      ucm = webkit_user_content_manager_new();
    if (scriptsStart)
      for (int i = 0; scriptsStart[i]; i++)
        addUserScript(ucm, scriptsStart[i],
                      WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START);
    if (scriptsEnd)
      for (int i = 0; scriptsEnd[i]; i++)
        addUserScript(ucm, scriptsEnd[i],
                      WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END);
  }
  return static_cast<WebKitUserContentManager *>(g_steal_pointer(&ucm));
}

// ---- CLI options ----

static gboolean parseSize(const char *, const char *v, gpointer, GError **e) {
  if (!v)
    return FALSE;
  g_auto(GStrv) p = g_strsplit(v, "x", 2);
  if (!p || !p[0] || !p[1]) {
    g_set_error(e, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Bad --size");
    return FALSE;
  }
  guint64 w, h;
  if (!g_ascii_string_to_unsigned(p[0], 10, 0, G_MAXUINT, &w, nullptr) ||
      !g_ascii_string_to_unsigned(p[1], 10, 0, G_MAXUINT, &h, nullptr)) {
    g_set_error(e, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "Bad number");
    return FALSE;
  }
  winW = w;
  winH = h;
  return TRUE;
}

static const GOptionEntry opts[] = {
    {"private", 'p', 0, G_OPTION_ARG_NONE, &privateMode, "Private mode",
     nullptr},
    {"profile-dir", 0, 0, G_OPTION_ARG_FILENAME, &profileDir, "Profile dir",
     "DIR"},
    {"cookies-file", 'c', 0, G_OPTION_ARG_FILENAME, &cookiesFile, "Cookie file",
     "FILE"},
    {"cookies-policy", 0, 0, G_OPTION_ARG_STRING, &cookiesPolicy,
     "Cookie policy", "POLICY"},
    {"proxy", 0, 0, G_OPTION_ARG_STRING, &proxy, "Proxy", "PROXY"},
    {"ignore-host", 0, 0, G_OPTION_ARG_STRING_ARRAY, &ignoreHosts,
     "Proxy ignore hosts", "HOSTS"},
    {"ignore-tls-errors", 0, 0, G_OPTION_ARG_NONE, &ignoreTLS,
     "Ignore TLS errors", nullptr},
    {"content-filter", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filters,
     "Content filter JSON", "FILE"},
    {"user-script-at-start", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &scriptsStart,
     "User script at start", "PATH"},
    {"user-script-at-end", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &scriptsEnd,
     "User script at end", "PATH"},
    {"cookies-debug", 0, 0, G_OPTION_ARG_NONE, &cookiesDb, "Debug cookies",
     nullptr},
    {"time-zone", 't', 0, G_OPTION_ARG_STRING, &tz, "Time zone", "TZ"},
    {"features", 'F', 0, G_OPTION_ARG_STRING, &features, "WebKit features",
     "LIST"},
    {"maximized", 'm', 0, G_OPTION_ARG_NONE, &windowMax, "Start maximized",
     nullptr},
    {"fullscreen", 'f', 0, G_OPTION_ARG_NONE, &windowFull, "Start fullscreen",
     nullptr},
    {"enable-fingerprinting-protection", 0, 0, G_OPTION_ARG_NONE, &fpProtect,
     "Fingerprint protection", nullptr},
    {"size", 's', 0, G_OPTION_ARG_CALLBACK,
     reinterpret_cast<gpointer>(parseSize), "Window size", "WxH"},
    {"version", 'v', 0, G_OPTION_ARG_NONE, &printVer, "Print version", nullptr},
    {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArgs, nullptr,
     "[URL]"},
    {nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr}};

// ---- main ----

int main(int argc, char **argv) {
  g_set_prgname("wpeshell");
  GError *err = nullptr;
  auto *ctx = g_option_context_new(nullptr);
  g_option_context_add_main_entries(ctx, opts, nullptr);
  if (!g_option_context_parse(ctx, &argc, &argv, &err)) {
    g_printerr("%s\n", err ? err->message : "?");
    g_clear_error(&err);
    g_option_context_free(ctx);
    return 1;
  }
  g_option_context_free(ctx);
  if (printVer) {
    g_print("WPE WebKit %u.%u.%u\n", webkit_get_major_version(),
            webkit_get_minor_version(), webkit_get_micro_version());
    return 0;
  }
  if (features && !g_strcmp0(features, "help")) {
    g_autoptr(WebKitFeatureList) fl = webkit_settings_get_all_features();
    g_print("Features (+/- = enabled/disabled by default):\n\n");
    for (gsize i = 0; i < webkit_feature_list_get_length(fl); i++) {
      auto *f = webkit_feature_list_get(fl, i);
      g_print("  %c %s: %s\n", webkit_feature_get_default_value(f) ? '+' : '-',
              webkit_feature_get_identifier(f),
              webkit_feature_get_name(f) ? webkit_feature_get_name(f) : "");
    }
    return 0;
  }
  if (fpProtect)
    g_setenv("WPE_FINGERPRINT_PROTECTION", "1", TRUE);
  if (windowMax && windowFull) {
    g_printerr("--maximized and --fullscreen conflict");
    return 1;
  }

  auto appDel = [](GApplication *a) { g_clear_object(&a); };
  std::unique_ptr<GApplication, decltype(appDel)> app(
      g_application_new("org.wpeshell", G_APPLICATION_NON_UNIQUE), appDel);
  g_application_hold(app.get());
  g_app = app.get();
  g_unix_signal_add(
      SIGINT,
      [](gpointer a) -> gboolean {
        g_application_quit(G_APPLICATION(a));
        return G_SOURCE_REMOVE;
      },
      app.get());
  g_unix_signal_add(
      SIGTERM,
      [](gpointer a) -> gboolean {
        g_application_quit(G_APPLICATION(a));
        return G_SOURCE_REMOVE;
      },
      app.get());

  GError *de = nullptr;
  auto *disp = wpe_display_wayland_new();
  if (!wpe_display_connect(disp, &de)) {
    g_printerr("Display: %s\n", de ? de->message : "?");
    g_clear_error(&de);
    return 1;
  }
  g_signal_connect(disp, "disconnected",
                   G_CALLBACK(+[](WPEDisplay *, GError *e, gpointer) {
                     if (e)
                       g_warning("Display: %s", e->message);
                     _exit(1);
                   }),
                   nullptr);

  g_signal_connect(
      app.get(), "activate", G_CALLBACK(+[](GApplication *, gpointer ud) {
        auto *disp =
            G_TYPE_CHECK_INSTANCE_CAST(ud, WPE_TYPE_DISPLAY, WPEDisplay);
        wpe_settings_set_boolean(wpe_display_get_settings(disp),
                                 WPE_SETTING_CREATE_VIEWS_WITH_A_TOPLEVEL,
                                 FALSE, WPE_SETTINGS_SOURCE_APPLICATION,
                                 nullptr);
        g_st = g_new0(State, 1);
        g_st->disp = disp;
        g_st->ns = createNS();
        g_st->ctx = WEBKIT_WEB_CONTEXT(
            g_object_new(WEBKIT_TYPE_WEB_CONTEXT,
                         tz ? "time-zone-override" : nullptr, tz, nullptr));
        g_st->set = createSet();
        g_st->ucm = createUCM();
        g_st->tabs = g_hash_table_new(g_direct_hash, g_direct_equal);
        guint w = winW ? winW : 1400;
        guint h = winH ? winH : (900 + kChromeH);
        g_st->tl = wpe_display_create_toplevel(disp, 0);
        wpe_toplevel_resize(g_st->tl, w, h);
        if (windowMax)
          wpe_toplevel_maximize(g_st->tl);
        if (windowFull)
          wpe_toplevel_fullscreen(g_st->tl);
        wpe_toplevel_set_title(g_st->tl, defaultTitle);
        g_st->cucm = webkit_user_content_manager_new();
        g_st->chrome = WEBKIT_WEB_VIEW(g_object_new(
            WEBKIT_TYPE_WEB_VIEW, "web-context", g_st->ctx, "network-session",
            g_st->ns, "settings", g_st->set, "user-content-manager", g_st->cucm,
            "display", disp, nullptr));
        webkit_user_content_manager_register_script_message_handler(
            g_st->cucm, "nav", nullptr);
        g_signal_connect(g_st->cucm, "script-message-received::nav",
                         G_CALLBACK(onNav), g_st);
        auto *cwv = webkit_web_view_get_wpe_view(g_st->chrome);
        g_object_set_data(G_OBJECT(g_st->tl), "chrome-wpe-view", cwv);
        g_signal_connect(
            g_st->tl, "resized", G_CALLBACK(+[](WPEToplevel *tl, gpointer) {
              auto *cv =
                  WPE_VIEW(g_object_get_data(G_OBJECT(tl), "chrome-wpe-view"));
              Tab *a = (Tab *)g_hash_table_lookup(
                  g_st->tabs, GINT_TO_POINTER(g_st->activeId));
              layout(tl, cv, a ? a->wv2 : nullptr);
              if (cv)
                mapToplevel(cv);
            }),
            nullptr);
        g_signal_connect(cwv, "event", G_CALLBACK(onEvent), g_st);
        g_signal_connect(cwv, "closed", G_CALLBACK(+[](WPEView *, gpointer) {
                           g_application_quit(g_app);
                         }),
                         nullptr);
        wpe_view_set_auto_resize_with_toplevel(cwv, FALSE);
        wpe_view_set_toplevel(cwv, g_st->tl);
        webkit_web_view_load_html(g_st->chrome, chromeHTML, "webkit://chrome");
        const char *su = "about:blank";
        if (uriArgs && uriArgs[0]) {
          const char *u = uriArgs[0];
          su = g_str_equal(u, "about:gpu") ? "webkit://gpu" : u;
        }
        char *suc = g_strdup(su);
        g_signal_connect(
            g_st->chrome, "load-changed",
            G_CALLBACK(+[](WebKitWebView *v, WebKitLoadEvent ev, gpointer ud) {
              auto *u = (char *)ud;
              if (ev == WEBKIT_LOAD_FINISHED) {
                g_st->chromeReady = TRUE;
                if (g_st->pTabs) {
                  chromeRunJS(g_st, g_st->pTabs);
                  g_free(g_st->pTabs);
                  g_st->pTabs = nullptr;
                }
                if (g_st->pUrl) {
                  chromeSetUrl(g_st, g_st->pUrl);
                  g_free(g_st->pUrl);
                  g_st->pUrl = nullptr;
                }
                if (g_hash_table_size(g_st->tabs) == 0)
                  newTab(g_st, u, nullptr);
                g_signal_handlers_disconnect_by_data(v, u);
                g_free(u);
              }
            }),
            suc);
        g_idle_add_full(
            G_PRIORITY_HIGH_IDLE,
            [](gpointer d) -> gboolean {
              mapToplevel(WPE_VIEW(d));
              return G_SOURCE_REMOVE;
            },
            g_object_ref(cwv), [](gpointer d) { g_object_unref(G_OBJECT(d)); });
        g_object_unref(g_st->set);
      }),
      g_object_ref(disp));

  g_application_run(app.get(), 0, nullptr);
  if (g_st) {
    if (g_st->tabs)
      g_hash_table_destroy(g_st->tabs);
    if (g_st->order)
      g_list_free(g_st->order);
    g_clear_object(&g_st->chrome);
    g_clear_object(&g_st->cucm);
    g_clear_object(&g_st->ucm);
    g_clear_object(&g_st->ctx);
    g_clear_object(&g_st->ns);
    g_object_unref(g_st->tl);
    g_free(g_st);
  }
  if (g_dummyBuf)
    wl_buffer_destroy(g_dummyBuf);
  g_app = nullptr;
  g_object_unref(disp);
  g_application_release(app.get());
  return 0;
}
