///|
/// Common high-value tables found in an ELF object.
///
/// `find_common_data` fills this structure with any matching sections found by
/// one pass over the section header table. All fields are optional because
/// stripped files, relocatable objects, shared objects, and executables carry
/// different combinations of these tables.
pub(all) struct CommonElfData {
  /// Static `.symtab` symbol table, if present.
  symtab : SymbolTable?
  /// String table linked from `.symtab`, if present.
  symtab_strs : StringTable?
  /// Dynamic `.dynsym` symbol table, if present.
  dynsyms : SymbolTable?
  /// String table linked from `.dynsym`, if present.
  dynsyms_strs : StringTable?
  /// `.dynamic` section or `PT_DYNAMIC` segment, if present.
  dynamic : DynamicTable?
  /// SysV `.hash` table for dynamic symbol lookup, if present.
  sysv_hash : SysVHashTable?
  /// GNU `.gnu.hash` table for dynamic symbol lookup, if present.
  gnu_hash : GnuHashTable?
}

///|
/// Bytes-oriented ELF parser.
///
/// `ElfBytes` owns no copy of the file data. It stores a `BytesView` supplied by
/// the caller, eagerly parses the file header, and lazily wraps the section and
/// program header table byte ranges. Higher-level helpers then expose table
/// views and raw section/segment bytes from the same source data.
pub(all) struct ElfBytes {
  /// Parsed ELF file header.
  ehdr : FileHeader
  /// Original file bytes.
  data : BytesView
  /// Lazy section-header table, absent when `e_shoff == 0`.
  shdrs : SectionHeaderTable?
  /// Lazy program-header table, absent when `e_phoff == 0`.
  phdrs : SegmentTable?
}

///|
fn find_shdrs(
  ehdr : FileHeader,
  data : BytesView,
) -> SectionHeaderTable? raise ParseError {
  // ELF files are allowed to omit the section header table.
  if ehdr.e_shoff == 0UL {
    return None
  }
  let shoff = u64_to_int_checked(ehdr.e_shoff)
  let mut shnum = ehdr.e_shnum.to_int()
  if shnum == 0 {
    // Extended section counts are stored in section header 0's `sh_size`.
    let (shdr0, _) = SectionHeader::parse_at(
      ehdr.endianness,
      ehdr.class,
      shoff,
      data,
    )
    shnum = u64_to_int_checked(shdr0.sh_size)
  }
  // Validate the on-disk entry size before wrapping the table. This catches
  // corrupt files early and keeps lazy entry parsing aligned.
  let entsize = validate_entsize(
    ehdr.e_shentsize.to_int(),
    SectionHeader::size_for(ehdr.class),
  )
  let size = checked_mul(entsize, shnum)
  let end = checked_add(shoff, size)
  Some(
    SectionHeaderTable(
      ehdr.endianness,
      ehdr.class,
      slice_checked(data, shoff, end),
    ),
  )
}

///|
fn find_phdrs(
  ehdr : FileHeader,
  data : BytesView,
) -> SegmentTable? raise ParseError {
  // ELF files are allowed to omit the program header table.
  if ehdr.e_phoff == 0UL {
    return None
  }
  let mut phnum = ehdr.e_phnum.to_int()
  if ehdr.e_phnum == PN_XNUM {
    // Extended program-header counts are stored in section header 0's `sh_info`.
    let shoff = u64_to_int_checked(ehdr.e_shoff)
    let (shdr0, _) = SectionHeader::parse_at(
      ehdr.endianness,
      ehdr.class,
      shoff,
      data,
    )
    phnum = uint_to_int_checked(shdr0.sh_info)
  }
  // Validate the on-disk entry size before wrapping the table.
  let entsize = validate_entsize(
    ehdr.e_phentsize.to_int(),
    ProgramHeader::size_for(ehdr.class),
  )
  let phoff = u64_to_int_checked(ehdr.e_phoff)
  let size = checked_mul(entsize, phnum)
  let end = checked_add(phoff, size)
  Some(
    SegmentTable(ehdr.endianness, ehdr.class, slice_checked(data, phoff, end)),
  )
}

