# Single-binary native build (jolt-a7ds, epic jolt-5vsp). The AOT path
# (cgen.janet) compiles an app's numeric-leaf fns into one prebuilt .so + a
# manifest, loaded at deploy with no cc. This module fuses that native code into
# the jpm-built executable so an app ships as ONE static file: no sidecar .so, no
# toolchain on the target.
#
# Shape (validated by the spike, docs/foundational-runtime-lever1-native-codegen.md):
# a staging dir with `src`/`jolt-core` symlinks into the jolt source tree, the
# generated cg.c (native module of the app's hot fns), an uberscript-style bundle
# of the app source, and an entry that bakes the jolt runtime, installs the native
# fns as var roots (:cgen-prebuilt), and runs -main. `jpm build` static-links cg.a
# (declare-native builds it) into the final exe. Build needs cc; the result does
# not. See build-app for the driver and `jolt cgen-build` (main.janet) for the CLI.
(import ./api :as api)
(import ./types :as t)
(import ./cgen :as cgen)

# --- jolt source location ----------------------------------------------------
# Baking the runtime into the app binary needs the jolt source tree (src/jolt +
# jolt-core). The shipped binary doesn't carry its own source, so locate a
# checkout: JOLT_HOME wins, else walk up from cwd for the marker files.
(defn- jolt-home? [d]
  (and (os/stat (string d "/src/jolt/api.janet"))
       (os/stat (string d "/jolt-core"))))

(defn find-jolt-home
  "Directory of a jolt source checkout (has src/jolt + jolt-core), or nil.
  JOLT_HOME overrides; otherwise walk up from `start` (default cwd)."
  [&opt start]
  (def env-home (os/getenv "JOLT_HOME"))
  (cond
    (and env-home (jolt-home? env-home)) env-home
    (do
      (var d (or start (os/cwd)))
      (var found nil)
      (while (and (not found) (> (length d) 1))
        (if (jolt-home? d)
          (set found d)
          (set d (string/slice d 0 (max 0 (last (string/find-all "/" d)))))))
      found)))

# --- generation --------------------------------------------------------------
(defn gen-c-and-manifest
  "collected: [{:ns :name :ir} ...] (numeric-leaf fns from a :cgen-collect? load).
  Emit the C source for ONE native module exporting them all, plus a positional
  manifest [{:ns :name :sym} ...] mapping each app fn to its C symbol. Mirrors
  cgen/aot-build but yields C source (compiled at build) rather than a .so."
  [collected]
  (def entries @[])
  (def manifest @[])
  (eachp [i e] collected
    (def sym (string "f" i))
    (array/push entries {:sym sym :ir (e :ir)})
    (array/push manifest {:ns (e :ns) :name (e :name) :sym sym}))
  {:c-src (cgen/gen-c-module entries) :manifest manifest})

(defn bundle-source
  "Concatenate the app's loaded source files (load order: deps before dependents)
  into one closed-world bundle the baked entry can eval. Verbatim — DCE is the
  uberscript path's concern, not needed here for correctness."
  [files]
  (def buf @"")
  (each f files
    (buffer/push-string buf "; --- " f " ---\n" (slurp f) "\n"))
  (string buf))

(defn collect-app
  "Load main-ns through the full pipeline with :cgen-collect? on (records the
  numeric-leaf fns' IR without swapping roots), returning the collected fns and
  the source files loaded (for the bundle). source-roots seed :source-paths so
  the app + its deps resolve. Throws on a load error."
  [main-ns source-roots]
  (def ctx (api/init-cached {:compile? true}))           # discarded; cached for speed
  (each r source-roots
    (when (> (length r) 0) (array/push (get (ctx :env) :source-paths) r)))
  (put (ctx :env) :direct-linking? true)
  (put (ctx :env) :cgen-collect? true)
  (put (ctx :env) :loaded-files @[])
  (api/load-string ctx (string "(require '[" main-ns "])") "<cgen-build>")
  (def seen @{})
  (def files @[])
  (each f (get (ctx :env) :loaded-files)
    (unless (get seen f) (put seen f true) (array/push files f)))
  {:collected (or (get (ctx :env) :cgen-collected) @[]) :files files})

(defn run-main!
  "Runtime helper baked into a generated app binary: bind *command-line-args* to
  args (a tuple/array of strings) and invoke main-ns/-main."
  [ctx main-ns args]
  (def core (t/ctx-find-ns ctx "clojure.core"))
  (t/ns-intern core "*command-line-args*" (tuple/slice (tuple ;args)))
  (api/eval-string ctx (string "(apply " main-ns "/-main *command-line-args*)")))

(defn gen-entry-source
  "The generated main.janet: imports the jolt runtime + the static cg module,
  builds qname->cfunction from the manifest, bakes the ctx with the app compiled
  and the native fns installed as roots, and runs -main at startup. Strings are
  emitted with %j so embedded source/manifest can't break the quoting."
  [main-ns app-source manifest]
  (string
    "# Generated by `jolt cgen-build` — do not edit.\n"
    "(import \"./src/jolt/api\" :as api)\n"
    "(import \"./src/jolt/cgen_build\" :as cb)\n"
    "(import \"./cg\" :as cg)\n\n"
    "(def manifest " (string/format "%j" manifest) ")\n"
    "(def app-source " (string/format "%j" app-source) ")\n"
    "(def main-ns " (string/format "%j" main-ns) ")\n\n"
    "(def cg-env (require \"./cg\"))\n"
    "(def prebuilt\n"
    "  (let [tb @{}]\n"
    "    (each e manifest\n"
    "      (when-let [cell (get cg-env (symbol (e :sym)))]\n"
    "        (put tb (string (e :ns) \"/\" (e :name)) (get cell :value))))\n"
    "    tb))\n\n"
    "(def ctx (api/init {:compile? true}))\n"
    "(put (ctx :env) :direct-linking? true)\n"
    "(put (ctx :env) :cgen-prebuilt prebuilt)\n"
    "(api/load-string ctx app-source \"app.clj\")\n\n"
    "(defn main [&]\n"
    "  (def args (or (dyn :args) @[]))\n"
    "  (cb/run-main! ctx main-ns (slice args 1)))\n"))

