(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)

(** Bash/POSIX shell emitter (heavily restricted MVP).

    Bash is genuinely limited: no first-class numbers, no closures, no
    structures. We accept Int-only programs whose entry returns Int (used
    as the shell exit code). Functions are emitted as Bash functions that
    [echo] their result; callers read it via [$(fn args)]. *)

open Ast

exception Bash_unsupported of string
let unsupported m = raise (Bash_unsupported m)

let bash_reserved = [
  "if"; "then"; "else"; "elif"; "fi"; "case"; "esac";
  "for"; "while"; "until"; "do"; "done"; "function"; "return";
  "in"; "break"; "continue"; "exit"; "true"; "false";
]

let mangle s = if List.mem s bash_reserved then s ^ "_" else s

(* Bash arithmetic only: every expression must evaluate to an integer. *)
let rec gen_expr (e : expr) : string =
  match e with
  | ExprLit (LitInt (n, _))     -> string_of_int n
  | ExprLit (LitBool (true, _)) -> "1"
  | ExprLit (LitBool (false, _))-> "0"
  | ExprLit _ -> unsupported "Bash backend accepts only Int/Bool literals"
  | ExprVar id -> "$" ^ mangle id.name
  | ExprBinary (a, op, b) ->
      let s = match op with
        | OpAdd -> "+" | OpSub -> "-" | OpMul -> "*" | OpDiv -> "/" | OpMod -> "%"
        | OpEq  -> "==" | OpNe -> "!="
        | OpLt  -> "<" | OpLe -> "<=" | OpGt -> ">" | OpGe -> ">="
        | OpAnd -> "&&" | OpOr -> "||"
        | OpBitAnd -> "&" | OpBitOr -> "|" | OpBitXor -> "^"
        | OpShl -> "<<" | OpShr -> ">>"
        | OpConcat -> unsupported "string concat not supported in Bash backend"
      in
      "(" ^ gen_expr a ^ " " ^ s ^ " " ^ gen_expr b ^ ")"
  | ExprUnary (OpNeg, x) -> "(-" ^ gen_expr x ^ ")"
  | ExprUnary (OpNot, x) -> "(! " ^ gen_expr x ^ ")"
  | ExprUnary _ -> unsupported "unary op not supported in Bash backend"
  | ExprApp (callee, args) ->
      let name = match callee with
        | ExprVar id -> id.name
        | _ -> unsupported "indirect call not supported in Bash backend"
      in
      let arg_strs = List.map gen_expr args in
      (* Call a Bash function and capture its echo: $(fn 1 2) *)
      Printf.sprintf "$(%s %s)" (mangle name) (String.concat " " arg_strs)
  | ExprIf { ei_cond; ei_then; ei_else } ->
      let c = gen_expr ei_cond in
      let t = gen_expr ei_then in
      let f = match ei_else with Some e -> gen_expr e | None -> "0" in
      (* Bash arithmetic ternary: $((c ? t : f)) — works in all $((...)) *)
      Printf.sprintf "(%s ? %s : %s)" c t f
  | ExprLet { el_pat = PatVar _; _ } ->
      unsupported "expression-position let not supported in Bash backend"
  | ExprSpan (inner, _) -> gen_expr inner
  | ExprBlock _ -> unsupported "block expressions not supported in Bash backend"
  | _ -> unsupported "expression form not supported in Bash backend"

let gen_function (fd : fn_decl) : string =
  let name = mangle fd.fd_name.name in
  let params = fd.fd_params in
  let body_expr = match fd.fd_body with
    | FnExpr e -> e
    | FnBlock { blk_stmts = []; blk_expr = Some e } -> e
    | FnBlock _ ->
        unsupported "Bash backend accepts only single-expression function bodies"
    | FnExtern ->
        unsupported "Bash backend has no story for `extern fn` (no host-link concept)"
  in
  let body_str = gen_expr body_expr in
  let buf = Buffer.create 128 in
  Buffer.add_string buf (name ^ "() {\n");
  List.iteri (fun i (p : param) ->
    Buffer.add_string buf
      (Printf.sprintf "  local %s=$%d\n" (mangle p.p_name.name) (i + 1))
  ) params;
  Buffer.add_string buf (Printf.sprintf "  echo $((%s))\n}\n\n" body_str);
  Buffer.contents buf

let generate (program : program) (_symbols : Symbol.t) : string =
  let buf = Buffer.create 1024 in
  Buffer.add_string buf "#!/usr/bin/env bash\n";
  Buffer.add_string buf "# Generated by AffineScript compiler\n";
  Buffer.add_string buf "# SPDX-License-Identifier: MPL-2.0\n";
  Buffer.add_string buf "set -euo pipefail\n\n";
  List.iter (function
    | TopFn fd -> Buffer.add_string buf (gen_function fd)
    | _ -> ()
  ) program.prog_decls;
  let has_main = List.exists (function
    | TopFn fd -> fd.fd_name.name = "main"
    | _ -> false) program.prog_decls in
  if has_main then Buffer.add_string buf "exit $(main)\n";
  Buffer.contents buf

let codegen_bash (program : program) (symbols : Symbol.t) : (string, string) result =
  try Ok (generate program symbols)
  with
  | Bash_unsupported msg -> Error ("Bash backend: " ^ msg)
  | Failure msg          -> Error ("Bash codegen error: " ^ msg)
  | e                    -> Error ("Bash codegen error: " ^ Printexc.to_string e)