///|
/// Parse the minimum metadata needed to inspect an ELF file.
///
/// This validates the ELF identity, parses the `FileHeader`, and locates the
/// section and program header tables without parsing every table entry.
pub fn ElfBytes::minimal_parse(data : BytesView) -> ElfBytes raise ParseError {
  let ident_buf = slice_checked(data, 0, EI_NIDENT)
  let ident = parse_ident(ident_buf)
  let (_, class, _, _) = ident
  let tail_start = EI_NIDENT
  let tail_size = match class {
    ELF32 => ELF32_EHDR_TAILSIZE
    ELF64 => ELF64_EHDR_TAILSIZE
  }
  let tail_end = checked_add(tail_start, tail_size)
  let tail_buf = slice_checked(data, tail_start, tail_end)
  let ehdr = FileHeader::parse_tail(ident, tail_buf)
  let shdrs = find_shdrs(ehdr, data)
  let phdrs = find_phdrs(ehdr, data)
  { ehdr, data, shdrs, phdrs }
}

///|
/// Return the lazy program-header table, if the file has one.
pub fn ElfBytes::segments(self : ElfBytes) -> SegmentTable? {
  self.phdrs
}

///|
/// Return the lazy section-header table, if the file has one.
pub fn ElfBytes::section_headers(self : ElfBytes) -> SectionHeaderTable? {
  self.shdrs
}

///|
/// Return section headers and their section-name string table.
///
/// If the file has section headers but no section-name string table, returns
/// `(Some(shdrs), None)`. Extended `e_shstrndx` values are resolved through
/// section header 0.
pub fn ElfBytes::section_headers_with_strtab(
  self : ElfBytes,
) -> (SectionHeaderTable?, StringTable?) raise ParseError {
  let shdrs = match self.section_headers() {
    Some(shdrs) => shdrs
    None => return (None, None)
  }
  if self.ehdr.e_shstrndx == SHN_UNDEF {
    return (Some(shdrs), None)
  }
  let mut shstrndx = self.ehdr.e_shstrndx.to_int()
  if self.ehdr.e_shstrndx == SHN_XINDEX {
    // Extended section-name string table index is in section header 0's `sh_link`.
    shstrndx = uint_to_int_checked(shdrs.get(0).sh_link)
  }
  let strtab = shdrs.get(shstrndx)
  let (start, end) = strtab.get_data_range()
  (Some(shdrs), Some(StringTable::new(slice_checked(self.data, start, end))))
}

///|
/// Find a section header by its name.
///
/// Returns `None` when the object has no section headers, no section-name
/// string table, or no matching section. Malformed string table entries are
/// skipped while searching.
pub fn ElfBytes::section_header_by_name(
  self : ElfBytes,
  name : String,
) -> SectionHeader? raise ParseError {
  let (shdrs_opt, strtab_opt) = self.section_headers_with_strtab()
  let (shdrs, strtab) = match (shdrs_opt, strtab_opt) {
    (Some(shdrs), Some(strtab)) => (shdrs, strtab)
    _ => return None
  }
  for shdr in shdrs.iter() {
    try strtab.get(uint_to_int_checked(shdr.sh_name)) catch {
      _ => ()
    } noraise {
      section_name => if section_name == name { return Some(shdr) }
    }
  }
  None
}

///|
/// Return raw data for a section.
///
/// `SHT_NOBITS` sections occupy no bytes in the file and return an empty view.
/// For compressed sections, the returned byte view starts after the parsed
/// `CompressionHeader`.
pub fn ElfBytes::section_data(
  self : ElfBytes,
  shdr : SectionHeader,
) -> (BytesView, CompressionHeader?) raise ParseError {
  if shdr.sh_type == SHT_NOBITS {
    return (b""[:], None)
  }
  let (start, end) = shdr.get_data_range()
  let buf = slice_checked(self.data, start, end)
  if (shdr.sh_flags & SHF_COMPRESSED.to_uint64()) == 0UL {
    (buf, None)
  } else {
    let (chdr, offset) = CompressionHeader::parse_at(
      self.ehdr.endianness,
      self.ehdr.class,
      0,
      buf,
    )
    (tail_checked(buf, offset), Some(chdr))
  }
}

///|
/// Interpret section data as a string table.
///
/// Raises `UnexpectedSectionType` unless `shdr.sh_type == SHT_STRTAB`.
pub fn ElfBytes::section_data_as_strtab(
  self : ElfBytes,
  shdr : SectionHeader,
) -> StringTable raise ParseError {
  if shdr.sh_type != SHT_STRTAB {
    raise UnexpectedSectionType(shdr.sh_type, SHT_STRTAB)
  }
  let (buf, _) = self.section_data(shdr)
  StringTable::new(buf)
}

