// dtbscope — device tree compiler and inspector in pure MoonBit.
//
// This file holds the vocabulary shared by every parser in the module: the
// error type and the byte-order cursor. Keep it free of format-specific rules
// so that the DTS front end and the DTB back end can both depend on it.

///|
/// Every way a device tree blob can be rejected.
///
/// Parsing never returns a half-built tree. A damaged blob surfaces as one of
/// these, and the payload carries the offset of the bytes that failed so a
/// caller can point at them instead of guessing.
pub(all) suberror DtbError {
  /// The buffer ended before a structure could be read completely.
  UnexpectedEnd(offset~ : Int64, needed~ : Int64, available~ : Int64)
  /// The magic number is not 0xd00dfeed.
  BadMagic(magic~ : UInt32)
  /// A field holds a value the specification does not allow.
  BadValue(field~ : String, value~ : Int64)
  /// A size or offset addresses bytes outside the blob.
  OutOfBounds(what~ : String, offset~ : Int64, size~ : Int64, limit~ : Int64)
  /// A name or string is not valid UTF-8.
  BadName(offset~ : Int64)
  /// Two structures are each well-formed but contradict one another.
  Inconsistent(what~ : String, detail~ : String)
  /// A nesting depth or node count exceeded the configured limit.
  LimitExceeded(what~ : String, limit~ : Int64)
  /// A code path this version does not implement was reached.
  Unsupported(what~ : String)
}

///|
/// Renders an error as one line naming the field and the offset.
///
/// The CLI prints this verbatim, so it has to identify a byte, not just say
/// "malformed file".
pub impl Show for DtbError with fn output(self, logger) {
  let text = match self {
    UnexpectedEnd(offset~, needed~, available~) =>
      "unexpected end at offset \{offset}: needed \{needed} bytes, \{available} available"
    BadMagic(magic~) => "not a device tree blob: magic \{hex32(magic)}"
    BadValue(field~, value~) => "bad value for \{field}: \{value}"
    OutOfBounds(what~, offset~, size~, limit~) =>
      "\{what} at offset \{offset}+\{size} exceeds limit \{limit}"
    BadName(offset~) => "invalid string at offset \{offset}"
    Inconsistent(what~, detail~) => "\{what} is inconsistent: \{detail}"
    LimitExceeded(what~, limit~) => "\{what} exceeds the limit of \{limit}"
    Unsupported(what~) => "unsupported: \{what}"
  }
  logger.write_string(text)
}

///|
/// Exposes the message as a plain method so callers can format it themselves.
pub extend DtbError with Show::{to_string, output}

///|
/// Byte order of a field.
///
/// The DTB format is big-endian throughout, but the DTS front end has to quote
/// `/bits/ 16` cell lists in the host's own order when it dumps them back out,
/// so the reader keeps the axis explicit rather than hard-coding it.
pub(all) enum Endian {
  Little
  Big
}

///|
/// A position in a buffer together with the byte order to decode it in.
pub(all) struct Cursor {
  /// Absolute byte offset of the next field.
  pos : Int64
  /// Byte order applied to multi-byte fields.
  endian : Endian
}

///|
/// Produces a cursor at `pos` reading little-endian fields.
pub fn Cursor::little(pos : Int64) -> Cursor {
  Cursor::{ pos, endian: Little, }
}

///|
/// Produces a cursor at `pos` reading big-endian fields, which is what every
/// field inside a DTB uses.
pub fn Cursor::big(pos : Int64) -> Cursor {
  Cursor::{ pos, endian: Big, }
}

///|
/// Formats a 32-bit value as `0x` followed by just enough hex digits.
///
/// Used in error messages where the reader is comparing against a hex dump, so
/// leading zeroes are dropped but the prefix is always present.
fn hex32(v : UInt32) -> String {
  let digits = "0123456789abcdef"
  let raw = v.reinterpret_as_int()
  let buf = StringBuilder()
  buf.write_string("0x")
  let mut started = false
  for i in 0..<8 {
    let index = (raw >> ((7 - i) * 4)) & 0xF
    if index != 0 || started || i == 7 {
      started = true
      buf.write_char(digits.get_char(index).unwrap())
    }
  }
  buf.to_string()
}