///|
pub struct DataSearchMatch {
  segment_index : Int
  payload_offset : Int
  file_offset : Int
  size : Int
} derive(Eq, Debug)

///|
/// Full results, ordered by original segment index and payload byte offset.
pub struct DataSearchReport {
  file_size : Int
  sha256 : String
  query : String
  query_size : Int
  matching_rule : String
  scope : String
  data_segments : DataSegmentsReport
  segment_count : Int
  matched_segment_count : Int
  unmatched_segment_count : Int
  occurrence_count : Int
  matches : Array[DataSearchMatch]
}

///|
pub enum DataSearchError {
  EmptyDataSearchQuery
  DataSearchInputError(WasmError)
}

///|
pub fn describe_data_search_error(error : DataSearchError) -> String {
  match error {
    EmptyDataSearchQuery => "data search query must not be empty"
    DataSearchInputError(error) => describe_error(error)
  }
}

///|
/// KMP prefix table; each mismatch follows a previously computed proper suffix.
fn data_search_prefix(query : Bytes) -> Array[Int] {
  let prefix = Array::make(query.length(), 0)
  let mut matched = 0
  for i = 1; i < query.length(); i = i + 1 {
    while matched > 0 && query[i] != query[matched] {
      matched = prefix[matched - 1]
    }
    if query[i] == query[matched] {
      matched += 1
    }
    prefix[i] = matched
  }
  prefix
}

///|
/// needle is nonempty and report belongs to data. One prefix table per search;
/// each payload resets state, overlaps retain the longest proper suffix.
fn data_search_matches(
  data : Bytes,
  report : DataSegmentsReport,
  needle : Bytes,
) -> (Array[DataSearchMatch], Int) {
  let prefix = data_search_prefix(needle)
  let matches : Array[DataSearchMatch] = []
  let mut matched_segment_count = 0
  for entry in report.entries {
    let mut matched = 0
    let mut found = false
    // Ranges were checked by inspect_data_segments against this same input.
    for i = 0; i < entry.payload.size; i = i + 1 {
      let byte = data[entry.payload.offset + i]
      while matched > 0 && byte != needle[matched] {
        matched = prefix[matched - 1]
      }
      if byte == needle[matched] {
        matched += 1
      }
      if matched == needle.length() {
        let payload_offset = i + 1 - needle.length()
        matches.push({
          segment_index: entry.index,
          payload_offset,
          file_offset: entry.payload.offset + payload_offset,
          size: needle.length(),
        })
        found = true
        matched = prefix[matched - 1]
      }
    }
    if found {
      matched_segment_count += 1
    }
  }
  (matches, matched_segment_count)
}

///|
/// One structural inspection and query encoding. Linear in query bytes,
/// payload bytes and occurrences; retains every hit, including overlaps.
/// Payload bytes are neither copied nor decoded as text. No function decoding
/// or graph analysis is performed beyond the inspector's module checks.
pub fn search_data_segments(
  data : Bytes,
  query : String,
) -> Result[DataSearchReport, DataSearchError] {
  if query.is_empty() {
    return Err(EmptyDataSearchQuery)
  }
  let needle = @utf8.encode(query)
  let report = match inspect_data_segments(data) {
    Ok(value) => value
    Err(error) => return Err(DataSearchInputError(error))
  }
  let (matches, matched_segment_count) = data_search_matches(
    data, report, needle,
  )
  Ok({
    file_size: data.length(),
    sha256: experiment_sha256(data),
    query,
    query_size: needle.length(),
    matching_rule: "Case-sensitive literal UTF-8 byte matching within each data payload; overlapping occurrences retained. No trimming, Unicode normalization, escape interpretation, regex or UTF-16 guessing. Invalid UTF-8 elsewhere in a payload is allowed. Never matches across entries or searches encoding metadata.",
    scope: "A hit only records query bytes in an initialization payload, not an independent string, source constant or used resource. No match does not prove content is absent: compiler string encodings may differ. File offsets are not memory addresses. No deletion savings or safe deduplication are inferred. Overlapping occurrence lengths must not be added as occupied bytes. Offset expressions remain unevaluated. Top limits text rows only, not search cost or complete JSON; result memory grows with occurrences. The query itself is included in reports.",
    data_segments: report,
    segment_count: report.entries.length(),
    matched_segment_count,
    unmatched_segment_count: report.entries.length() - matched_segment_count,
    occurrence_count: matches.length(),
    matches,
  })
}