// Copyright 2026 The moon-diff Authors.
// Licensed under the Apache License, Version 2.0.
//
// `moon-diff` command-line front-end. It is also the project's *runnable
// example*: every subcommand demonstrates a piece of the library on text passed
// as arguments. Because the Core SDK bundles no filesystem package, multi-line
// inputs use the escape sequences `\n` (newline) and `\t` (tab), e.g.
//
//   moon run cli -- diff "line1\nline2" "line1\nLINE2"
//
// All output goes to stdout; the process exits 0 on success.

/// Converts the escape sequences `\n` (newline) and `\t` (tab) in `s` into the
/// corresponding control characters, so multi-line inputs can be supplied as a
/// single command-line argument. A backslash before any other character is
/// preserved literally.
fn unescape(s: String) -> String {
  let mut out: String = ""
  let mut i = 0
  let n = s.length()
  while i < n {
    let c = s[i]
    if c == '\\' && i + 1 < n {
      let nxt = s[i + 1]
      if nxt == 'n' {
        out = out + "\n"
        i = i + 2
      } else if nxt == 't' {
        out = out + "\t"
        i = i + 2
      } else {
        out = out + "\{c}"
        i = i + 1
      }
    } else {
      out = out + "\{c}"
      i = i + 1
    }
  }
  out
}

/// Parses a non-negative integer, returning `default` on any malformed input.
fn parse_int(s: String, default: Int) -> Int {
  let mut v = 0
  let mut any = false
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i]
    if c >= '0' && c <= '9' {
      v = v * 10 + (c.to_int() - '0'.to_int())
      any = true
    } else {
      return default
    }
  }
  if any {
    v
  } else {
    default
  }
}

/// Splits `args[start..]` into positional arguments plus the parsed flags
/// `--algo `, `--context `, `--git`, `--stat`, `--ignore-whitespace`,
/// `--ignore-case`, `--ours`, `--theirs`.
fn parse_flags(
  args: Array[String],
  start: Int
) -> (Array[String], String, Int, Bool, String, Bool, Bool, Bool) {
  let pos: Array[String] = []
  let mut algo = "linear"
  let mut context = 3
  let mut git = false
  let mut resolve = ""
  let mut stat = false
  let mut ignore_ws = false
  let mut ignore_case = false
  let mut i = start
  while i < args.length() {
    let a = args[i]
    if a == "--algo" && i + 1 < args.length() {
      algo = args[i + 1]
      i = i + 2
    } else if a == "--context" && i + 1 < args.length() {
      context = parse_int(args[i + 1], 3)
      i = i + 2
    } else if a == "--git" {
      git = true
      i = i + 1
    } else if a == "--stat" {
      stat = true
      i = i + 1
    } else if a == "--ignore-whitespace" {
      ignore_ws = true
      i = i + 1
    } else if a == "--ignore-case" {
      ignore_case = true
      i = i + 1
    } else if a == "--ours" {
      resolve = "ours"
      i = i + 1
    } else if a == "--theirs" {
      resolve = "theirs"
      i = i + 1
    } else {
      pos.push(a)
      i = i + 1
    }
  }
  (pos, algo, context, git, resolve, stat, ignore_ws, ignore_case)
}

fn print_usage() -> Unit {
  println("moon-diff - a text diff & patch toolkit")
  println("")
  println("usage: moon run cli --  [args] [flags]")
  println("")
  println("commands:")
  println("  diff OLD NEW [--algo lcs|myers|patience|histogram|linear]")
  println("              [--context N] [--git] [--stat] [--ignore-whitespace]")
  println("              [--ignore-case]   unified (or git/stat) diff of two texts")
  println("  patch OLD PATCH                         apply a unified diff to OLD")
  println("  merge BASE OURS THEIRS [--ours|--theirs]  3-way merge (diff3 style)")
  println("  json A B                                RFC 6902 JSON Patch of A -> B")
  println("  jsonapply DOC PATCH                     apply an RFC 6902 JSON Patch to DOC")
  println("  ratio A B                               similarity ratio of A and B in [0,1]")
  println("  algo OLD NEW                            edit distance of all 5 algorithms")
  println("  selftest                                internal consistency checks")
  println("")
  println("multi-line arguments use \\n (newline) and \\t (tab) escapes.")
  println("--ignore-whitespace/--ignore-case apply to `diff` and `diff --stat`.")
}

fn cmd_diff(args: Array[String]) -> Unit {
  let (pos, algo, context, git, _, stat, iws, iwc) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("diff: expected two texts (OLD NEW)")
    return
  }
  let old = unescape(pos[0])
  let new = unescape(pos[1])
  if git {
    println(@diff.git_diff_text(old, new, "a", "b", context))
  } else {
    let old_lines = @diff.split_lines(old)
    let new_lines = @diff.split_lines(new)
    let changed =
      if iws || iwc {
        @diff.diff_lines_ignore(old, new, iws, iwc)
      } else {
        @diff.diff_algorithm(
          @diff.diff_algorithm_by_name(algo),
          old_lines,
          new_lines,
        )
      }
    if stat {
      println(@diff.to_unified_stat(changed, "a.txt", 40))
    } else {
      println(@diff.to_unified(changed, "a.txt", "b.txt", context))
    }
  }
}

fn cmd_patch(args: Array[String]) -> Unit {
  let (pos, _, _, _, _, _, _, _) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("patch: expected (OLD PATCH)")
    return
  }
  let old = unescape(pos[0])
  let patch = unescape(pos[1])
  println(@diff.apply_unified(old, patch))
}

