///|
/// Parsed ELF note.
///
/// Known GNU notes are decoded into dedicated variants. Other note records are
/// preserved as `Unknown` so callers can inspect the raw owner name and
/// descriptor bytes.
pub(all) enum Note {
  /// GNU ABI tag note (`name == "GNU\0"`, `n_type == NT_GNU_ABI_TAG`).
  GnuAbiTag(NoteGnuAbiTag)
  /// GNU build ID note (`name == "GNU\0"`, `n_type == NT_GNU_BUILD_ID`).
  GnuBuildId(NoteGnuBuildId)
  /// Any note that is not recognized by this package.
  Unknown(NoteAny)
} derive(Debug, Eq)

///|
/// Contents of a GNU ABI tag note.
///
/// The version fields describe the earliest compatible kernel ABI. For
/// example, `major = 2`, `minor = 6`, `subminor = 32` means Linux 2.6.32.
pub(all) struct NoteGnuAbiTag {
  /// Operating-system code, such as `ELF_NOTE_GNU_ABI_TAG_OS_LINUX`.
  os : UInt
  /// Major kernel ABI version.
  major : UInt
  /// Minor kernel ABI version.
  minor : UInt
  /// Patch/subminor kernel ABI version.
  subminor : UInt
} derive(Debug, Eq)

///|
/// GNU build ID descriptor bytes.
///
/// The bytes are a view into the original note payload and are not interpreted
/// by the parser.
pub(all) struct NoteGnuBuildId(BytesView) derive(Debug, Eq)

///|
/// Raw representation of an ELF note the parser does not specialize.
pub(all) struct NoteAny {
  /// Note type value from the note header.
  n_type : UInt64
  /// Raw owner/name bytes, usually including a trailing NUL byte.
  name : BytesView
  /// Raw note descriptor bytes.
  desc : BytesView
} derive(Debug, Eq)

///|
/// Decode the note owner/name bytes as UTF-8 and trim trailing NUL bytes.
pub fn NoteAny::name_str(self : NoteAny) -> String raise ParseError {
  let text = @utf8.decode(self.name) catch { _ => raise Utf8Error }
  let mut end = text.length()
  while end > 0 && text[end - 1] == 0 {
    end -= 1
  }
  text[0:end].to_owned()
}

///|
struct NoteHeader {
  n_namesz : UInt64
  n_descsz : UInt64
  n_type : UInt64
} derive(Debug, Eq)

///|
impl ParseAt for NoteHeader with parse_at(endian, class, offset, data) {
  let size = match class {
    ELF32 => 12
    ELF64 => 24
  }
  let end = checked_add(offset, size)
  let record = slice_checked(data, offset, end)
  let header = match (class, endian) {
    (ELF32, Little) =>
      match record {
        [u32le(n_namesz32), u32le(n_descsz32), u32le(n_type32)] =>
          {
            n_namesz: n_namesz32.to_uint64(),
            n_descsz: n_descsz32.to_uint64(),
            n_type: n_type32.to_uint64(),
          }
        _ => raise SliceReadError(offset, end)
      }
    (ELF32, Big) =>
      match record {
        [u32be(n_namesz32), u32be(n_descsz32), u32be(n_type32)] =>
          {
            n_namesz: n_namesz32.to_uint64(),
            n_descsz: n_descsz32.to_uint64(),
            n_type: n_type32.to_uint64(),
          }
        _ => raise SliceReadError(offset, end)
      }
    (ELF64, Little) =>
      match record {
        [u64le(n_namesz), u64le(n_descsz), u64le(n_type)] =>
          { n_namesz, n_descsz, n_type }
        _ => raise SliceReadError(offset, end)
      }
    (ELF64, Big) =>
      match record {
        [u64be(n_namesz), u64be(n_descsz), u64be(n_type)] =>
          { n_namesz, n_descsz, n_type }
        _ => raise SliceReadError(offset, end)
      }
  }
  (header, end)
}

