///|
fn main {
  let args = @env.args()
  let command = if args.length() > 1 { args[1] } else { "help" }
  match command {
    "inspect" => run_inspect(args)
    "lookup" => run_lookup(args)
    "reverse" => run_reverse(args)
    "reverse-all" => run_reverse_all(args)
    "wasm-url" => run_wasm_url(args)
    "wasm-set-url" => run_wasm_set_url(args)
    "wasm-remove-url" => run_wasm_remove_url(args)
    "demo" => {
      let output = run_demo() catch {
        error => {
          fail(error.to_string())
          ""
        }
      }
      println(output)
    }
    "version" => println("moonbit-sourcemap 0.1.0")
    "help" | "--help" | "-h" => usage() |> println
    _ => fail("unknown command '\{command}'\n\n\{usage()}")
  }
}

///|
fn usage() -> String {
  (
    #|moonbit-sourcemap - inspect Source Map v3 files
    #|
    #|Usage:
    #|  moon run cmd/main -- inspect 
    #|  moon run cmd/main -- lookup   
    #|  moon run cmd/main -- reverse    
    #|  moon run cmd/main -- reverse-all    
    #|  moon run cmd/main -- wasm-url 
    #|  moon run cmd/main -- wasm-set-url   
    #|  moon run cmd/main -- wasm-remove-url  
    #|  moon run cmd/main -- demo
    #|  moon run cmd/main -- version
  )
}

///|
fn fail(message : String) -> Unit {
  println("error: \{message}")
  abort("moonbit-sourcemap: \{message}")
}

///|
fn read_or_fail(path : String) -> String {
  match read_file(path) {
    Some(content) => content
    None => {
      fail("cannot read '\{path}'")
      ""
    }
  }
}

///|
fn read_binary_or_fail(path : String) -> Bytes {
  match read_binary(path) {
    Some(content) => content
    None => {
      fail("cannot read '\{path}'")
      b""
    }
  }
}

///|
fn write_binary_or_fail(path : String, content : Bytes) -> Unit {
  if !write_binary(path, content) {
    fail("cannot write '\{path}'")
  }
}

///|
fn parse_int_or_fail(value : String, label : String) -> Int {
  @string.parse_int(value) catch {
    _ => {
      fail("\{label} must be an integer, got '\{value}'")
      0
    }
  }
}

///|
fn run_inspect(args : Array[String]) -> Unit {
  if args.length() != 3 {
    fail("usage: moon run cmd/main -- inspect ")
  }
  let output = inspect_document(read_or_fail(args[2])) catch {
    error => {
      fail(error.to_string())
      ""
    }
  }
  println(output)
}

///|
fn run_lookup(args : Array[String]) -> Unit {
  if args.length() != 5 {
    fail("usage: moon run cmd/main -- lookup   ")
  }
  let line = parse_int_or_fail(args[3], "line")
  let column = parse_int_or_fail(args[4], "column")
  let output = lookup_document(read_or_fail(args[2]), line, column) catch {
    error => {
      fail(error.to_string())
      ""
    }
  }
  println(output)
}

///|
fn run_reverse(args : Array[String]) -> Unit {
  if args.length() != 6 {
    fail(
      "usage: moon run cmd/main -- reverse    ",
    )
  }
  let line = parse_int_or_fail(args[4], "line")
  let column = parse_int_or_fail(args[5], "column")
  let output = reverse_document(read_or_fail(args[2]), args[3], line, column) catch {
    error => {
      fail(error.to_string())
      ""
    }
  }
  println(output)
}

///|
fn run_reverse_all(args : Array[String]) -> Unit {
  if args.length() != 6 {
    fail(
      "usage: moon run cmd/main -- reverse-all    ",
    )
  }
  let line = parse_int_or_fail(args[4], "line")
  let column = parse_int_or_fail(args[5], "column")
  let output = reverse_all_document(
    read_or_fail(args[2]),
    args[3],
    line,
    column,
  ) catch {
    error => {
      fail(error.to_string())
      ""
    }
  }
  println(output)
}

