///|
/// One imported symbol.
///
/// PE import tables can reference a symbol either by name or by ordinal. Both
/// are common: system DLLs frequently use ordinals for undocumented exports,
/// and a parser that only understands names silently drops those entries.

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

///|
/// One DLL and everything imported from it.
pub(all) struct ImportLibrary {
  name : String
  entries : Array[ImportEntry]
} derive(@debug.Debug)

///|
/// One import descriptor after its three address fields have been validated.
priv struct ImportDescriptor {
  original_thunk : Int32
  name_rva : Int32
  first_thunk : Int32
}

///|
/// Walk the import descriptors once. Import and IAT APIs share this code so
/// their symbol ordering can never drift apart.
fn Pe::import_descriptors(self : Pe) -> Array[ImportDescriptor] raise PeError {
  let out : Array[ImportDescriptor] = []
  let directory = match self.directory(1) {
    None => return out
    Some(directory) => directory
  }
  if directory.virtual_address == 0 || directory.size == 0 {
    return out
  }
  let descriptor = match self.reader_at_rva(directory.virtual_address) {
    Some(reader) => reader
    None =>
      raise PeError::Malformed(
        "import directory RVA " +
        hex(directory.virtual_address) +
        " is not backed by file bytes",
        0,
      )
  }
  let mut index = 0
  let mut finished = false
  while !finished && index < 4096 {
    let original_thunk = descriptor.u32()
    let _timestamp = descriptor.u32()
    let _forwarder = descriptor.u32()
    let name_rva = descriptor.u32()
    let first_thunk = descriptor.u32()
    if name_rva == 0 && original_thunk == 0 && first_thunk == 0 {
      finished = true
    } else {
      if name_rva == 0 {
        raise PeError::Malformed(
          "import descriptor has no library-name RVA",
          descriptor.position() - 8,
        )
      }
      if first_thunk == 0 {
        raise PeError::Malformed(
          "import descriptor has no FirstThunk (IAT) RVA",
          descriptor.position() - 4,
        )
      }
      out.push({ original_thunk, name_rva, first_thunk, })
      index = index + 1
    }
  }
  out
}

///|
/// Read the import directory.
///
/// Returns an empty array when the image has no imports. Structural problems —
/// a descriptor or thunk table that points outside the file — are reported as
/// errors rather than skipped, because a silently truncated import list is worse
/// than a loud failure.
pub fn Pe::imports(self : Pe) -> Array[ImportLibrary] raise PeError {
  let out : Array[ImportLibrary] = []
  for descriptor in self.import_descriptors() {
    let library_name = self.string_at_rva(descriptor.name_rva, 4096)
    let thunk_rva = if descriptor.original_thunk != 0 {
      descriptor.original_thunk
    } else {
      descriptor.first_thunk
    }
    let entries = self.read_thunks(thunk_rva)
    out.push({ name: library_name, entries, })
  }
  out
}

///|
/// The data directory with the given slot number, if the header declares it.
pub fn Pe::directory(self : Pe, index : Int32) -> DataDirectory? {
  for directory in self.optional.directories {
    if directory.index == index {
      return Some(directory)
    }
  }
  None
}

///|
/// Read a NUL-terminated ASCII string located at an RVA.
pub fn Pe::string_at_rva(
  self : Pe,
  rva : Int32,
  limit : Int32,
) -> String raise PeError {
  match self.reader_at_rva(rva) {
    Some(r) => r.cstring(limit)
    None =>
      raise PeError::Malformed(
        "string RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
}

///|
/// Walk one import lookup table.
fn Pe::read_thunks(self : Pe, rva : Int32) -> Array[ImportEntry] raise PeError {
  let out : Array[ImportEntry] = []
  let reader = match self.reader_at_rva(rva) {
    Some(r) => r
    None =>
      raise PeError::Malformed(
        "import lookup table RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
  let wide = self.bits() == 64
  let mut count = 0
  let mut done = false
  while !done && count < 65536 {
    if wide {
      let value = reader.u64()
      if value == 0 {
        done = true
      } else if value < 0 {
        // The high bit marks an ordinal import.
        out.push({ name: None, ordinal: Some((value & 0xFFFF).to_int()), })
      } else {
        if value > 2147483647 {
          raise PeError::Malformed(
            "import-by-name RVA exceeds the supported 2 GiB range",
            reader.position(),
          )
        }
        let name = self.import_name(value.to_int())
        out.push({ name: Some(name), ordinal: None, })
      }
    } else {
      let value = reader.u32_raw()
      if value == 0 {
        done = true
      } else if (value.to_int64() & 0x80000000) != 0 {
        out.push({
          name: None,
          ordinal: Some((value.to_int64() & 0xFFFF).to_int()),
        })
      } else {
        let name = self.import_name(value)
        out.push({ name: Some(name), ordinal: None, })
      }
    }
    count = count + 1
  }
  out
}

///|
/// Read the name of an import, which lives behind a two-byte hint in an
/// `IMAGE_IMPORT_BY_NAME` structure.
fn Pe::import_name(self : Pe, rva : Int32) -> String raise PeError {
  match self.reader_at_rva(rva) {
    Some(r) => {
      r.skip(2)
      r.cstring(4096)
    }
    None =>
      raise PeError::Malformed(
        "import name RVA " + hex(rva) + " is not backed by file bytes",
        0,
      )
  }
}