// elb.el - El Build Coordinator
//
// The build system for El programs. Written in El. Builds El.
//
// Usage:
//   elb                          # build from manifest.el in current dir
//   elb --clean                  # remove generated artifacts and rebuild
//   elb --dry-run                # print actions without executing
//   elb --jobs=N                 # parallel compile jobs (default: 4)
//   elb --out=DIR                # output directory (default: dist)
//   elb --runtime=PATH           # path to el_runtime.c
//
// How it works (the .NET model):
//   1. Read manifest.el to find the entry file
//   2. Walk the import graph depth-first, build topological order
//   3. For each file: if .el is newer than .elh/.c, compile with elc --emit-header
//   4. Link all .c files + el_runtime.c into the final binary
//
// Each module compiles independently - no 128K-line blobs.
// Downstream compilations read .elh headers (function signatures only),
// not source. Incremental: only recompile what changed.

// -- Flags ---------------------------------------------------------------------

fn flag_bool(argv: [String], name: String) -> Bool {
    let n: Int = native_list_len(argv)
    let i = 0
    while i < n {
        let a: String = native_list_get(argv, i)
        if str_eq(a, name) { return true }
        let i = i + 1
    }
    return false
}

fn flag_val(argv: [String], name: String, default_val: String) -> String {
    let n: Int = native_list_len(argv)
    let prefix: String = name + "="
    let i = 0
    while i < n {
        let a: String = native_list_get(argv, i)
        if str_starts_with(a, prefix) {
            return str_slice(a, str_len(prefix), str_len(a))
        }
        let i = i + 1
    }
    return default_val
}

// -- Manifest parsing ----------------------------------------------------------
//
// Read the entry file from manifest.el:
//   build { entry "soul.el" }

fn parse_manifest_entry(src: String) -> String {
    let lines: [String] = str_split(src, "\n")
    let n: Int = native_list_len(lines)
    let i = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let t: String = str_trim(line)
        if str_starts_with(t, "entry ") {
            // entry "soul.el"
            let after: String = str_slice(t, 6, str_len(t))
            let trimmed: String = str_trim(after)
            // strip surrounding quotes
            if str_starts_with(trimmed, "\"") {
                let inner: String = str_slice(trimmed, 1, str_len(trimmed))
                let q: Int = str_index_of(inner, "\"")
                if q >= 0 {
                    return str_slice(inner, 0, q)
                }
            }
        }
        let i = i + 1
    }
    return ""
}

// parse_manifest_c_sources - collect all `c_source "path"` lines from the
// build block.  Returns a flat list of path strings.
fn parse_manifest_c_sources(src: String) -> [String] {
    let result: [String] = native_list_empty()
    let lines: [String] = str_split(src, "\n")
    let n: Int = native_list_len(lines)
    let i = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let t: String = str_trim(line)
        if str_starts_with(t, "c_source ") {
            let after: String = str_slice(t, 9, str_len(t))
            let trimmed: String = str_trim(after)
            if str_starts_with(trimmed, "\"") {
                let inner: String = str_slice(trimmed, 1, str_len(trimmed))
                let q: Int = str_index_of(inner, "\"")
                if q >= 0 {
                    let path: String = str_slice(inner, 0, q)
                    let result = native_list_append(result, path)
                }
            }
        }
        let i = i + 1
    }
    return result
}

fn parse_manifest_name(src: String) -> String {
    let lines: [String] = str_split(src, "\n")
    let n: Int = native_list_len(lines)
    let i = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let t: String = str_trim(line)
        if str_starts_with(t, "package ") {
            let after: String = str_slice(t, 8, str_len(t))
            let trimmed: String = str_trim(after)
            if str_starts_with(trimmed, "\"") {
                let inner: String = str_slice(trimmed, 1, str_len(trimmed))
                let q: Int = str_index_of(inner, "\"")
                if q >= 0 {
                    return str_slice(inner, 0, q)
                }
            }
        }
        let i = i + 1
    }
    return "out"
}

// -- Path helpers ---------------------------------------------------------------

fn dirname_of(path: String) -> String {
    let n: Int = str_len(path)
    let i: Int = n - 1
    while i >= 0 {
        let c: String = str_slice(path, i, i + 1)
        if str_eq(c, "/") {
            return str_slice(path, 0, i)
        }
        let i = i - 1
    }
    return "."
}

fn basename_noext(path: String) -> String {
    // strip directory
    let n: Int = str_len(path)
    let last_slash: Int = -1
    let i = 0
    while i < n {
        let c: String = str_slice(path, i, i + 1)
        if str_eq(c, "/") { let last_slash = i }
        let i = i + 1
    }
    let base: String = str_slice(path, last_slash + 1, n)
    // strip .el extension
    let bn: Int = str_len(base)
    if str_ends_with(base, ".el") {
        return str_slice(base, 0, bn - 3)
    }
    return base
}

