///|
fn shared_metadata_role(role : String) -> String {
  if role.has_suffix("/l1") {
    "l1"
  } else if role.has_suffix("/l2") {
    "l2"
  } else if role.has_prefix("bitmap-data:") {
    "bitmap-data"
  } else {
    role
  }
}

///|
/// Validate metadata aliasing. Shared identical L1/L2/bitmap data blocks are
/// allowed; overlap between unrelated structures is an error.
pub fn Image::layout_issues(
  self : Image,
  max_issues~ : Int32 = 1000,
) -> Array[AuditIssue] raise QcowError {
  if max_issues <= 0 {
    raise Invalid("invalid layout issue limit")
  }
  let regions = self.metadata_regions()
  regions.sort_by(fn(a, b) {
    if a.offset < b.offset {
      -1
    } else if a.offset > b.offset {
      1
    } else if a.length > b.length {
      -1
    } else if a.length < b.length {
      1
    } else {
      0
    }
  })
  let result : Array[AuditIssue] = []
  let mut previous : HostRegion? = None
  for region in regions {
    if region.length == 0UL {
      continue
    }
    match previous {
      Some(last) => {
        let same_range = region.offset == last.offset &&
          region.length == last.length
        let family = shared_metadata_role(region.role)
        let shared = same_range &&
          family == shared_metadata_role(last.role) &&
          (family == "l1" || family == "l2" || family == "bitmap-data")
        if shared {
          continue
        }
        if region.offset < last.offset + last.length {
          result.push({
            severity: "error",
            code: "metadata-overlap",
            offset: region.offset,
            message: region.role + " overlaps " + last.role,
          })
          if result.length() >= max_issues {
            return result
          }
          if region.offset + region.length <= last.offset + last.length {
            continue
          }
        }
      }
      None => ()
    }
    previous = Some(region)
  }
  result
}

///|
pub fn Image::metadata_size(self : Image) -> UInt64 raise QcowError {
  region_union_size(self.metadata_regions()[:])
}

///|
pub(all) struct ChainLayer {
  depth : Int32
  format : String
  virtual_size : UInt64
  physical_size : UInt64
  backing_filename : String?
  version : Int32
}

///|
/// Describe attached sources without opening any additional files.
pub fn Image::chain(self : Image) -> Array[ChainLayer] raise QcowError {
  let result : Array[ChainLayer] = []
  let mut image = self
  let mut depth = 0
  while true {
    result.push({
      depth,
      format: "qcow2",
      virtual_size: image.virtual_size(),
      physical_size: image.physical_size(),
      backing_filename: backing_filename(image.source, limits=image.limits),
      version: image.header.version,
    })
    match image.backing {
      Some(Qcow(base)) => {
        image = base
        depth = depth + 1
      }
      Some(Raw(source)) => {
        result.push({
          depth: depth + 1,
          format: "raw",
          virtual_size: source.size,
          physical_size: source.size,
          backing_filename: None,
          version: 0,
        })
        break
      }
      None => break
    }
  }
  result
}

///|
pub fn ChainLayer::json(self : ChainLayer) -> Json {
  let filename : Json = match self.backing_filename {
    Some(value) => Json::string(value)
    None => Json::null()
  }
  {
    "depth": self.depth,
    "format": self.format,
    "virtual_size": self.virtual_size.to_string(),
    "physical_size": self.physical_size.to_string(),
    "backing_filename": filename,
    "version": self.version,
  }
}

///|
/// Obtain the host source corresponding to a resolved extent's depth.
pub fn Image::source_at_depth(
  self : Image,
  depth : Int32,
) -> Source raise QcowError {
  if depth < 0 {
    raise Invalid("negative backing depth")
  }
  if depth == 0 {
    return self.source
  }
  match self.backing {
    Some(Qcow(image)) => image.source_at_depth(depth - 1)
    Some(Raw(source)) if depth == 1 => source
    _ => raise Invalid("backing depth does not exist")
  }
}

///|
/// Backup intervals outside the selected plan, clipped to virtual size.
pub fn BackupPlan::complement(self : BackupPlan) -> BackupPlan raise QcowError {
  let result : Array[Difference] = []
  let mut position = 0UL
  for range in self.ranges {
    if position < range.offset {
      result.push({ offset: position, length: range.offset - position, })
    }
    position = range.offset + range.length
  }
  if position < self.virtual_size {
    result.push({ offset: position, length: self.virtual_size - position, })
  }
  BackupPlan::new(self.virtual_size, result[:])
}

///|
pub fn BackupPlan::window(
  self : BackupPlan,
  offset : UInt64,
  length : UInt64,
) -> BackupPlan raise QcowError {
  let selection : Array[Difference] = [{ offset, length, }]
  let mask = BackupPlan::new(self.virtual_size, selection[:])
  self.intersection(mask)
}

///|
pub fn BackupPlan::chunk_count(
  self : BackupPlan,
  chunk_size : UInt64,
) -> UInt64 raise QcowError {
  if chunk_size == 0UL {
    raise Invalid("zero backup chunk size")
  }
  let mut count = 0UL
  for range in self.ranges {
    count = add(count, ceiling(range.length, chunk_size))
  }
  count
}