///|
/// Execution metadata conflicts are separate from data overwrite policy.
pub(all) enum EntryPolicy {
  RejectEntryConflict
  PreferFirst
  PreferLast
} derive(Eq, Debug)

///|
/// Merge images transactionally; no caller-owned image is modified on failure.
/// Default rejects every overlap. AllowIdentical still rejects differing bytes.
pub fn merge_images(
  images : Array[@model.FirmwareImage],
  policy? : @model.OverlapPolicy = Reject,
  entry_policy? : EntryPolicy = RejectEntryConflict,
) -> @model.FirmwareImage raise @model.FirmwareError {
  if images.is_empty() {
    raise @model.FirmwareError(
      @model.diagnostic(InvalidOption, "merge requires at least one image"),
    )
  }
  let memory = @model.MemoryMap::new()
  let warnings = []
  let mut entry : @model.EntryPoint? = None
  let mut header : Bytes? = None
  for image in images {
    if image.entry is Some(value) {
      ignore(value.address())
      if entry != None && entry != image.entry {
        match entry_policy {
          RejectEntryConflict =>
            raise @model.FirmwareError(
              @model.diagnostic(
                EntryConflict,
                "merge has conflicting execution entry points",
              ),
            )
          PreferFirst =>
            warnings.push(
              @model.diagnostic(
                EntryConflict,
                "merge retained first execution entry",
              ),
            )
          PreferLast => {
            entry = image.entry
            warnings.push(
              @model.diagnostic(
                EntryConflict,
                "merge retained last execution entry",
              ),
            )
          }
        }
      } else {
        entry = image.entry
      }
    }
    if image.metadata.header is Some(value) {
      if header != None && header != Some(value) {
        warnings.push(
          @model.diagnostic(InvalidRecord, "merge retained first S0 header"),
        )
      } else {
        header = Some(value)
      }
    }
    for s in image.memory.segments() {
      memory.insert(s.start, s.data, policy~)
    }
    for d in image.warnings {
      warnings.push(d)
    }
  }
  {
    memory,
    entry,
    warnings,
    metadata: { ..@model.Metadata::new(Unknown), header, },
  }
}

///|
/// Extract a half-open window. An entry outside it is dropped with a warning.
pub fn extract_range(
  image : @model.FirmwareImage,
  range : @model.AddressRange,
) -> @model.FirmwareImage raise @model.FirmwareError {
  let warnings = image.warnings.copy()
  let mut entry = image.entry
  if entry is Some(value) {
    if !range.contains(value.address()) {
      entry = None
      warnings.push(
        @model.diagnostic(
          InvalidRange,
          "extraction removed entry point outside the selected range",
        ),
      )
    }
  }
  {
    memory: image.memory.slice(range),
    entry,
    warnings,
    metadata: {
      ..@model.Metadata::new(Unknown),
      header: image.metadata.header,
    },
  }
}

///|
/// Relocate all payload and execution addresses with checked signed arithmetic.
/// Segmented entry representation is replaced with Linear and diagnosed.
pub fn relocate(
  image : @model.FirmwareImage,
  delta : Int64,
) -> @model.FirmwareImage raise @model.FirmwareError {
  let memory = @model.MemoryMap::new()
  for segment in image.memory.segments() {
    let shifted = segment.range().shift(delta)
    memory.insert(shifted.start, segment.data)
  }
  let warnings = image.warnings.copy()
  let entry = match image.entry {
    Some(value) => {
      let address = value.address()
      let shifted = @model.AddressRange::inclusive(address, address).shift(
        delta,
      )
      if value is Segment(_, _) {
        warnings.push(
          @model.diagnostic(
            InvalidRecord,
            "relocation converted segmented entry to linear address",
          ),
        )
      }
      Some(@model.EntryPoint::Linear(shifted.start))
    }
    None => None
  }
  {
    memory,
    entry,
    warnings,
    metadata: {
      ..@model.Metadata::new(Unknown),
      header: image.metadata.header,
    },
  }
}

///|
/// Remove a window, preserving both disjoint outer portions and dropping an
/// entry that points into removed memory. This never fills the new gap.
pub fn remove_range(
  image : @model.FirmwareImage,
  range : @model.AddressRange,
) -> @model.FirmwareImage raise @model.FirmwareError {
  let memory = @model.MemoryMap::new()
  for s in image.memory.segments() {
    for retained in s.range().subtract(range) {
      let start = (retained.start - s.start).to_int()
      let end = (retained.end - s.start).to_int()
      memory.insert(retained.start, s.data[start:end].to_owned())
    }
  }
  let warnings = image.warnings.copy()
  let mut entry = image.entry
  if entry is Some(value) {
    if range.contains(value.address()) {
      entry = None
      warnings.push(
        @model.diagnostic(
          InvalidRange,
          "removed execution entry within erased window",
        ),
      )
    }
  }
  {
    memory,
    entry,
    warnings,
    metadata: {
      ..@model.Metadata::new(Unknown),
      header: image.metadata.header,
    },
  }
}

///|
/// Fill only unoccupied bytes in a bounded window. Existing payload is retained.
pub fn fill_range(
  image : @model.FirmwareImage,
  range : @model.AddressRange,
  value : Byte,
  max_added? : Int = 16 * 1024 * 1024,
) -> @model.FirmwareImage raise @model.FirmwareError {
  let holes = image.memory.holes_in(range)
  let mut added = 0L
  for hole in holes {
    added += hole.length()
  }
  if max_added < 0 ||
    max_added > 64 * 1024 * 1024 ||
    added > max_added.to_int64() ||
    added > (64 * 1024 * 1024 - image.memory.payload_size()).to_int64() {
    raise @model.FirmwareError(
      @model.diagnostic(ResourceLimit, "fill would exceed payload budget"),
    )
  }
  let result = image.copy()
  for hole in holes {
    result.memory.insert(hole.start, Bytes::make(hole.length().to_int(), value))
  }
  {
    ..result,
    metadata: {
      ..@model.Metadata::new(Unknown),
      header: image.metadata.header,
    },
  }
}