///|
suberror CliError {
  CliError(String)
}

///|
fn main {
  let args = @env.args()
  run(args) catch {
    CliError(message) => println("error: \{message}")
  }
}

///|
fn usage() -> Unit {
  println("MoonPDB 0.3.2")
  println("Usage:")
  println("  moonpdb summary ")
  println("  moonpdb modules ")
  println("  moonpdb types  [limit]")
  println("  moonpdb symbols  [limit]")
  println("  moonpdb publics  [limit]")
  println("  moonpdb find  ")
  println("  moonpdb address    ")
}

///|
fn run(args : Array[String]) -> Unit raise CliError {
  let command = match argument(args, 1) {
    Some(value) => value
    None => {
      usage()
      return
    }
  }
  match command {
    "help" | "--help" | "-h" => usage()
    "summary" => command_summary(required_argument(args, 2, "PDB path"))
    "modules" => command_modules(required_argument(args, 2, "PDB path"))
    "types" =>
      command_types(
        required_argument(args, 2, "PDB path"),
        optional_limit(args, 3),
      )
    "symbols" =>
      command_symbols(
        required_argument(args, 2, "PDB path"),
        optional_limit(args, 3),
      )
    "publics" =>
      command_publics(
        required_argument(args, 2, "PDB path"),
        optional_limit(args, 3),
      )
    "find" =>
      command_find(
        required_argument(args, 2, "PDB path"),
        required_argument(args, 3, "symbol name"),
      )
    "address" =>
      command_address(
        required_argument(args, 2, "PE path"),
        required_argument(args, 3, "PDB path"),
        required_argument(args, 4, "segment"),
        required_argument(args, 5, "section offset"),
      )
    _ => raise CliError("unknown command: \{command}")
  }
}

///|
fn argument(args : Array[String], index : Int32) -> String? {
  if index < 0 || index >= args.length() {
    None
  } else {
    Some(args[index])
  }
}

///|
fn required_argument(
  args : Array[String],
  index : Int32,
  field : String,
) -> String raise CliError {
  match argument(args, index) {
    Some(value) => value
    None => raise CliError("missing \{field}")
  }
}

///|
fn optional_limit(args : Array[String], index : Int32) -> Int32 raise CliError {
  match argument(args, index) {
    None => 100
    Some(value) =>
      match parse_uint64(value) {
        Some(number) => {
          if number == 0UL || number > 10_000UL {
            raise CliError("limit must be between 1 and 10000")
          }
          number.to_uint().reinterpret_as_int()
        }
        None => raise CliError("invalid limit: \{value}")
      }
  }
}

///|
fn load_pdb(path : String) -> @lib.MsfFile raise CliError {
  let bytes = @fs.read_file_to_bytes(path) catch {
    _ => raise CliError("cannot read PDB: \{path}")
  }
  @lib.parse_msf(bytes, @lib.MsfLimits::default()) catch {
    _ => raise CliError("cannot parse PDB: \{path}")
  }
}

///|
fn load_pe(path : String) -> @lib.PeImage raise CliError {
  let bytes = @fs.read_file_to_bytes(path) catch {
    _ => raise CliError("cannot read PE: \{path}")
  }
  @lib.parse_pe_image(bytes, @lib.PeLimits::default()) catch {
    _ => raise CliError("cannot parse PE: \{path}")
  }
}

///|
fn command_summary(path : String) -> Unit raise CliError {
  let msf = load_pdb(path)
  println("file: \{path}")
  println("streams: \{msf.stream_count()}")
  let info = Some(msf.pdb_info(@lib.PdbInfoLimits::default())) catch {
    _ => None
  }
  match info {
    Some(value) => {
      println("guid: \{value.guid_string()}")
      println("age: \{value.age()}")
      println("named streams: \{value.named_stream_count()}")
    }
    None => println("guid: unavailable")
  }
  let tpi = Some(msf.tpi(@lib.TpiLimits::default())) catch { _ => None }
  match tpi {
    Some(value) => println("tpi types: \{value.record_count()}")
    None => println("tpi types: unavailable")
  }
  let dbi = Some(msf.dbi(@lib.DbiLimits::default())) catch { _ => None }
  match dbi {
    Some(value) => println("modules: \{value.module_count()}")
    None => println("modules: unavailable")
  }
  match dbi {
    Some(value) => {
      let publics = Some(
        msf.publics_stream(value, @lib.SymbolLimits::default()),
      ) catch {
        _ => None
      }
      match publics {
        Some(table) => println("public symbols: \{table.public_symbol_count()}")
        None => println("public symbols: unavailable")
      }
    }
    None => println("public symbols: unavailable")
  }
}

///|
fn command_modules(path : String) -> Unit raise CliError {
  let msf = load_pdb(path)
  let dbi = msf.dbi(@lib.DbiLimits::default()) catch {
    _ => raise CliError("DBI stream is unavailable")
  }
  println("modules: \{dbi.module_count()}")
  for index in 0.. Unit raise CliError {
  let msf = load_pdb(path)
  let tpi = msf.tpi(@lib.TpiLimits::default()) catch {
    _ => raise CliError("TPI stream is unavailable")
  }
  let count = if limit < tpi.record_count() {
    limit
  } else {
    tpi.record_count()
  }
  println("types: \{tpi.record_count()} shown: \{count}")
  for index in 0.. Unit raise CliError {
  let msf = load_pdb(path)
  let dbi = msf.dbi(@lib.DbiLimits::default()) catch {
    _ => raise CliError("DBI stream is unavailable")
  }
  let symbols = msf.symbol_record_stream(dbi, @lib.SymbolLimits::default()) catch {
    _ => raise CliError("global symbol stream is unavailable")
  }
  let count = if limit < symbols.record_count() {
    limit
  } else {
    symbols.record_count()
  }
  println("symbols: \{symbols.record_count()} shown: \{count}")
  for index in 0.. value
      None => "-"
    }
    println("\{record.offset()}: kind=\{record.kind()} name=\{name}")
  }
}