///|
impl ParseAt for NoteGnuAbiTag with parse_at(endian, _class, offset, data) {
  let end = checked_add(offset, 16)
  let record = slice_checked(data, offset, end)
  let tag = match endian {
    Little =>
      match record {
        [u32le(os), u32le(major), u32le(minor), u32le(subminor)] =>
          { os, major, minor, subminor }
        _ => raise SliceReadError(offset, end)
      }
    Big =>
      match record {
        [u32be(os), u32be(major), u32be(minor), u32be(subminor)] =>
          { os, major, minor, subminor }
        _ => raise SliceReadError(offset, end)
      }
  }
  (tag, end)
}

///|
fn align_offset(offset : Int, align : Int) -> Int raise ParseError {
  if align <= 1 {
    offset
  } else {
    let remainder = offset % align
    if remainder == 0 {
      offset
    } else {
      checked_add(offset, align - remainder)
    }
  }
}

///|
fn Note::parse_at(
  endian : Endian,
  class : Class,
  align : Int,
  offset : Int,
  data : BytesView,
) -> (Note, Int) raise ParseError {
  let align = if align == 0 { 4 } else { align }
  // ELF notes are commonly encoded with 32-bit note headers even in 64-bit objects
  let (nhdr, next) = NoteHeader::parse_at(endian, ELF32, offset, data)
  let mut idx = next
  let name_start = idx
  let name_size = u64_to_int_checked(nhdr.n_namesz)
  let name_end = checked_add(name_start, name_size)
  let name = slice_checked(data, name_start, name_end)
  idx = align_offset(name_end, align)
  let desc_start = idx
  let desc_size = u64_to_int_checked(nhdr.n_descsz)
  let desc_end = checked_add(desc_start, desc_size)
  let desc = slice_checked(data, desc_start, desc_end)
  idx = align_offset(desc_end, align)
  let note = if name == ELF_NOTE_GNU[:] && nhdr.n_type == NT_GNU_ABI_TAG {
    // GNU ABI tag descriptors contain four 32-bit words.
    let (abi_tag, _) = NoteGnuAbiTag::parse_at(endian, class, 0, desc)
    GnuAbiTag(abi_tag)
  } else if name == ELF_NOTE_GNU[:] && nhdr.n_type == NT_GNU_BUILD_ID {
    GnuBuildId(NoteGnuBuildId(desc))
  } else {
    Unknown({ n_type: nhdr.n_type, name, desc })
  }
  (note, idx)
}

///|
/// Stateful iterator over notes in a note section or note segment.
///
/// Invalid note data ends iteration. Use `ElfBytes::section_data_as_notes` or
/// `ElfBytes::segment_data_as_notes` to construct this with the correct class,
/// endian, and alignment.
pub struct NoteIterator {
  /// Byte order used for note headers and known descriptors.
  endian : Endian
  /// ELF class of the owning file.
  class : Class
  /// Alignment used between note name and descriptor payloads.
  align : Int
  /// Raw note section or segment bytes.
  data : BytesView
  /// Current byte offset in `data`.
  mut offset : Int
}

///|
/// Create a note iterator over raw note bytes.
///
/// `align` comes from `sh_addralign` for note sections or `p_align` for note
/// segments. A value of zero is treated as four-byte note alignment.
pub fn NoteIterator::new(
  endian : Endian,
  class : Class,
  align : Int,
  data : BytesView,
) -> NoteIterator {
  { endian, class, align, data, offset: 0 }
}

///|
/// Parse and return the next note, or `None` at end of data or after malformed input.
pub fn NoteIterator::next(self : NoteIterator) -> Note? {
  if self.offset >= self.data.length() {
    None
  } else {
    try
      Note::parse_at(
        self.endian,
        self.class,
        self.align,
        self.offset,
        self.data,
      )
    catch {
      _ => {
        self.offset = self.data.length()
        None
      }
    } noraise {
      (note, next) => {
        self.offset = next
        Some(note)
      }
    }
  }
}

///|
/// Convert the note iterator to a standard `Iter[Note]`.
pub fn NoteIterator::iter(self : NoteIterator) -> Iter[Note] {
  Iter::new(fn() { self.next() })
}