// JSON output.
//
// The CLI needs a machine-readable form so it can sit in a build script or feed
// another tool. Building `Json` values in the library rather than formatting
// strings in the CLI keeps the CLI free of schema knowledge and lets a caller
// re-serialize the report however it likes.

///|
/// The header of one architecture as a JSON object.
pub fn MachHeader::to_json(self : MachHeader) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["magic"] = Json::string(Show::to_string(self.kind))
  fields["endian"] = Json::string(Show::to_string(self.kind.endian()))
  fields["pointer_size"] = Json::number(self.kind.pointer_size().to_double())
  fields["cpu_type"] = Json::number(self.cpu_type.to_double())
  fields["cpu_subtype"] = Json::number(self.cpu_subtype_base().to_double())
  fields["cpu_capabilities"] = Json::number(self.cpu_capabilities().to_double())
  fields["file_type"] = Json::number(self.file_type.to_int64().to_double())
  fields["ncmds"] = Json::number(self.ncmds.to_int64().to_double())
  fields["sizeofcmds"] = Json::number(self.sizeofcmds.to_int64().to_double())
  fields["flags"] = Json::number(self.flags.to_int64().to_double())
  Json::object(fields)
}

///|
/// One segment and its sections as a JSON object.
pub fn Segment::to_json(self : Segment) -> Json {
  let sections : Array[Json] = []
  for s in self.sections {
    let f : Map[String, Json] = Map([])
    f["name"] = Json::string(s.name)
    f["addr"] = Json::string("0x\{hex(s.addr)}")
    f["size"] = Json::number(s.size.to_double())
    f["file_offset"] = Json::number(s.file_offset.to_int64().to_double())
    f["align"] = Json::number(s.align.to_int64().to_double())
    f["flags"] = Json::string("0x\{hex(s.flags.to_int64())}")
    f["zero_fill"] = Json::boolean(s.is_zero_fill())
    f["debug"] = Json::boolean(s.is_debug())
    sections.push(Json::object(f))
  }
  let fields : Map[String, Json] = Map([])
  fields["name"] = Json::string(self.name)
  fields["vmaddr"] = Json::string("0x\{hex(self.vmaddr)}")
  fields["vmsize"] = Json::number(self.vmsize.to_double())
  fields["file_offset"] = Json::number(self.file_offset.to_double())
  fields["file_size"] = Json::number(self.file_size.to_double())
  fields["init_prot"] = Json::number(self.init_prot.to_int64().to_double())
  fields["flags"] = Json::number(self.flags.to_int64().to_double())
  fields["sections"] = Json::array(sections)
  Json::object(fields)
}

///|
/// One symbol as a JSON object.
pub fn Symbol::to_json(self : Symbol) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["name"] = Json::string(self.name)
  fields["address"] = Json::string("0x\{hex(self.n_value)}")
  fields["type"] = Json::number(self.n_type.to_int64().to_double())
  fields["sect"] = Json::number(self.n_sect.to_int64().to_double())
  fields["desc"] = Json::number(self.n_desc.to_int64().to_double())
  fields["external"] = Json::boolean(self.is_external())
  fields["debug"] = Json::boolean(self.is_debug())
  Json::object(fields)
}

///|
/// One export as a JSON object.
pub fn Export::to_json(self : Export) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["name"] = Json::string(self.name)
  fields["offset"] = Json::string(
    "0x\{hex(self.address.reinterpret_as_int64())}",
  )
  fields["flags"] = Json::number(self.flags.reinterpret_as_int64().to_double())
  fields["weak"] = Json::boolean(self.is_weak())
  fields["reexport"] = Json::boolean(self.is_reexport())
  if self.original_name != "" {
    fields["original_name"] = Json::string(self.original_name)
  }
  Json::object(fields)
}

///|
/// A resolved source position as a JSON object.
pub fn SourceLocation::to_json(self : SourceLocation) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["file"] = Json::string(self.file)
  fields["line"] = Json::number(self.line.to_int64().to_double())
  fields["column"] = Json::number(self.column.to_int64().to_double())
  Json::object(fields)
}

///|
/// Builds a report for one architecture.
///
/// Sections are omitted when not requested so that a caller asking only for the
/// header does not pay for a full table walk, and so that a failure in one
/// section does not take down the whole report.
pub fn slice_report(
  slice : MachSlice,
  segments_~ : Bool = false,
  symbols_~ : Bool = false,
  exports_~ : Bool = false,
  fixups_~ : Bool = false,
) -> Json raise MachscopeError {
  let fields : Map[String, Json] = Map([])
  fields["header"] = slice.header.to_json()
  let commands : Array[Json] = []
  for cmd in slice.commands {
    let f : Map[String, Json] = Map([])
    f["cmd"] = Json::string("0x\{hex(cmd.cmd.to_int64())}")
    f["size"] = Json::number(cmd.size.to_int64().to_double())
    f["offset"] = Json::number(cmd.offset.to_double())
    commands.push(Json::object(f))
  }
  fields["commands"] = Json::array(commands)
  if segments_ {
    let segs : Array[Json] = []
    for seg in segments(slice) {
      segs.push(seg.to_json())
    }
    fields["segments"] = Json::array(segs)
  }
  if symbols_ {
    let syms : Array[Json] = []
    for s in symbols(slice) {
      syms.push(s.to_json())
    }
    fields["symbols"] = Json::array(syms)
  }
  if exports_ {
    let exps : Array[Json] = []
    for e in exports(slice) {
      exps.push(e.to_json())
    }
    fields["exports"] = Json::array(exps)
  }
  if fixups_ {
    // A segment whose pointer format this version does not decode makes the
    // walk raise; reporting the failure as a string keeps the rest of the
    // report usable.
    let list = Ok(fixups(slice)) catch { e => Err(e) }
    match list {
      Ok(items) => {
        let arr : Array[Json] = []
        for f in items {
          arr.push(f.to_json())
        }
        fields["fixups"] = Json::array(arr)
      }
      Err(e) => fields["fixups_error"] = Json::string(e.to_string())
    }
  }
  Json::object(fields)
}

