#!/usr/bin/env python3
# Inject whitepaper-specific CSS overrides into the rst2html5 output:
#
#   1. Embed the ChunkFive webfont (base64 woff2) and assign it to h1 /
#      h2, so whitepaper titles read in the same voice as the homepage
#      logo instead of docutils's default serif fallback.
#
#   2. Cancel docutils's responsive.css `body > *` rule for the
#      uncloseai floating button — that rule slaps `padding: 0.5rem
#      calc(29% - 7.2rem)` onto every direct child of <body>, which
#      blows the 121px button up to ~40% of the viewport width. Scoped
#      override plus `all: revert` so the button falls back to its own
#      stylesheet's intended sizing.
#
# Usage: inject-whitepaper-css.py <html-file> <chunkfive-woff2-path>

import base64
import os
import sys


TEMPLATE = """\
<style>
@font-face {{
  font-family: 'chunkfive';
  src: url(data:font/woff2;base64,{woff2_b64}) format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}}
main > h1, main > h2, section > h2 {{
  font-family: 'chunkfive', Georgia, serif;
  font-weight: normal;
  letter-spacing: 0.01em;
}}
/* Declarative period on the whitepaper title picks up the same brand
   green (#227842) as the homepage "lumbda." period. */
h1.title .period {{
  color: #227842;
}}
/* docutils's responsive.css applies a blanket rule to every `body > *`
   that sets `background-color: white; line-height: 1.6; padding: 0.5rem
   calc(29% - 7.2rem); margin: auto; max-width: 100rem`. The uncloseai
   floating button gets appended to <body> and inherits those rules —
   exploding it to nearly half the viewport and painting its background
   white (which hides the black pill). Override just the damaged
   properties so uncloseai's own stylesheet still supplies position,
   width, height, background, border, and color. */
body > button#floating-ai-button,
body > .uncloseai-floating-button {{
  padding: 0 !important;
  margin: 0 !important;
  max-width: calc(100vw - 20px) !important;
  background-color: unset !important;
  line-height: normal !important;
}}
/* Click-to-zoom lightbox for diagrams. */
main img, section img {{
  cursor: zoom-in;
}}
.wp-lightbox {{
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.92);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 10001;
  cursor: zoom-out;
  padding: 1.5rem;
}}
.wp-lightbox.open {{ display: flex; }}
.wp-lightbox img {{
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6);
}}
/* Left-rail table of contents. Cancels the same docutils responsive.css
   `body > *` blanket rule that broke the uncloseai button (padding /
   margin / max-width / background / line-height) and then lays the
   sidebar out as a fixed left rail on wide viewports. Below 1100px the
   sidebar collapses behind a toggle button so it cannot crowd the
   single-column reading flow on phones and narrow laptops. */
body > .wp-toc {{
  all: revert;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 280px;
  padding: 1.25rem 1rem 1.25rem 1.5rem;
  margin: 0;
  background: #fafafa;
  border-right: 1px solid #e5e5e5;
  overflow-y: auto;
  overflow-x: hidden;
  z-index: 50;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica,
               Arial, sans-serif;
  font-size: 0.82rem;
  line-height: 1.4;
  box-sizing: border-box;
}}
.wp-toc-title {{
  font-family: 'chunkfive', Georgia, serif;
  font-size: 1.15rem;
  margin: 0 0 0.6rem 0;
  color: #227842;
  letter-spacing: 0.02em;
}}
.wp-toc-title .period {{ color: #227842; }}
.wp-toc ol {{
  list-style: none;
  padding: 0;
  margin: 0;
}}
.wp-toc li {{
  margin: 0;
}}
.wp-toc a {{
  display: block;
  padding: 0.22rem 0.4rem;
  color: #333;
  text-decoration: none;
  border-left: 2px solid transparent;
  border-radius: 0 3px 3px 0;
  word-wrap: break-word;
}}
.wp-toc a:hover {{
  background: #efefef;
  color: #000;
}}
.wp-toc a.active {{
  background: #e8f1ec;
  color: #155c30;
  border-left-color: #227842;
  font-weight: 600;
}}
.wp-toc li.lvl-3 a {{
  padding-left: 1.2rem;
  font-size: 0.78rem;
  color: #555;
}}
.wp-toc li.lvl-3 a.active {{
  color: #155c30;
}}
/* Toggle button — hidden on wide viewports, shown when the sidebar
   collapses. Same `all: revert` trick so docutils does not stretch it. */
body > .wp-toc-toggle {{
  all: revert;
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 51;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid #cfcfcf;
  border-radius: 4px;
  background: #ffffff;
  color: #227842;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  display: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}}
body > .wp-toc-backdrop {{
  all: revert;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  z-index: 49;
  display: none;
}}
@media (min-width: 1100px) {{
  /* Push the centered docutils column right so it never slides under
     the rail. The blanket `body > *` rule already centers content with
     huge calc() padding, so we just add a left margin to body. */
  body {{ margin-left: 280px; }}
  /* The uncloseai floating button is `position: fixed; left: ...`, so
     it sits independent of body margin. No further adjustment. */
}}
@media (max-width: 1099px) {{
  body > .wp-toc {{
    transform: translateX(-100%);
    transition: transform 0.2s ease-out;
    width: min(85vw, 320px);
    box-shadow: 2px 0 8px rgba(0, 0, 0, 0.12);
  }}
  body > .wp-toc.open {{ transform: translateX(0); }}
  body > .wp-toc-toggle {{ display: block; }}
  body > .wp-toc-backdrop.open {{ display: block; }}
}}
</style>
<script>
(function () {{
  if (window.__wpLightbox) return;
  window.__wpLightbox = true;
  document.addEventListener("DOMContentLoaded", function () {{
    var imgs = document.querySelectorAll("main img, section img");
    if (!imgs.length) return;
    var overlay = document.createElement("div");
    overlay.className = "wp-lightbox";
    var big = document.createElement("img");
    big.alt = "";
    overlay.appendChild(big);
    document.body.appendChild(overlay);
    function close() {{ overlay.classList.remove("open"); big.src = ""; }}
    overlay.addEventListener("click", close);
    document.addEventListener("keydown", function (e) {{
      if (e.key === "Escape") close();
    }});
    imgs.forEach(function (img) {{
      img.addEventListener("click", function () {{
        big.src = img.currentSrc || img.src;
        overlay.classList.add("open");
      }});
    }});
  }});
}})();
(function () {{
  if (window.__wpToc) return;
  window.__wpToc = true;
  document.addEventListener("DOMContentLoaded", function () {{
    var main = document.querySelector("main");
    if (!main) return;
    // Pick up only the top two heading levels — section-id <section>s
    // that wrap <h2> (chapters) and <h3> (sections). Anything deeper
    // creates noise in a left rail. Skip the doc title <h1> since the
    // sidebar header already labels the doc.
    var sections = main.querySelectorAll("section[id] > h2, section[id] > h3");
    if (sections.length < 3) return;
    var nav = document.createElement("nav");
    nav.className = "wp-toc";
    nav.setAttribute("aria-label", "Table of contents");
    var title = document.createElement("p");
    title.className = "wp-toc-title";
    title.innerHTML = 'lumbda<span class="period" aria-hidden="true">.</span>';
    nav.appendChild(title);
    var list = document.createElement("ol");
    var byId = {{}};
    sections.forEach(function (h) {{
      var sec = h.parentElement;
      var id = sec.id;
      if (!id) return;
      var lvl = h.tagName === "H2" ? 2 : 3;
      var li = document.createElement("li");
      li.className = "lvl-" + lvl;
      var a = document.createElement("a");
      a.href = "#" + id;
      a.textContent = h.textContent.trim();
      li.appendChild(a);
      list.appendChild(li);
      byId[id] = a;
    }});
    nav.appendChild(list);
    var backdrop = document.createElement("div");
    backdrop.className = "wp-toc-backdrop";
    var toggle = document.createElement("button");
    toggle.className = "wp-toc-toggle";
    toggle.setAttribute("aria-label", "Open table of contents");
    toggle.textContent = "☰"; // hamburger
    function close() {{
      nav.classList.remove("open");
      backdrop.classList.remove("open");
    }}
    function open() {{
      nav.classList.add("open");
      backdrop.classList.add("open");
    }}
    toggle.addEventListener("click", function () {{
      if (nav.classList.contains("open")) close(); else open();
    }});
    backdrop.addEventListener("click", close);
    // Auto-close after navigating from a link on narrow viewports.
    nav.addEventListener("click", function (e) {{
      if (e.target.tagName === "A" && window.innerWidth < 1100) close();
    }});
    document.body.insertBefore(nav, document.body.firstChild);
    document.body.appendChild(backdrop);
    document.body.appendChild(toggle);
    // Active-section tracking. IntersectionObserver picks the
    // highest-up section currently in view; falls back to a scroll
    // handler when IO is unavailable (very old browsers).
    var active = null;
    function setActive(id) {{
      if (active === id) return;
      if (active && byId[active]) byId[active].classList.remove("active");
      active = id;
      if (byId[id]) {{
        byId[id].classList.add("active");
        // Keep the active link in view inside the rail.
        var rect = byId[id].getBoundingClientRect();
        var navRect = nav.getBoundingClientRect();
        if (rect.top < navRect.top || rect.bottom > navRect.bottom) {{
          byId[id].scrollIntoView({{ block: "nearest" }});
        }}
      }}
    }}
    if ("IntersectionObserver" in window) {{
      var visible = new Set();
      var io = new IntersectionObserver(function (entries) {{
        entries.forEach(function (e) {{
          if (e.isIntersecting) visible.add(e.target.id);
          else visible.delete(e.target.id);
        }});
        // Pick the visible section that appears earliest in the doc.
        var first = null;
        sections.forEach(function (h) {{
          var sid = h.parentElement.id;
          if (visible.has(sid) && first === null) first = sid;
        }});
        if (first) setActive(first);
      }}, {{ rootMargin: "-80px 0px -60% 0px", threshold: 0 }});
      sections.forEach(function (h) {{ io.observe(h.parentElement); }});
    }} else {{
      window.addEventListener("scroll", function () {{
        var y = window.scrollY + 100;
        var current = null;
        sections.forEach(function (h) {{
          if (h.parentElement.offsetTop <= y) current = h.parentElement.id;
        }});
        if (current) setActive(current);
      }}, {{ passive: true }});
    }}
  }});
}})();
</script>
"""