///|
fn command_publics(path : String, limit : Int32) -> Unit raise CliError {
  let msf = load_pdb(path)
  let dbi = msf.dbi(@lib.DbiLimits::default()) catch {
    _ => raise CliError("DBI stream is unavailable")
  }
  let publics = msf.publics_stream(dbi, @lib.SymbolLimits::default()) catch {
    _ => raise CliError("Publics stream is unavailable")
  }
  let count = if limit < publics.public_symbol_count() {
    limit
  } else {
    publics.public_symbol_count()
  }
  println("public symbols: \{publics.public_symbol_count()} shown: \{count}")
  for index in 0.. Unit raise CliError {
  let msf = load_pdb(path)
  let dbi = msf.dbi(@lib.DbiLimits::default()) catch {
    _ => raise CliError("DBI stream is unavailable")
  }
  let globals = msf.globals_stream(dbi, @lib.SymbolLimits::default()) catch {
    _ => raise CliError("Globals stream is unavailable")
  }
  let matches = globals.find_records_by_name(name) catch {
    _ => raise CliError("Globals index is invalid")
  }
  println("matches: \{matches.length()}")
  for record in matches {
    let symbol_name = match record.name() {
      Some(value) => value
      None => "-"
    }
    println("\{record.offset()}: kind=\{record.kind()} name=\{symbol_name}")
  }
}

///|
fn command_address(
  pe_path : String,
  pdb_path : String,
  segment_text : String,
  offset_text : String,
) -> Unit raise CliError {
  let pdb = load_pdb(pdb_path)
  let pe = load_pe(pe_path)
  let dbi = pdb.dbi(@lib.DbiLimits::default()) catch {
    _ => raise CliError("DBI stream is unavailable")
  }
  let publics = pdb.publics_stream(dbi, @lib.SymbolLimits::default()) catch {
    _ => raise CliError("Publics stream is unavailable")
  }
  let segment = match parse_uint64(segment_text) {
    Some(value) => {
      if value > 65_535UL {
        raise CliError("segment is outside UInt16 range")
      }
      value.to_uint16()
    }
    None => raise CliError("invalid segment: \{segment_text}")
  }
  let offset = match parse_uint64(offset_text) {
    Some(value) => {
      if value > 0xFFFFFFFFUL {
        raise CliError("offset is outside UInt32 range")
      }
      UInt32(value.to_uint())
    }
    None => raise CliError("invalid offset: \{offset_text}")
  }
  let modules = collect_c13(pdb, dbi)
  let result = match
    @lib.symbolicate_address(pe, publics, modules, segment, offset) {
    Some(value) => value
    None => raise CliError("address is outside the PE section table")
  }
  println("segment: \{result.segment()}")
  println("section offset: \{result.section_offset()}")
  println("section: \{result.section_name()}")
  println("rva: \{result.rva()}")
  println("image address: \{result.image_address()}")
  match result.public_symbol() {
    Some(value) =>
      println(
        "public: \{value.name()} +\{result.section_offset() - value.offset()}",
      )
    None => println("public: unavailable")
  }
  match result.source() {
    Some(value) => {
      println("source: \{value.file_path()}:\{value.line_start()}")
      match value.columns() {
        Some(columns) => println("columns: \{columns.start()}-\{columns.end()}")
        None => println("columns: unavailable")
      }
    }
    None => println("source: unavailable")
  }
}

///|
fn collect_c13(
  msf : @lib.MsfFile,
  dbi : @lib.DbiFile,
) -> Array[@lib.C13DebugInfo] {
  let modules = []
  for index in 0.. None
    }
    match info {
      Some(value) => modules.push(value)
      None => ()
    }
  }
  modules
}

///|
fn parse_uint64(value : String) -> UInt64? {
  let bytes = @utf8.encode(value)
  let length = bytes.length()
  if length == 0 {
    return None
  }
  let mut cursor = 0
  let mut base = 10
  if length >= 2 &&
    bytes.at(0) == 0x30 &&
    (bytes.at(1) == 0x78 || bytes.at(1) == 0x58) {
    base = 16
    cursor = 2
  }
  if cursor == length {
    return None
  }
  let mut result = 0UL
  let base_u = base.reinterpret_as_uint().to_uint64()
  while cursor < length {
    let code = bytes.at(cursor).to_int()
    let digit = if code >= 0x30 && code <= 0x39 {
      code - 0x30
    } else if code >= 0x41 && code <= 0x46 {
      code - 0x41 + 10
    } else if code >= 0x61 && code <= 0x66 {
      code - 0x61 + 10
    } else {
      return None
    }
    if digit < 0 || digit >= base {
      return None
    }
    let digit_u = digit.to_int64().reinterpret_as_uint64()
    if result > (0xFFFFFFFFFFFFFFFFUL - digit_u) / base_u {
      return None
    }
    result = result * base_u + digit_u
    cursor = cursor + 1
  }
  Some(result)
}