///|
/// One resolved fixup as a JSON object.
pub fn Fixup::to_json(self : Fixup) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["address"] = Json::string("0x\{hex(self.address)}")
  fields["kind"] = Json::string(if self.is_bind() { "bind" } else { "rebase" })
  match self.symbol {
    Some(s) => fields["symbol"] = Json::string(s)
    None => ()
  }
  if !self.is_bind() {
    fields["target"] = Json::string("0x\{hex(self.target)}")
  }
  fields["library_ordinal"] = Json::number(
    self.library_ordinal.to_int64().to_double(),
  )
  if self.has_discriminator {
    fields["auth"] = Json::boolean(true)
    fields["key"] = Json::number(self.key.to_int64().to_double())
    fields["diversity"] = Json::number(self.diversity.to_int64().to_double())
  }
  Json::object(fields)
}

///|
/// Builds an address-lookup report for one slice.
///
/// A lookup is reported as a list because a slice may carry more than one line
/// program, and an address can legitimately resolve in more than one of them
/// when a compilation unit was linked twice. An address that resolves nowhere
/// reports the address with an empty list rather than omitting the key, so a
/// caller can tell "no answer" from "not asked".
pub fn slice_lookup_report(
  slice : MachSlice,
  addr : Int64,
) -> Json raise MachscopeError {
  let fields : Map[String, Json] = Map([])
  fields["address"] = Json::string("0x\{hex(addr)}")
  let locations : Array[Json] = []
  for table in line_tables(slice, comp_dir="") {
    match lookup_address(table, addr) {
      Some(loc) => locations.push(loc.to_json())
      None => ()
    }
  }
  fields["locations"] = Json::array(locations)
  Json::object(fields)
}

///|
/// Builds an address-lookup report for a whole file.
pub fn file_lookup_report(
  file : MachFile,
  addr : Int64,
) -> Json raise MachscopeError {
  let archs : Array[Json] = []
  match file {
    Thin(slice) => archs.push(slice_lookup_report(slice, addr))
    Fat(slices) =>
      for s in slices {
        let fields = match slice_lookup_report(s.image, addr) {
          Object(m) => m
          _ => Map([])
        }
        fields["fat_cpu_type"] = Json::number(s.cpu_type.to_double())
        fields["fat_offset"] = Json::number(s.offset.to_double())
        archs.push(Json::object(fields))
      }
  }
  let fields : Map[String, Json] = Map([])
  fields["lookup"] = Json::array(archs)
  Json::object(fields)
}

///|
/// Builds a report for a whole file.
///
/// A thin image reports `architectures` with one entry; a fat one reports each
/// slice. The shape is the same either way so that a consumer does not have to
/// branch on which kind of file it was given.
pub fn file_report(
  file : MachFile,
  segments_~ : Bool = false,
  symbols_~ : Bool = false,
  exports_~ : Bool = false,
  fixups_~ : Bool = false,
) -> Json raise MachscopeError {
  let archs : Array[Json] = []
  match file {
    Thin(slice) =>
      archs.push(
        slice_report(slice, segments_~, symbols_~, exports_~, fixups_~),
      )
    Fat(slices) =>
      for s in slices {
        // The slice's own placement in the file is merged into the same object
        // as its image, so that a consumer reads `architectures[i].header` and
        // `architectures[i].symbols` whichever kind of file it was given.
        let inner = slice_report(s.image, segments_~, symbols_~, exports_~)
        let fields : Map[String, Json] = match inner {
          Object(m) => m
          _ => Map([])
        }
        fields["fat_cpu_type"] = Json::number(s.cpu_type.to_double())
        fields["fat_cpu_subtype"] = Json::number(
          s.cpu_subtype_base().to_double(),
        )
        fields["fat_offset"] = Json::number(s.offset.to_double())
        fields["fat_size"] = Json::number(s.size.to_double())
        fields["fat_align"] = Json::number(s.align.to_int64().to_double())
        archs.push(Json::object(fields))
      }
  }
  let fields : Map[String, Json] = Map([])
  fields["architectures"] = Json::array(archs)
  Json::object(fields)
}