fn path_with_ext(path: String, ext: String) -> String {
    let n: Int = str_len(path)
    if str_ends_with(path, ".el") {
        return str_slice(path, 0, n - 3) + ext
    }
    return path + ext
}

fn file_is_newer(a: String, b: String) -> Bool {
    // Returns true if file a is newer than file b, or if b doesn't exist.
    // Uses exec_capture with stat to compare modification times.
    let cmd: String = "test -f " + b + " && test " + a + " -nt " + b + " && echo yes || echo no"
    let result: String = str_trim(exec_capture(cmd))
    if str_eq(result, "yes") { return true }
    // b doesn't exist - check with test -f
    let exist_cmd: String = "test -f " + b + " && echo exists || echo missing"
    let exist: String = str_trim(exec_capture(exist_cmd))
    if str_eq(exist, "missing") { return true }
    return false
}

// -- Import graph walker --------------------------------------------------------
//
// Walk import statements in each .el file to build the dependency graph.
// Returns a list of absolute paths in topological order (deps before dependents).

fn parse_import_path(line: String, dir: String) -> String {
    let t: String = str_trim(line)
    if str_starts_with(t, "import \"") {
        let after: String = str_slice(t, 8, str_len(t))
        let q: Int = str_index_of(after, "\"")
        if q > 0 {
            let mod: String = str_slice(after, 0, q)
            return dir + "/" + mod
        }
    }
    if str_starts_with(t, "from ") {
        let after: String = str_slice(t, 5, str_len(t))
        let sp: Int = str_index_of(after, " ")
        if sp > 0 {
            let mod: String = str_trim(str_slice(after, 0, sp))
            if !str_eq(mod, "") {
                return dir + "/" + mod + ".el"
            }
        }
    }
    return ""
}

fn walk_imports(src_path: String, visited: [String], order: [String]) -> Map<String, Any> {
    // Dedup check
    let n: Int = native_list_len(visited)
    let i = 0
    while i < n {
        let v: String = native_list_get(visited, i)
        if str_eq(v, src_path) {
            return { "visited": visited, "order": order }
        }
        let i = i + 1
    }
    let visited = native_list_append(visited, src_path)

    let source: String = fs_read(src_path)
    if str_eq(source, "") {
        return { "visited": visited, "order": order }
    }
    let dir: String = dirname_of(src_path)
    let lines: [String] = str_split(source, "\n")
    let ln: Int = native_list_len(lines)
    let j = 0
    while j < ln {
        let line: String = native_list_get(lines, j)
        let imp: String = parse_import_path(line, dir)
        if !str_eq(imp, "") {
            let r = walk_imports(imp, visited, order)
            let visited = r["visited"]
            let order = r["order"]
        }
        let j = j + 1
    }
    // Add self after all deps
    let order = native_list_append(order, src_path)
    return { "visited": visited, "order": order }
}

// -- Build ----------------------------------------------------------------------

fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: Bool, verbose: Bool) -> Bool {
    let bname: String = basename_noext(src_path)
    let c_out: String = out_dir + "/" + bname + ".c"
    let elh_out: String = out_dir + "/" + bname + ".elh"
    let err_tmp: String = "/tmp/elb-err-" + bname + ".txt"

    // Check if recompile needed
    if !file_is_newer(src_path, c_out) {
        if verbose {
            println("  skip  " + bname + ".el  (up to date)")
        }
        return true
    }

    // elc streams C to stdout; redirect stderr to a temp file so we can
    // surface the actual error message on failure instead of swallowing it.
    let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp
    println("  compile  " + src_path)

    if dry_run { return true }

    let ret: Int = exec_command(cmd)
    if ret != 0 {
        // Surface the actual compiler error from stderr
        let err_msg: String = str_trim(fs_read(err_tmp))
        if !str_eq(err_msg, "") {
            println(err_msg)
        }
        // Remove partial output so a retry starts clean
        exec_command("rm -f " + c_out + " " + err_tmp)
        println("elb: compile failed: " + src_path)
        return false
    }
    exec_command("rm -f " + err_tmp)

    // Move the generated .elh (written next to the source by elc) into
    // out_dir so that #include "module.elh" lines in the generated .c
    // files resolve correctly when cc is invoked with -I <out_dir>.
    let src_elh: String = path_with_ext(src_path, ".elh")
    let mv_cmd: String = "cp " + src_elh + " " + elh_out + " 2>/dev/null || true"
    exec_command(mv_cmd)

    return true
}

fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir: String, dry_run: Bool) -> Bool {
    let n: Int = native_list_len(c_files)
    let parts: [String] = native_list_empty()
    // Include both the runtime dir (for el_runtime.h) and the output dir
    // (for module.elh cross-module forward declarations).
    // Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored
    // if unsupported but gcc rejects it with an error.
    let bracket_flag: String = "$(cc --version 2>&1 | grep -q clang && printf -- '-fbracket-depth=1024' || true)"
    // On macOS, OpenSSL is not on the default linker path. Detect homebrew
    // prefix and add it if present (no-op on Linux where libssl is in /usr/lib).
    let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)"
    let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
    // Force-include the C-level master declarations header so every translation
    // unit sees all cross-module function signatures. Handles packages (like ELP)
    // where modules call each other without explicit El import statements.
    // The header is generated by elb --gen-decls or manually placed in out_dir.
    let master_decls: String = out_dir + "/elp-c-decls.h"
    let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no"))
    let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" }
    let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
    let i = 0
    while i < n {
        let f: String = native_list_get(c_files, i)
        let parts = native_list_append(parts, f)
        let i = i + 1
    }
    let parts = native_list_append(parts, runtime_path)
    let parts = native_list_append(parts, ossl_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
    let parts = native_list_append(parts, "-o " + out_bin)
    let cmd: String = str_join(parts, " ")
    println("  link     " + out_bin)
    if dry_run { return true }
    let ret: Int = exec_command(cmd)
    if ret != 0 {
        println("elb: link failed")
        return false
    }
    return true
}

// -- Main -----------------------------------------------------------------------

fn main() -> Void {
    let argv: [String] = args()
    let clean: Bool   = flag_bool(argv, "--clean")
    let dry_run: Bool = flag_bool(argv, "--dry-run")
    let verbose: Bool = flag_bool(argv, "--verbose")
    let out_dir: String  = flag_val(argv, "--out", "dist")
    let elc_bin: String  = flag_val(argv, "--elc", "elc")
    let runtime: String  = flag_val(argv, "--runtime", "")

    // Find manifest
    let manifest_src: String = fs_read("manifest.el")
    if str_eq(manifest_src, "") {
        println("elb: no manifest.el found in current directory")
        exit(1)
    }

    let pkg_name: String = parse_manifest_name(manifest_src)
    let entry: String    = parse_manifest_entry(manifest_src)
    let extra_c: [String] = parse_manifest_c_sources(manifest_src)
    if str_eq(entry, "") {
        println("elb: manifest.el has no 'entry' declaration")
        exit(1)
    }

    println("elb: building " + pkg_name + " (entry: " + entry + ")")

    // Locate runtime
    let runtime_path: String = runtime
    if str_eq(runtime_path, "") {
        // Try to find el_runtime.c relative to elc binary
        let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null"))
        if !str_eq(which_out, "") {
            let elc_dir: String = dirname_of(which_out)
            runtime_path = elc_dir + "/../runtime/el_runtime.c"
        }
    }
    // If --runtime points to a directory, auto-locate el_runtime.c inside it.
    // This lets both forms work:
    //   --runtime=/opt/el/runtime          (directory form)
    //   --runtime=/opt/el/runtime/el_runtime.c  (file form)
    if !str_eq(runtime_path, "") {
        let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file"))
        if str_eq(is_dir, "dir") {
            let candidate: String = runtime_path + "/el_runtime.c"
            let has_file: String = str_trim(exec_capture("test -f " + candidate + " && echo yes || echo no"))
            if str_eq(has_file, "yes") {
                let runtime_path = candidate
            }
        }
    }
    if str_eq(runtime_path, "") {
        println("elb: cannot locate el_runtime.c - use --runtime=PATH")
        exit(1)
    }

    // Ensure output directory
    let mkdir_ret: Int = exec_command("mkdir -p " + out_dir)

    // Clean if requested
    if clean {
        println("elb: cleaning " + out_dir)
        if !dry_run {
            let rm_ret: Int = exec_command("rm -f " + out_dir + "/*.c " + out_dir + "/*.elh")
        }
    }

    // Walk import graph from entry file
    let empty_visited: [String] = native_list_empty()
    let empty_order: [String]   = native_list_empty()
    let r = walk_imports(entry, empty_visited, empty_order)
    let order: [String] = r["order"]
    let total: Int = native_list_len(order)
    println("elb: " + native_int_to_str(total) + " modules in build graph")

    // Compile each module
    let c_files: [String] = native_list_empty()
    let i = 0
    let ok = true
    while i < total {
        let src: String = native_list_get(order, i)
        let bname: String = basename_noext(src)
        let c_out: String = out_dir + "/" + bname + ".c"
        let compiled: Bool = compile_module(src, out_dir, elc_bin, dry_run, verbose)
        if !compiled {
            let ok = false
            let i = total
        } else {
            let c_files = native_list_append(c_files, c_out)
        }
        let i = i + 1
    }

    if !ok {
        println("elb: build failed")
        exit(1)
    }

    // Append any extra C sources declared in the manifest (e.g. platform stubs)
    let ei = 0
    let en: Int = native_list_len(extra_c)
    while ei < en {
        let ec: String = native_list_get(extra_c, ei)
        let c_files = native_list_append(c_files, ec)
        let ei = ei + 1
    }

    // Link
    let out_bin: String = out_dir + "/" + pkg_name
    let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
    if !linked {
        println("elb: link failed")
        exit(1)
    }

    println("elb: done -> " + out_bin)
}