def main(argv):
    if len(argv) != 3:
        sys.stderr.write("usage: inject-whitepaper-css.py <html-file> <chunkfive-woff2-path>\n")
        return 2
    html_path, woff2_path = argv[1], argv[2]
    if not os.path.isfile(woff2_path):
        sys.stderr.write(f"inject-whitepaper-css: chunkfive missing: {woff2_path}\n")
        return 1
    with open(woff2_path, "rb") as f:
        woff2_b64 = base64.b64encode(f.read()).decode()
    style_block = TEMPLATE.format(woff2_b64=woff2_b64)
    with open(html_path, "r") as f:
        html = f.read()
    if "</head>" not in html:
        sys.stderr.write("inject-whitepaper-css: no </head> found\n")
        return 1
    html = html.replace("</head>", style_block + "</head>", 1)
    # Wrap the trailing period on the `<h1 class="title">lumbda.</h1>`
    # heading in a span so CSS can color just the period green. Docutils
    # emits exactly this markup for the top-level RST title.
    html = html.replace(
        '<h1 class="title">lumbda.</h1>',
        '<h1 class="title">lumbda<span class="period" aria-hidden="true">.</span></h1>',
        1,
    )
    with open(html_path, "w") as f:
        f.write(html)
    print(f"inject-whitepaper-css: injected {len(style_block)} bytes into {html_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
