///|
fn human(n : Int32) -> String {
  if n >= 1024 {
    let whole = n / 1024
    let frac = n % 1024 * 10 / 1024
    "\{whole}.\{frac} KiB"
  } else {
    "\{n} B"
  }
}

///|
/// Digits only; used for --budget. Ignores anything that is not 0-9.
fn parse_uint(s : String) -> Int32 {
  let mut n = 0
  for c in s.to_array() {
    let d = c.to_int()
    if d >= 48 && d <= 57 {
      n = n * 10 + (d - 48)
    }
  }
  n
}

///|
fn usage() -> Unit {
  println("usage:")
  println("  wasmslim                  size attribution")
  println(
    "  wasmslim  --check         release gate (exit 1 on error)",
  )
  println("  wasmslim  --check --budget 16384")
  println("  wasmslim  --strip [--out ]")
}

///|
fn main raise {
  let args = @env.args()
  if args.length() < 2 {
    usage()
    return
  }
  let path = args[1]
  let data = @fs.read_file_to_bytes(path)
  let m = @wasm.parse_module(data)
  let total = data.length()

  let mut do_check = false
  let mut do_strip = false
  let mut do_debug = false
  let mut budget = 0
  let mut top_n = 0
  let mut diff_path = ""
  let mut out_path = path + ".stripped.wasm"
  let mut i = 2
  while i < args.length() {
    let a = args[i]
    if a == "--check" {
      do_check = true
    } else if a == "--strip" {
      do_strip = true
    } else if a == "--debug" {
      do_debug = true
    } else if a == "--budget" && i + 1 < args.length() {
      let raw = args[i + 1]
      budget = parse_uint(raw)
      if budget == 0 {
        println(
          "warning: --budget expects a positive integer, got \"" + raw + "\"",
        )
      }
      i = i + 1
    } else if a == "--diff" && i + 1 < args.length() {
      diff_path = args[i + 1]
      i = i + 1
    } else if a == "--top" && i + 1 < args.length() {
      top_n = parse_uint(args[i + 1])
      i = i + 1
    } else if a == "--out" && i + 1 < args.length() {
      out_path = args[i + 1]
      i = i + 1
    }
    i = i + 1
  }

  if do_check {
    let r = @audit.audit(m, budget)
    println("file      \{path}")
    println("size      \{human(total)} (\{total} bytes)")
    println("profile   \{r.profile} (custom sections \{r.custom_pct}%)")
    println(
      "dead code \{r.dead_functions} function(s), \{r.dead_bytes} body bytes",
    )
    if budget > 0 {
      println("budget    \{budget} bytes")
    } else {
      println("budget    not set")
    }
    println("")
    for f in r.findings {
      println("  \{f.level}  \{f.code}  \{f.message}")
    }
    println("")
    let verdict = if @audit.passed(r) { "PASS" } else { "FAIL" }
    println("verdict   \{verdict}")
    if !@audit.passed(r) {
      // abort carries the exit code (1) so CI can gate on it
      abort("audit failed")
    }
    return
  }

  if do_strip {
    let stripped = @wasm.strip_custom(m)
    @fs.write_bytes_to_file(out_path, stripped) catch {
      _ => {
        println("cannot write " + out_path)
        return
      }
    }
    let saved = total - stripped.length()
    println("stripped   " + out_path)
    println("before     " + human(total) + " (" + total.to_string() + " bytes)")
    println(
      "after      " +
      human(stripped.length()) +
      " (" +
      stripped.length().to_string() +
      " bytes)",
    )
    println(
      "saved      " +
      human(saved) +
      " (" +
      (saved * 100 / total).to_string() +
      "%)",
    )
    println("removed    custom sections only; code/type/export untouched")
    return
  }

  if do_debug {
    let bodies = @wasm.function_bodies(m)
    let mut bi = 0
    while bi < bodies.length() {
      let b = bodies[bi]
      let scan = @wasm.scan_body(m.data, b)
      if scan.unsupported.length() > 0 {
        let at = scan.unsupported_at[0]
        let bad = scan.unsupported[0]
        println(
          "func[" +
          bi.to_string() +
          "] body=" +
          b.start.to_string() +
          ".." +
          b.end.to_string() +
          " bad op=" +
          bad.to_string() +
          " at=" +
          at.to_string(),
        )
      }
      bi = bi + 1
    }
    return
  }

  if top_n > 0 {
    let names = @wasm.function_names(m)
    let bodies = @wasm.function_bodies(m)
    let imported = @wasm.imported_function_count(m)
    let sizes : Array[(Int32, Int32)] = []
    let mut bi = 0
    while bi < bodies.length() {
      let b = bodies[bi]
      sizes.push((b.end - b.start, imported + bi))
      bi = bi + 1
    }
    // selection sort by size, descending; N is small
    let mut out_i = 0
    while out_i < top_n && out_i < sizes.length() {
      let mut best = out_i
      let mut j = out_i + 1
      while j < sizes.length() {
        if sizes[j].0 > sizes[best].0 {
          best = j
        }
        j = j + 1
      }
      let tmp = sizes[out_i]
      sizes[out_i] = sizes[best]
      sizes[best] = tmp
      let item = sizes[out_i]
      let nm = @wasm.name_of(names, item.1.reinterpret_as_uint())
      println("\{human(item.0)}  \{nm}")
      out_i = out_i + 1
    }
    return
  }

  if diff_path != "" {
    if !@fs.path_exists(diff_path) {
      println("error: cannot read " + diff_path)
      abort("missing file")
    }
    let old_data = @fs.read_file_to_bytes(diff_path)
    let old_m = @wasm.parse_module(old_data)
    let d = @audit.diff(old_m, m)
    let delta = d.new_size - d.old_size
    println(
      "old       " +
      human(d.old_size) +
      " (" +
      d.old_size.to_string() +
      " bytes)",
    )
    println(
      "new       " +
      human(d.new_size) +
      " (" +
      d.new_size.to_string() +
      " bytes)",
    )
    println(
      "delta     " +
      (if delta >= 0 { "+" } else { "" }) +
      delta.to_string() +
      " bytes",
    )
    println(
      "functions old=" +
      d.old_functions.to_string() +
      " new=" +
      d.new_functions.to_string() +
      " added=" +
      d.added.length().to_string() +
      " removed=" +
      d.removed.length().to_string(),
    )
    if !d.named {
      println(
        "note      at least one side has no name section; functions are aligned by index",
      )
    }
    println("")
    let top = @audit.top_changes(d, 10)
    if top.length() == 0 {
      println("no function changed size")
    } else {
      println("largest changes:")
    }
    for item in top {
      let dd = @audit.delta_of(item)
      let sign = if dd >= 0 { "+" } else { "" }
      println(
        "  " +
        sign +
        dd.to_string() +
        " B  " +
        item.name +
        "  (" +
        item.old_size.to_string() +
        " -> " +
        item.new_size.to_string() +
        ")",
      )
    }
    return
  }

  // default: attribution
  println("file    \{path}")
  println("size    \{human(total)} (\{total} bytes)")
  println("sections \{m.sections.length()}")
  for s in m.sections {
    let pct = s.content_size * 100 / total
    println("  id=\{s.id}  \{human(s.content_size)}  \{pct}%")
  }
  let r = @analysis.analyze(m)
  let mut reach = 0
  for b in r.reachable {
    if b {
      reach = reach + 1
    }
  }
  let dead = @analysis.unreachable_functions(r)
  let bodies = @wasm.function_bodies(m)
  let mut dead_bytes = 0
  for idx in dead {
    let body_idx = idx.reinterpret_as_int() - r.imported
    if body_idx >= 0 && body_idx < bodies.length() {
      dead_bytes = dead_bytes + (bodies[body_idx].end - bodies[body_idx].start)
    }
  }
  println("")
  println(
    "functions  imported=\{r.imported} defined=\{r.defined} total=\{r.total}",
  )
  println("reachable  \{reach}")
  println("unreachable \{dead.length()}  bodies=\{human(dead_bytes)}")
  if r.indirect_sites > 0 {
    println(
      "note       \{r.indirect_sites} function(s) use call_indirect; element targets are kept as roots",
    )
  }
  if r.unsupported.length() > 0 {
    let mut ops = ""
    for u in r.unsupported {
      ops = ops + " 0x" + u.to_string()
    }
    println("warning    \{r.unsupported.length()} unsupported opcode(s):\{ops}")
  }
}