fn cmd_merge(args: Array[String]) -> Unit {
  let (pos, _, _, _, resolve, _, _, _) = parse_flags(args, 2)
  if pos.length() < 3 {
    println("merge: expected (BASE OURS THEIRS)")
    return
  }
  let base = @diff.split_lines(unescape(pos[0]))
  let ours = @diff.split_lines(unescape(pos[1]))
  let theirs = @diff.split_lines(unescape(pos[2]))
  let r = @diff.merge3(base, ours, theirs)
  if resolve == "ours" {
    let resolved = @diff.merge3_resolve_ours(r)
    println(resolved.join("\n"))
  } else if resolve == "theirs" {
    let resolved = @diff.merge3_resolve_theirs(r)
    println(resolved.join("\n"))
  } else {
    println(r.merged.join("\n"))
    if r.has_conflict {
      println("")
      println(
        "warning: " + r.conflicts.length().to_string() +
        " conflict(s); use --ours/--theirs to resolve",
      )
    }
  }
}

fn cmd_json(args: Array[String]) -> Unit {
  let (pos, _, _, _, _, _, _, _) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("json: expected two JSON texts (A B)")
    return
  }
  let a = unescape(pos[0])
  let b = unescape(pos[1])
  match @diff.json_diff_text(a, b) {
    Ok(s) => println(s)
    Err(e) => println("json error: " + e)
  }
}

fn cmd_jsonapply(args: Array[String]) -> Unit {
  let (pos, _, _, _, _, _, _, _) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("jsonapply: expected (DOC PATCH)")
    return
  }
  match @diff.apply_json_patch_text(unescape(pos[0]), unescape(pos[1])) {
    Ok(s) => println(s)
    Err(e) => println("json apply error: " + e)
  }
}

fn cmd_ratio(args: Array[String]) -> Unit {
  let (pos, _, _, _, _, _, _, _) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("ratio: expected two texts (A B)")
    return
  }
  let r = @diff.ratio(unescape(pos[0]), unescape(pos[1]))
  println(r.to_string())
}

fn cmd_algo(args: Array[String]) -> Unit {
  let (pos, _, _, _, _, _, _, _) = parse_flags(args, 2)
  if pos.length() < 2 {
    println("algo: expected two texts (OLD NEW)")
    return
  }
  let a = @diff.split_lines(unescape(pos[0]))
  let b = @diff.split_lines(unescape(pos[1]))
  let names = ["lcs", "myers", "patience", "histogram", "linear"]
  for name in names {
    let ch = @diff.diff_algorithm(@diff.diff_algorithm_by_name(name), a, b)
    let mut d = 0
    for c in ch {
      match c {
        @diff.Equal(_) => ()
        _ => d = d + 1
      }
    }
    println(name + ": distance=" + d.to_string())
  }
}

fn cmd_selftest() -> Unit {
  // 1. reconstruction: @diff.to_new / @diff.to_old round-trip for every algorithm
  let a = ["x", "y", "z"]
  let b = ["x", "Y", "z", "w"]
  let names = ["lcs", "myers", "patience", "histogram", "linear"]
  for name in names {
    let ch = @diff.diff_algorithm(@diff.diff_algorithm_by_name(name), a, b)
    if @diff.to_new(ch) != b {
      println("selftest FAIL: " + name + " to_new")
      return
    }
    if @diff.to_old(ch) != a {
      println("selftest FAIL: " + name + " to_old")
      return
    }
  }
  // 2. clean 3-way merge (non-overlapping edits)
  let m = @diff.merge3(
    @diff.split_lines("1\n2\n3"),
    @diff.split_lines("1\n2\n3\n4"),
    @diff.split_lines("0\n1\n2\n3"),
  )
  if m.has_conflict {
    println("selftest FAIL: unexpected conflict")
    return
  }
  if m.merged.join("\n") != "0\n1\n2\n3\n4" {
    println("selftest FAIL: merge result wrong")
    return
  }
  // 3. json diff produces a non-empty patch for differing docs
  match @diff.json_diff_text("{\"a\":1}", "{\"a\":2}") {
    Ok(s) => {
      if s.length() < 2 {
        println("selftest FAIL: empty json patch")
        return
      }
    }
    Err(_) => {
      println("selftest FAIL: json parse error")
      return
    }
  }
  // 4. RFC 6902 apply round-trips a known document (closes the patch loop)
  let doc1 =
    match @diff.parse_json("{\"a\":1,\"b\":[1,2]}") {
      Ok(d) => d
      Err(_) => @diff.json_null()
    }
  let doc2 =
    match @diff.parse_json("{\"a\":2,\"b\":[1,3]}") {
      Ok(d) => d
      Err(_) => @diff.json_null()
    }
  let ops = @diff.json_diff(doc1, doc2, "")
  match @diff.apply_json_patch(doc1, ops) {
    Ok(res) => {
      if !@diff.json_equal(res, doc2) {
        println("selftest FAIL: json apply round-trip mismatch")
        return
      }
    }
    Err(_) => {
      println("selftest FAIL: json apply error")
      return
    }
  }
  println("selftest: all checks passed")
}

fn main {
  let args = @env.args()
  if args.length() < 2 {
    print_usage()
    return
  }
  let cmd = args[1]
  match cmd {
    "diff" => cmd_diff(args)
    "patch" => cmd_patch(args)
    "merge" => cmd_merge(args)
    "json" => cmd_json(args)
    "jsonapply" => cmd_jsonapply(args)
    "ratio" => cmd_ratio(args)
    "algo" => cmd_algo(args)
    "selftest" => cmd_selftest()
    _ => print_usage()
  }
}