///|
/// Bytes plus a per-bit mask. A zero mask bit is a wildcard; an all-ones mask
/// performs an exact byte comparison.
pub struct MaskedPattern {
  bytes : Bytes
  mask : Bytes
} derive(Eq, Debug)

///|
/// Construct an exact nonempty pattern.
pub fn MaskedPattern::exact(
  bytes : Bytes,
) -> MaskedPattern raise @model.FirmwareError {
  if bytes.is_empty() {
    raise @model.FirmwareError(
      @model.diagnostic(InvalidOption, "search pattern cannot be empty"),
    )
  }
  if bytes.length() > 1024 * 1024 {
    raise @model.FirmwareError(
      @model.diagnostic(ResourceLimit, "search pattern exceeds 1 MiB"),
    )
  }
  { bytes, mask: Bytes::make(bytes.length(), 0xFF), }
}

///|
/// Construct a masked nonempty pattern. Pattern and mask lengths must match.
pub fn MaskedPattern::new(
  bytes : Bytes,
  mask : Bytes,
) -> MaskedPattern raise @model.FirmwareError {
  if bytes.is_empty() || bytes.length() != mask.length() {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "search bytes and mask must have the same nonzero length",
      ),
    )
  }
  if bytes.length() > 1024 * 1024 {
    raise @model.FirmwareError(
      @model.diagnostic(ResourceLimit, "search pattern exceeds 1 MiB"),
    )
  }
  { bytes, mask, }
}

///|
fn pattern_matches(data : Bytes, offset : Int, pattern : MaskedPattern) -> Bool {
  for index in 0.. Array[Int64] raise @model.FirmwareError {
  if max_matches < 0 || max_matches > 1000000 {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "match limit must be between 0 and 1000000",
      ),
    )
  }
  let result = []
  for segment in image.memory.segments() {
    let searched = match within {
      Some(window) => segment.range().intersection(window)
      None => Some(segment.range())
    }
    if searched is Some(range) {
      let from = (range.start - segment.start).to_int()
      let until = (range.end - segment.start).to_int()
      let last = until - pattern.bytes.length()
      if last >= from {
        for offset in from..<=last {
          if pattern_matches(segment.data, offset, pattern) {
            if result.length() >= max_matches {
              raise @model.FirmwareError(
                @model.diagnostic(ResourceLimit, "pattern match limit exceeded"),
              )
            }
            result.push(segment.start + offset.to_int64())
          }
        }
      }
    }
  }
  result
}

///|
/// One printable ASCII run, retaining its absolute firmware address and bytes.
pub struct AsciiSpan {
  address : Int64
  bytes : Bytes
} derive(Eq, Debug)

///|
fn printable_ascii(byte : Byte) -> Bool {
  let value = byte.to_int()
  value >= 32 && value <= 126
}

///|
/// Extract printable ASCII runs from occupied data. A gap always terminates a
/// run. Limits bound both returned objects and retained bytes.
pub fn find_ascii_strings(
  image : @model.FirmwareImage,
  min_length? : Int = 4,
  max_strings? : Int = 10000,
  max_retained_bytes? : Int = 16 * 1024 * 1024,
) -> Array[AsciiSpan] raise @model.FirmwareError {
  if min_length < 1 ||
    min_length > 1024 * 1024 ||
    max_strings < 0 ||
    max_strings > 1000000 ||
    max_retained_bytes < 0 ||
    max_retained_bytes > 64 * 1024 * 1024 {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "ASCII search limits are outside supported bounds",
      ),
    )
  }
  let result = []
  let mut retained = 0
  for segment in image.memory.segments() {
    let mut start = 0
    while start < segment.data.length() {
      while start < segment.data.length() &&
            !printable_ascii(segment.data[start]) {
        start += 1
      }
      let mut end = start
      while end < segment.data.length() && printable_ascii(segment.data[end]) {
        end += 1
      }
      if end - start >= min_length {
        if result.length() >= max_strings ||
          end - start > max_retained_bytes - retained {
          raise @model.FirmwareError(
            @model.diagnostic(
              ResourceLimit,
              "ASCII search result limit exceeded",
            ),
          )
        }
        result.push({
          address: segment.start + start.to_int64(),
          bytes: segment.data[start:end].to_owned(),
        })
        retained += end - start
      }
      start = if end == start { start + 1 } else { end }
    }
  }
  result
}