(defn gen-project-source
  "The generated project.janet. The executable's entry imports cg at build
  (dofile) and static-links cg.a, so both must build first — jpm infers neither,
  and cg.a isn't pulled into a non-release `build` task, so wire deps explicitly."
  [exe-name]
  (string
    "(declare-project :name \"jolt-cgen-app\")\n\n"
    "(declare-native :name \"cg\" :source @[\"cg.c\"])\n\n"
    "(declare-executable :name " (string/format "%j" exe-name) " :entry \"main.janet\")\n\n"
    "(add-dep \"./" exe-name "\" \"./cg.so\")\n"
    "(add-dep \"./" exe-name "\" \"./cg.a\")\n"))

# --- driver ------------------------------------------------------------------
(defn- mkdir-p [path]
  (def parts (filter |(not (empty? $)) (string/split "/" path)))
  (var acc (if (string/has-prefix? "/" path) "" "."))
  (each p parts (set acc (string acc "/" p)) (os/mkdir acc)))

(defn- rm-rf [path]
  # lstat, never stat: the staging dir holds symlinks INTO the jolt tree
  # (src, jolt-core); following them would delete real source. A symlink — even
  # one pointing at a directory — is removed with os/rm, not recursed into.
  (when-let [st (os/lstat path)]
    (if (= :directory (st :mode))
      (do (each e (os/dir path) (rm-rf (string path "/" e))) (os/rmdir path))
      (os/rm path))))

(defn build-app
  "Build main-ns into a single static executable at :out. Needs cc + jpm at build
  time; the result needs neither. opts:
    :main        app namespace (string, required)
    :out         output binary path (required)
    :jolt-home   jolt source checkout (default: find-jolt-home)
    :source-roots extra source roots for the app + deps (default from JOLT_PATH)
    :stage       staging dir (default: a fresh temp dir; removed unless :keep)
    :keep        keep the staging dir for inspection
  Returns {:out <path> :native-count <n> :stage <dir>}."
  [opts]
  (def main-ns (assert (opts :main) "cgen-build: :main namespace required"))
  (def out (assert (opts :out) "cgen-build: :out path required"))
  (def home (or (opts :jolt-home) (find-jolt-home)))
  (assert home "cgen-build: could not locate jolt source (set JOLT_HOME)")
  (def source-roots
    (or (opts :source-roots)
        (let [jp (os/getenv "JOLT_PATH")]
          (if jp (filter |(> (length $) 0) (string/split ":" jp)) @[]))))
  (def exe-name "app")

  # 1. Collect the app's numeric-leaf fns + its full source bundle.
  (def {:collected collected :files files} (collect-app main-ns source-roots))
  (def {:c-src c-src :manifest manifest} (gen-c-and-manifest collected))
  (def app-source (bundle-source files))

  # 2. Stage: symlinks into the jolt tree + the generated build inputs.
  (def stage
    (or (opts :stage)
        (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-cgen-build-"
                (string/format "%x" (band (hash (string main-ns out)) 0x7fffffff)))))
  (rm-rf stage)
  (mkdir-p stage)
  (os/symlink (string home "/src") (string stage "/src"))
  (os/symlink (string home "/jolt-core") (string stage "/jolt-core"))
  (spit (string stage "/cg.c") c-src)
  (spit (string stage "/main.janet") (gen-entry-source main-ns app-source manifest))
  (spit (string stage "/project.janet") (gen-project-source exe-name))

  # 3. jpm build in the staging dir (cwd + JANET_BUILDPATH=. so artifacts land
  #    beside the entry, where ./cg and cg.a resolve).
  (def prev-cwd (os/cwd))
  (def prev-bp (os/getenv "JANET_BUILDPATH"))
  (defer (do (os/cd prev-cwd) (os/setenv "JANET_BUILDPATH" prev-bp))
    (os/cd stage)
    (os/setenv "JANET_BUILDPATH" ".")
    # --workers=1: the build shells out to cc; one worker keeps it from saturating
    # every core (it runs inside the parallel test gate, where that starves
    # siblings into timeouts). Only a few small C files, so the cost is marginal.
    (def code (os/execute ["jpm" "--workers=1" "build"] :p))
    (os/cd prev-cwd)
    (unless (= 0 code) (error (string "cgen-build: jpm build failed (" code ")"))))

  # 4. Install the binary, then clean up staging.
  (def built (string stage "/" exe-name))
  (assert (os/stat built) (string "cgen-build: expected binary at " built))
  (let [od (string/slice out 0 (max 0 (last (or (string/find-all "/" out) [0]))))]
    (when (> (length od) 0) (mkdir-p od)))
  (spit out (slurp built))
  (os/chmod out 8r755)
  (unless (opts :keep) (rm-rf stage))
  {:out out :native-count (length manifest) :stage stage})