///|
/// Interpret section data as no-addend relocation entries.
///
/// Raises `UnexpectedSectionType` unless `shdr.sh_type == SHT_REL`.
pub fn ElfBytes::section_data_as_rels(
  self : ElfBytes,
  shdr : SectionHeader,
) -> RelIterator raise ParseError {
  if shdr.sh_type != SHT_REL {
    raise UnexpectedSectionType(shdr.sh_type, SHT_REL)
  }
  if shdr.sh_entsize != 0UL {
    ignore(
      validate_entsize(
        u64_to_int_checked(shdr.sh_entsize),
        Rel::size_for(self.ehdr.class),
      ),
    )
  }
  let (buf, _) = self.section_data(shdr)
  RelIterator(self.ehdr.endianness, self.ehdr.class, buf)
}

///|
/// Interpret section data as addend relocation entries.
///
/// Raises `UnexpectedSectionType` unless `shdr.sh_type == SHT_RELA`.
pub fn ElfBytes::section_data_as_relas(
  self : ElfBytes,
  shdr : SectionHeader,
) -> RelaIterator raise ParseError {
  if shdr.sh_type != SHT_RELA {
    raise UnexpectedSectionType(shdr.sh_type, SHT_RELA)
  }
  if shdr.sh_entsize != 0UL {
    ignore(
      validate_entsize(
        u64_to_int_checked(shdr.sh_entsize),
        Rela::size_for(self.ehdr.class),
      ),
    )
  }
  let (buf, _) = self.section_data(shdr)
  RelaIterator(self.ehdr.endianness, self.ehdr.class, buf)
}

///|
/// Interpret section data as ELF notes.
///
/// Raises `UnexpectedSectionType` unless `shdr.sh_type == SHT_NOTE`.
pub fn ElfBytes::section_data_as_notes(
  self : ElfBytes,
  shdr : SectionHeader,
) -> NoteIterator raise ParseError {
  if shdr.sh_type != SHT_NOTE {
    raise UnexpectedSectionType(shdr.sh_type, SHT_NOTE)
  }
  let (buf, _) = self.section_data(shdr)
  NoteIterator::new(
    self.ehdr.endianness,
    self.ehdr.class,
    u64_to_int_checked(shdr.sh_addralign),
    buf,
  )
}

///|
// Internal dynamic-section adapter used by `dynamic` and `find_common_data`.
fn ElfBytes::section_data_as_dynamic(
  self : ElfBytes,
  shdr : SectionHeader,
) -> DynamicTable raise ParseError {
  if shdr.sh_type != SHT_DYNAMIC {
    raise UnexpectedSectionType(shdr.sh_type, SHT_DYNAMIC)
  }
  ignore(
    validate_entsize(
      u64_to_int_checked(shdr.sh_entsize),
      Dyn::size_for(self.ehdr.class),
    ),
  )
  let (buf, _) = self.section_data(shdr)
  DynamicTable(self.ehdr.endianness, self.ehdr.class, buf)
}

///|
/// Return the bytes occupied by a program segment in the file.
pub fn ElfBytes::segment_data(
  self : ElfBytes,
  phdr : ProgramHeader,
) -> BytesView raise ParseError {
  let (start, end) = phdr.get_file_data_range()
  slice_checked(self.data, start, end)
}

///|
/// Interpret segment data as ELF notes.
///
/// Raises `UnexpectedSegmentType` unless `phdr.p_type == PT_NOTE`.
pub fn ElfBytes::segment_data_as_notes(
  self : ElfBytes,
  phdr : ProgramHeader,
) -> NoteIterator raise ParseError {
  if phdr.p_type != PT_NOTE {
    raise UnexpectedSegmentType(phdr.p_type, PT_NOTE)
  }
  NoteIterator::new(
    self.ehdr.endianness,
    self.ehdr.class,
    u64_to_int_checked(phdr.p_align),
    self.segment_data(phdr),
  )
}

