///|
/// Named target region. Disjoint regions describe the allowed payload union;
/// executable marks regions in which an execution entry is permitted.
pub(all) struct MemoryRegion {
  name : String
  range : @model.AddressRange
  executable : Bool
} derive(Eq, Debug)

///|
/// Whether entry checks are disabled, advisory, or required for acceptance.
pub(all) enum CheckLevel {
  Disabled
  Warning
  Error
} derive(Eq, Debug)

///|
/// Stable categories for machine-readable target-layout results.
pub(all) enum LayoutRule {
  OutsideAllowedRegions
  SegmentStartAlignment
  SegmentLengthAlignment
  PayloadBudget
  SpanBudget
  MissingEntry
  EntryNotMapped
  EntryNotExecutable
  EntryAlignment
} derive(Eq, Debug)

///|
/// A specific violation. Ranges are half-open, matching the memory model.
/// region is provided when an entry belongs to a non-executable named region.
pub struct LayoutIssue {
  rule : LayoutRule
  level : CheckLevel
  range : @model.AddressRange?
  region : String?
  message : String
} derive(Eq, Debug)

///|
/// Layout constraints. Empty regions allow no payload; use one full-address
/// region to permit any address. Entry normalization never changes the image.
/// Alignment values are positive powers of two, and 1 disables alignment.
pub(all) struct LayoutOptions {
  regions : Array[MemoryRegion]
  segment_start_alignment : Int
  segment_length_alignment : Int
  max_payload_bytes : Int
  max_span_bytes : Int64
  entry_required : CheckLevel
  entry_mapped : CheckLevel
  entry_executable : CheckLevel
  entry_alignment : Int
  clear_entry_thumb_bit : Bool
} derive(Eq, Debug)

///|
/// Default constraints for an explicit target region list. Payload must fit
/// those regions; entry placement problems are warnings, not parse errors.
pub fn LayoutOptions::new(regions : Array[MemoryRegion]) -> LayoutOptions {
  {
    regions: regions.copy(),
    segment_start_alignment: 1,
    segment_length_alignment: 1,
    max_payload_bytes: 64 * 1024 * 1024,
    max_span_bytes: 0x100000000L,
    entry_required: Disabled,
    entry_mapped: Warning,
    entry_executable: Warning,
    entry_alignment: 1,
    clear_entry_thumb_bit: false,
  }
}

///|
/// Complete deterministic result. Invalid configuration raises FirmwareError;
/// a valid configuration reports every layout violation without fail-fast.
pub struct LayoutReport {
  issues : Array[LayoutIssue]
  payload_bytes : Int
  address_span : Int64
  segment_count : Int
  checked_entry : Int64?
} derive(Eq, Debug)

///|
/// Warnings do not reject an image. A report can be accepted while advisory
/// entry placement checks remain visible in issues.
pub fn LayoutReport::is_valid(self : LayoutReport) -> Bool {
  !self.issues.any(issue => issue.level == Error)
}

///|
/// Produce a readable validation report without discarding typed rule data.
pub fn LayoutReport::render(self : LayoutReport) -> String {
  let out = StringBuilder()
  out.write_string(
    if self.is_valid() {
      "Layout: valid\n"
    } else {
      "Layout: invalid\n"
    },
  )
  out.write_string("Payload: \{self.payload_bytes} bytes\n")
  out.write_string("Address span: \{self.address_span} bytes\n")
  out.write_string("Segments: \{self.segment_count}\n")
  for issue in self.issues {
    out.write_string(
      if issue.level == Warning {
        "warning: "
      } else {
        "error: "
      },
    )
    out.write_string(issue.message)
    if issue.range is Some(range) {
      out.write_string(" at " + range.render())
    }
    if issue.region is Some(name) {
      out.write_string(" (region " + name + ")")
    }
    out.write_char('\n')
  }
  out.to_string()
}

///|
fn valid_alignment(value : Int) -> Bool {
  value > 0 && value <= 1024 * 1024 && (value & (value - 1)) == 0
}

///|
fn checked_regions(
  options : LayoutOptions,
) -> Array[MemoryRegion] raise @model.FirmwareError {
  if !valid_alignment(options.segment_start_alignment) ||
    !valid_alignment(options.segment_length_alignment) ||
    !valid_alignment(options.entry_alignment) {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "layout alignments must be powers of two between 1 and 1048576",
      ),
    )
  }
  if options.max_payload_bytes < 0 ||
    options.max_payload_bytes > 64 * 1024 * 1024 ||
    options.max_span_bytes < 0L ||
    options.max_span_bytes > 0x100000000L {
    raise @model.FirmwareError(
      @model.diagnostic(
        InvalidOption,
        "layout budgets exceed supported payload or address bounds",
      ),
    )
  }
  if options.regions.length() > 1024 {
    raise @model.FirmwareError(
      @model.diagnostic(
        ResourceLimit,
        "target layout permits at most 1024 named regions",
      ),
    )
  }
  let regions = options.regions.copy()
  regions.sort_by_key(region => region.range.start)
  let names : Array[String] = []
  for i in 0.. 128 ||
      region.name.contains("\n") ||
      region.name.contains("\r") {
      raise @model.FirmwareError(
        @model.diagnostic(
          InvalidOption,
          "region names must be nonempty single-line strings of at most 128 characters",
        ),
      )
    }
    if names.contains(region.name) {
      raise @model.FirmwareError(
        @model.diagnostic(
          InvalidOption,
          "duplicate target region name: " + region.name,
        ),
      )
    }
    names.push(region.name)
    if region.range.is_empty() {
      raise @model.FirmwareError(
        @model.diagnostic(
          InvalidOption,
          "target region cannot be empty: " + region.name,
          address=region.range.start,
        ),
      )
    }
    if i > 0 && regions[i - 1].range.end > region.range.start {
      raise @model.FirmwareError(
        @model.diagnostic(
          InvalidOption,
          "target regions overlap: " +
          regions[i - 1].name +
          " and " +
          region.name,
          address=region.range.start,
          end_address=regions[i - 1].range.end.min(region.range.end) - 1L,
        ),
      )
    }
  }
  regions
}