///|
/// One exported symbol.
///
/// `name` is `None` for exports that are reachable by ordinal only, which real
/// DLLs use for internal helpers. Keeping them in the list means the reported
/// count matches what the loader sees.

///|
pub(all) struct Export {
  name : String?
  ordinal : Int32
  rva : Int32
} derive(@debug.Debug)

///|
/// A parsed export directory.
pub(all) struct ExportTable {
  dll_name : String
  exports : Array[Export]
} derive(@debug.Debug)

///|
/// Read the export directory.
///
/// Returns `None` when the image exports nothing, which is the normal case for
/// executables and for many DLLs.
pub fn Pe::exports(self : Pe) -> ExportTable? raise PeError {
  let directory = match self.directory(0) {
    None => return None
    Some(directory) => directory
  }
  if directory.virtual_address == 0 || directory.size == 0 {
    return None
  }
  let r = match self.reader_at_rva(directory.virtual_address) {
    Some(r) => r
    None =>
      raise PeError::Malformed(
        "export directory RVA " +
        hex(directory.virtual_address) +
        " is not backed by file bytes",
        0,
      )
  }
  r.skip(4)
  r.skip(4)
  r.skip(4)
  let name_rva = r.u32()
  let ordinal_base = r.u32()
  let number_of_functions = r.u32()
  let number_of_names = r.u32()
  let functions_rva = r.u32()
  let names_rva = r.u32()
  let ordinals_rva = r.u32()
  let dll_name = self.string_at_rva(name_rva, 4096)
  let names = self.export_names(names_rva, number_of_names)
  let ordinals = self.export_name_ordinals(ordinals_rva, number_of_names)
  let functions = self.export_function_rvas(functions_rva, number_of_functions)
  let table : Array[Export] = []
  let mut i = 0
  while i < names.length() && i < ordinals.length() {
    let index = ordinals[i]
    if index >= 0 && index < functions.length() {
      table.push({
        name: Some(names[i]),
        ordinal: ordinal_base + index,
        rva: functions[index],
      })
    } else {
      raise PeError::Malformed(
        "export name " +
        i.to_string() +
        " points at function index " +
        index.to_string() +
        ", outside the function table",
        0,
      )
    }
    i = i + 1
  }
  // Ordinal-only exports: every function slot not covered by a name.
  let mut slot = 0
  while slot < functions.length() {
    if functions[slot] != 0 && !contains_index(ordinals, slot) {
      table.push({
        name: None,
        ordinal: ordinal_base + slot,
        rva: functions[slot],
      })
    }
    slot = slot + 1
  }
  Some({ dll_name, exports: table, })
}

///|
fn contains_index(indices : Array[Int32], needle : Int32) -> Bool {
  for index in indices {
    if index == needle {
      return true
    }
  }
  false
}

///|
/// Read the export name-pointer array as a list of strings.
fn Pe::export_names(
  self : Pe,
  rva : Int32,
  count : Int32,
) -> Array[String] raise PeError {
  let out : Array[String] = []
  if count == 0 {
    return out
  }
  let r = match self.reader_at_rva(rva) {
    Some(r) => r
    None =>
      raise PeError::Malformed(
        "export name table RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
  let mut i = 0
  while i < count {
    let name_rva = r.u32()
    out.push(self.string_at_rva(name_rva, 4096))
    i = i + 1
  }
  out
}

///|
fn Pe::export_name_ordinals(
  self : Pe,
  rva : Int32,
  count : Int32,
) -> Array[Int32] raise PeError {
  let out : Array[Int32] = []
  if count == 0 {
    return out
  }
  let r = match self.reader_at_rva(rva) {
    Some(r) => r
    None =>
      raise PeError::Malformed(
        "export ordinal table RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
  let mut i = 0
  while i < count {
    out.push(r.u16())
    i = i + 1
  }
  out
}

///|
fn Pe::export_function_rvas(
  self : Pe,
  rva : Int32,
  count : Int32,
) -> Array[Int32] raise PeError {
  let out : Array[Int32] = []
  if count == 0 {
    return out
  }
  let r = match self.reader_at_rva(rva) {
    Some(r) => r
    None =>
      raise PeError::Malformed(
        "export address table RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
  let mut i = 0
  while i < count {
    out.push(r.u32())
    i = i + 1
  }
  out
}