///|
fn run_wasm_url(args : Array[String]) -> Unit {
  if args.length() != 3 {
    fail("usage: moon run cmd/main -- wasm-url ")
  }
  let output = wasm_url_document(read_binary_or_fail(args[2])) catch {
    error => {
      fail(error.to_string())
      ""
    }
  }
  println(output)
}

///|
fn run_wasm_set_url(args : Array[String]) -> Unit {
  if args.length() != 5 {
    fail(
      "usage: moon run cmd/main -- wasm-set-url   ",
    )
  }
  let output = @sourcemap.wasm_set_source_mapping_url(
    read_binary_or_fail(args[2]),
    args[3],
  ) catch {
    error => {
      fail(error.to_string())
      b""
    }
  }
  write_binary_or_fail(args[4], output)
  println(args[3])
}

///|
fn run_wasm_remove_url(args : Array[String]) -> Unit {
  if args.length() != 4 {
    fail(
      "usage: moon run cmd/main -- wasm-remove-url  ",
    )
  }
  let output = @sourcemap.wasm_remove_source_mapping_url(
    read_binary_or_fail(args[2]),
  ) catch {
    error => {
      fail(error.to_string())
      b""
    }
  }
  write_binary_or_fail(args[3], output)
  println("removed")
}

///|
pub fn wasm_url_document(
  input : Bytes,
) -> String raise @sourcemap.SourceMapError {
  match @sourcemap.wasm_source_mapping_url(input) {
    Some(url) => url
    None => "no sourceMappingURL"
  }
}

///|
pub fn inspect_document(
  input : String,
) -> String raise @sourcemap.SourceMapError {
  let map = @sourcemap.parse_json(input)
  let file = match map.file {
    Some(value) => value
    None => "(none)"
  }
  "file: \{file}\nsources: \{map.sources.length()}\nnames: \{map.names.length()}\nmappings: \{map.mappings.length()}"
}

///|
pub fn lookup_document(
  input : String,
  line : Int,
  column : Int,
) -> String raise @sourcemap.SourceMapError {
  if line < 0 || column < 0 {
    return "no mapping"
  }
  let map = @sourcemap.parse_json(input)
  match map.original_position_for(line, column) {
    None => "no mapping"
    Some(position) =>
      match
        (position.source, position.original_line, position.original_column) {
        (Some(source), Some(original_line), Some(original_column)) => {
          let name = match position.name {
            Some(value) => " name=\{value}"
            None => ""
          }
          "\{source}:\{original_line}:\{original_column}\{name}"
        }
        _ => "unmapped"
      }
  }
}

///|
pub fn reverse_document(
  input : String,
  source : String,
  line : Int,
  column : Int,
) -> String raise @sourcemap.SourceMapError {
  if line < 0 || column < 0 {
    return "no mapping"
  }
  let map = @sourcemap.parse_json(input)
  match map.generated_position_for(source, line, column) {
    None => "no mapping"
    Some(position) => {
      let file = match map.file {
        Some(value) => value
        None => "(generated)"
      }
      "\{file}:\{position.generated_line}:\{position.generated_column}"
    }
  }
}

///|
pub fn reverse_all_document(
  input : String,
  source : String,
  line : Int,
  column : Int,
) -> String raise @sourcemap.SourceMapError {
  let map = @sourcemap.parse_json(input)
  let positions = map.generated_positions_for(source, line, column)
  if positions.length() == 0 {
    return "no mapping"
  }
  let file = match map.file {
    Some(value) => value
    None => "(generated)"
  }
  let mut output = ""
  for i in 0.. 0 {
      output = output + "\n"
    }
    let position = positions[i]
    output = output +
      "\{file}:\{position.generated_line}:\{position.generated_column}"
  }
  output
}

///|
fn run_demo() -> String raise @sourcemap.SourceMapError {
  let builder = @sourcemap.SourceMapBuilder::new()
  builder.add_mapping(0, 0, "src/main.mbt", 4, 2, name=Some("main"))
  builder.add_mapping(0, 12, "src/main.mbt", 5, 4)
  let map = builder.build(file=Some("app.wasm"))
  "mappings: \{map.encode_mappings()}\n" + lookup_document(map.to_json(), 0, 14)
}