///|
/// Return the `.dynamic` table, if present.
///
/// Section headers are preferred when available. If the file has no section
/// headers, the parser falls back to the `PT_DYNAMIC` program segment.
pub fn ElfBytes::dynamic(self : ElfBytes) -> DynamicTable? raise ParseError {
  match self.section_headers() {
    Some(shdrs) =>
      for shdr in shdrs.iter() {
        if shdr.sh_type == SHT_DYNAMIC {
          return Some(self.section_data_as_dynamic(shdr))
        }
      }
    None =>
      match self.segments() {
        Some(phdrs) =>
          for phdr in phdrs.iter() {
            if phdr.p_type == PT_DYNAMIC {
              let (start, end) = phdr.get_file_data_range()
              return Some(
                DynamicTable(
                  self.ehdr.endianness,
                  self.ehdr.class,
                  slice_checked(self.data, start, end),
                ),
              )
            }
          }
        None => ()
      }
  }
  None
}

///|
// Shared helper for `.symtab` and `.dynsym`; both sections link to their
// corresponding string table through `sh_link`.
fn ElfBytes::section_data_as_symbol_table(
  self : ElfBytes,
  shdr : SectionHeader,
  strtab_shdr : SectionHeader,
) -> (SymbolTable, StringTable) raise ParseError {
  ignore(
    validate_entsize(
      u64_to_int_checked(shdr.sh_entsize),
      Symbol::size_for(self.ehdr.class),
    ),
  )
  // Load both the symbol table and the linked string table from the original
  // bytes so callers can keep the two lazy views together.
  let (symtab_start, symtab_end) = shdr.get_data_range()
  let symtab_buf = slice_checked(self.data, symtab_start, symtab_end)
  let (strtab_start, strtab_end) = strtab_shdr.get_data_range()
  let strtab_buf = slice_checked(self.data, strtab_start, strtab_end)
  (
    SymbolTable(self.ehdr.endianness, self.ehdr.class, symtab_buf),
    StringTable::new(strtab_buf),
  )
}

///|
/// Return the file's static `.symtab` and its linked string table, if present.
pub fn ElfBytes::symbol_table(
  self : ElfBytes,
) -> (SymbolTable, StringTable)? raise ParseError {
  let shdrs = match self.section_headers() {
    Some(shdrs) => shdrs
    None => return None
  }
  for shdr in shdrs.iter() {
    if shdr.sh_type == SHT_SYMTAB {
      return Some(
        self.section_data_as_symbol_table(
          shdr,
          shdrs.get(uint_to_int_checked(shdr.sh_link)),
        ),
      )
    }
  }
  None
}

///|
/// Return the file's dynamic `.dynsym` and its linked string table, if present.
pub fn ElfBytes::dynamic_symbol_table(
  self : ElfBytes,
) -> (SymbolTable, StringTable)? raise ParseError {
  let shdrs = match self.section_headers() {
    Some(shdrs) => shdrs
    None => return None
  }
  for shdr in shdrs.iter() {
    if shdr.sh_type == SHT_DYNSYM {
      return Some(
        self.section_data_as_symbol_table(
          shdr,
          shdrs.get(uint_to_int_checked(shdr.sh_link)),
        ),
      )
    }
  }
  None
}

///|
/// Locate common ELF data tables in one pass over the section headers.
///
/// This is useful when a caller expects to inspect several common structures:
/// symbol tables, string tables, dynamic entries, and hash tables. If no
/// `SHT_DYNAMIC` section is found, the method also checks for `PT_DYNAMIC`.
pub fn ElfBytes::find_common_data(
  self : ElfBytes,
) -> CommonElfData raise ParseError {
  let mut symtab : SymbolTable? = None
  let mut symtab_strs : StringTable? = None
  let mut dynsyms : SymbolTable? = None
  let mut dynsyms_strs : StringTable? = None
  let mut dynamic : DynamicTable? = None
  let mut sysv_hash_table : SysVHashTable? = None
  let mut gnu_hash_table : GnuHashTable? = None
  match self.shdrs {
    Some(shdrs) =>
      // Collect known table-like sections while scanning section headers once.
      for shdr in shdrs.iter() {
        match shdr.sh_type {
          SHT_SYMTAB => {
            let (table, strs) = self.section_data_as_symbol_table(
              shdr,
              shdrs.get(uint_to_int_checked(shdr.sh_link)),
            )
            symtab = Some(table)
            symtab_strs = Some(strs)
          }
          SHT_DYNSYM => {
            let (table, strs) = self.section_data_as_symbol_table(
              shdr,
              shdrs.get(uint_to_int_checked(shdr.sh_link)),
            )
            dynsyms = Some(table)
            dynsyms_strs = Some(strs)
          }
          SHT_DYNAMIC => dynamic = Some(self.section_data_as_dynamic(shdr))
          SHT_HASH => {
            let (start, end) = shdr.get_data_range()
            sysv_hash_table = Some(
              SysVHashTable::new(
                self.ehdr.endianness,
                self.ehdr.class,
                slice_checked(self.data, start, end),
              ),
            )
          }
          SHT_GNU_HASH => {
            let (start, end) = shdr.get_data_range()
            gnu_hash_table = Some(
              GnuHashTable::new(
                self.ehdr.endianness,
                self.ehdr.class,
                slice_checked(self.data, start, end),
              ),
            )
          }
          _ => ()
        }
      }
    None => ()
  }
  if dynamic is None {
    match self.phdrs {
      Some(phdrs) =>
        // Stripped or unusual objects may expose dynamic data only through a
        // program segment.
        for phdr in phdrs.iter() {
          if phdr.p_type == PT_DYNAMIC {
            let (start, end) = phdr.get_file_data_range()
            dynamic = Some(
              DynamicTable(
                self.ehdr.endianness,
                self.ehdr.class,
                slice_checked(self.data, start, end),
              ),
            )
            break
          }
        }
      None => ()
    }
  }
  {
    symtab,
    symtab_strs,
    dynsyms,
    dynsyms_strs,
    dynamic,
    sysv_hash: sysv_hash_table,
    gnu_hash: gnu_hash_table,
  }
}

///|
/// Locate GNU symbol-versioning data, if the object uses it.
///
/// Returns a `SymbolVersionTable` that can map dynamic-symbol indices to
/// version requirements (`.gnu.version_r`) and definitions (`.gnu.version_d`).
/// GNU symbol versioning is optional, so files without `.gnu.version` return
/// `None`.
pub fn ElfBytes::symbol_version_table(
  self : ElfBytes,
) -> SymbolVersionTable? raise ParseError {
  let shdrs = match self.section_headers() {
    Some(shdrs) => shdrs
    None => return None
  }
  let mut versym_opt : SectionHeader? = None
  let mut needs_opt : SectionHeader? = None
  let mut defs_opt : SectionHeader? = None
  for shdr in shdrs.iter() {
    if shdr.sh_type == SHT_GNU_VERSYM {
      versym_opt = Some(shdr)
    } else if shdr.sh_type == SHT_GNU_VERNEED {
      needs_opt = Some(shdr)
    } else if shdr.sh_type == SHT_GNU_VERDEF {
      defs_opt = Some(shdr)
    }
  }
  let versym = match versym_opt {
    Some(shdr) => shdr
    None => return None
  }
  // `.gnu.version` has one 16-bit version index per dynamic symbol.
  ignore(
    validate_entsize(
      u64_to_int_checked(versym.sh_entsize),
      VersionIndex::size_for(self.ehdr.class),
    ),
  )
  let (versym_start, versym_end) = versym.get_data_range()
  let version_ids = VersionIndexTable(
    self.ehdr.endianness,
    self.ehdr.class,
    slice_checked(self.data, versym_start, versym_end),
  )
  let verneeds = match needs_opt {
    Some(shdr) => {
      // Version requirements carry library and version-name offsets into the
      // string table linked by the section header.
      let (start, end) = shdr.get_data_range()
      let strs_shdr = shdrs.get(uint_to_int_checked(shdr.sh_link))
      let (strs_start, strs_end) = strs_shdr.get_data_range()
      Some(
        (
          VerNeedIterator::new(
            self.ehdr.endianness,
            self.ehdr.class,
            shdr.sh_info.to_uint64(),
            0,
            slice_checked(self.data, start, end),
          ),
          StringTable::new(slice_checked(self.data, strs_start, strs_end)),
        ),
      )
    }
    None => None
  }
  let verdefs = match defs_opt {
    Some(shdr) => {
      // Version definitions also refer to names in their linked string table.
      let (start, end) = shdr.get_data_range()
      let strs_shdr = shdrs.get(uint_to_int_checked(shdr.sh_link))
      let (strs_start, strs_end) = strs_shdr.get_data_range()
      Some(
        (
          VerDefIterator::new(
            self.ehdr.endianness,
            self.ehdr.class,
            shdr.sh_info.to_uint64(),
            0,
            slice_checked(self.data, start, end),
          ),
          StringTable::new(slice_checked(self.data, strs_start, strs_end)),
        ),
      )
    }
    None => None
  }
  Some(SymbolVersionTable::new(version_ids, verneeds, verdefs))
}