//! A small, dependency-free Rust `mkfs.xfs` library.
//!
//! The main flow is:
//!
//! 1. Build [`FormatOptions`] explicitly or from a target path.
//! 2. Validate and derive geometry with [`FormatPlan::new`].
//! 3. Stream metadata to a seekable target with [`image::write_plan_to`].
//!
//! ```no_run
//! use mkfs_xfs::image::write_plan_to;
//! use mkfs_xfs::{FormatOptions, FormatPlan, InodeGenerationPolicy};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let uuid = [0_u8; 16];
//! let options = FormatOptions::new(1 << 30, uuid)
//!     .with_inode_generation(InodeGenerationPolicy::Random);
//! let plan = FormatPlan::new(options)?;
//!
//! let mut output = std::fs::OpenOptions::new()
//!     .write(true)
//!     .open("disk.img")?;
//! write_plan_to(&plan, &mut output)?;
//! # Ok(())
//! # }
//! ```

pub mod crc32c;
pub mod image;
pub mod ondisk;

use std::fmt;
use std::io;
use std::path::Path;

const BBSHIFT: u8 = 9;
const BBSIZE: u64 = 1 << BBSHIFT;

const XFS_DFL_BLOCKSIZE_LOG: u8 = 12;
const XFS_DINODE_DFL_CRC_LOG: u8 = 9;
const XFS_DFL_IMAXIMUM_PCT: u8 = 25;
const XFS_MIN_REC_DIRSIZE_LOG: u8 = 12;

const XFS_MIN_SECTORSIZE: u32 = 512;
const XFS_MAX_SECTORSIZE: u32 = 32_768;
const XFS_MIN_BLOCKSIZE_LOG: u8 = 9;
const XFS_MAX_BLOCKSIZE_LOG: u8 = 16;
const XFS_MIN_BLOCKSIZE: u32 = 1 << XFS_MIN_BLOCKSIZE_LOG;
const XFS_MAX_BLOCKSIZE: u32 = 1 << XFS_MAX_BLOCKSIZE_LOG;
const XFS_DINODE_MIN_SIZE: u32 = 256;
const XFS_MIN_INODE_PERBLOCK: u32 = 2;

const XFS_AG_MIN_BYTES: u64 = BBSIZE << 15; // 16 MiB
const XFS_AG_MAX_BYTES: u64 = BBSIZE << 31; // 1 TiB
const XFS_MULTIDISK_AGLOG: u8 = 5;
const XFS_NOMULTIDISK_AGLOG: u8 = 2;
const XFS_MIN_LOG_BLOCKS: u64 = 512;
const XFS_MAX_LOG_BLOCKS: u64 = 1024 * 1024;
const XFS_MIN_LOG_BYTES: u64 = 10 * 1024 * 1024;
const XFS_MIN_REALISTIC_LOG_BYTES: u64 = 64 * 1024 * 1024;
const XFS_MAX_LOG_BYTES: u64 = 2 * 1024 * 1024 * 1024 - XFS_MIN_LOG_BYTES;
const XFS_MIN_REALISTIC_FS_BYTES: u64 = 300 * 1024 * 1024;

/// Errors returned while validating mkfs inputs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
    InvalidPowerOfTwo {
        name: &'static str,
        value: u64,
    },
    OutOfRange {
        name: &'static str,
        value: u64,
        min: u64,
        max: u64,
    },
    NotMultiple {
        name: &'static str,
        value: u64,
        unit: u64,
    },
    TooSmall {
        name: &'static str,
        value: u64,
        min: u64,
    },
    UnsupportedFeature {
        name: &'static str,
        reason: &'static str,
    },
    LabelTooLong {
        len: usize,
        max: usize,
    },
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidPowerOfTwo { name, value } => {
                write!(f, "{name} must be a power of two, got {value}")
            }
            Self::OutOfRange {
                name,
                value,
                min,
                max,
            } => write!(f, "{name} {value} is outside {min}..={max}"),
            Self::NotMultiple { name, value, unit } => {
                write!(f, "{name} {value} is not a multiple of {unit}")
            }
            Self::TooSmall { name, value, min } => {
                write!(f, "{name} {value} is smaller than minimum {min}")
            }
            Self::UnsupportedFeature { name, reason } => {
                write!(f, "unsupported feature {name}: {reason}")
            }
            Self::LabelTooLong { len, max } => write!(f, "label is {len} bytes, maximum is {max}"),
        }
    }
}

impl std::error::Error for Error {}

/// Filesystem feature defaults for mkfs.xfs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Features {
    log_version: u8,
    attr_version: u8,
    inode_align: bool,
    lazy_sb_counters: bool,
    projid32bit: bool,
    crcs_enabled: bool,
    dirftype: bool,
    finobt: bool,
    spinodes: bool,
    rmapbt: bool,
    reflink: bool,
    inobtcnt: bool,
    parent_pointers: bool,
    bigtime: bool,
    nrext64: bool,
    exchrange: bool,
}

impl Default for Features {
    fn default() -> Self {
        Self {
            log_version: 2,
            attr_version: 2,
            inode_align: true,
            lazy_sb_counters: true,
            projid32bit: true,
            crcs_enabled: true,
            dirftype: true,
            finobt: true,
            spinodes: true,
            rmapbt: true,
            reflink: true,
            inobtcnt: true,
            parent_pointers: true,
            bigtime: true,
            nrext64: true,
            exchrange: true,
        }
    }
}

impl Features {
    pub fn log_version(self) -> u8 {
        self.log_version
    }

    pub fn attr_version(self) -> u8 {
        self.attr_version
    }

    pub fn inode_align(self) -> bool {
        self.inode_align
    }

    pub fn lazy_sb_counters(self) -> bool {
        self.lazy_sb_counters
    }

    pub fn projid32bit(self) -> bool {
        self.projid32bit
    }

    pub fn crcs_enabled(self) -> bool {
        self.crcs_enabled
    }

    pub fn dirftype(self) -> bool {
        self.dirftype
    }

    pub fn finobt(self) -> bool {
        self.finobt
    }

    pub fn spinodes(self) -> bool {
        self.spinodes
    }

    pub fn rmapbt(self) -> bool {
        self.rmapbt
    }

    pub fn reflink(self) -> bool {
        self.reflink
    }

    pub fn inobtcnt(self) -> bool {
        self.inobtcnt
    }

    pub fn parent_pointers(self) -> bool {
        self.parent_pointers
    }

    pub fn bigtime(self) -> bool {
        self.bigtime
    }

    pub fn nrext64(self) -> bool {
        self.nrext64
    }

    pub fn exchrange(self) -> bool {
        self.exchrange
    }
}

/// User-supplied inputs for planning an XFS format.
///
/// `FormatOptions::new` is the explicit constructor: callers provide the data
/// device size and UUID, then optionally override defaults such as sector size
/// or inode generation policy.
///
/// ```rust
/// use mkfs_xfs::{FormatOptions, InodeGenerationPolicy};
///
/// let uuid = [0_u8; 16];
/// let options = FormatOptions::new(1 << 30, uuid)
///     .with_sector_size(4096)
///     .with_inode_generation(InodeGenerationPolicy::Random);
///
/// assert_eq!(options.data_bytes, 1 << 30);
/// assert_eq!(options.sector_size, 4096);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatOptions {
    /// Data device size in bytes.
    pub data_bytes: u64,
    pub block_size: u32,
    pub sector_size: u32,
    pub inode_size: u32,
    pub uuid: [u8; 16],
    pub label: Option<String>,
    pub multidisk: bool,
    pub features: Features,
    pub inode_generation: InodeGenerationPolicy,
}

impl FormatOptions {
    /// Create default format options for a data device of `data_bytes`.
    ///
    /// Defaults currently follow the implemented single-data-device path:
    /// 4 KiB filesystem blocks, 512-byte sectors, 512-byte inodes, and current
    /// default feature bits.
    pub fn new(data_bytes: u64, uuid: [u8; 16]) -> Self {
        Self {
            data_bytes,
            block_size: 1 << XFS_DFL_BLOCKSIZE_LOG,
            sector_size: XFS_MIN_SECTORSIZE,
            inode_size: 1 << XFS_DINODE_DFL_CRC_LOG,
            uuid,
            label: None,
            multidisk: false,
            features: Features::default(),
            inode_generation: InodeGenerationPolicy::Zero,
        }
    }

    /// Select how inode generation numbers are assigned.
    ///
    /// Use [`InodeGenerationPolicy::Random`] for normal formatting and a
    /// deterministic policy for reproducible tests or byte-for-byte comparison
    /// with `mkfs.xfs`.
    pub fn with_inode_generation(mut self, inode_generation: InodeGenerationPolicy) -> Self {
        self.inode_generation = inode_generation;
        self
    }

    /// Override the filesystem sector size.
    ///
    /// This should usually match the target device logical sector size. On
    /// Linux block devices, [`FormatOptions::from_target`] derives it with
    /// `BLKSSZGET`.
    pub fn with_sector_size(mut self, sector_size: u32) -> Self {
        self.sector_size = sector_size;
        self
    }

    /// Build options from a regular file or Linux block device.
    ///
    /// Regular files use their file length and keep the default 512-byte
    /// sector size. Linux block devices use `BLKGETSIZE64` for size and
    /// `BLKSSZGET` for logical sector size.
    pub fn from_target(path: impl AsRef<Path>, uuid: [u8; 16]) -> io::Result<Self> {
        format_options_from_target(path.as_ref(), uuid)
    }
}

fn format_options_from_target(path: &Path, uuid: [u8; 16]) -> io::Result<FormatOptions> {
    let file = std::fs::File::open(path)?;
    let metadata = file.metadata()?;
    if metadata.is_file() {
        return Ok(FormatOptions::new(metadata.len(), uuid));
    }

    format_options_from_non_file_target(&file, metadata, uuid)
}

#[cfg(target_os = "linux")]
fn format_options_from_non_file_target(
    file: &std::fs::File,
    metadata: std::fs::Metadata,
    uuid: [u8; 16],
) -> io::Result<FormatOptions> {
    use std::os::fd::AsRawFd;
    use std::os::raw::{c_int, c_ulong};
    use std::os::unix::fs::FileTypeExt;

    const BLKGETSIZE64: c_ulong = 0x8008_1272;
    const BLKSSZGET: c_ulong = 0x1268;

    unsafe extern "C" {
        fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
    }

    if !metadata.file_type().is_block_device() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "target is neither a regular file nor a block device",
        ));
    }

    let fd = file.as_raw_fd();
    let mut data_bytes = 0_u64;
    let mut sector_size = 0_i32;

    if unsafe { ioctl(fd, BLKGETSIZE64, &mut data_bytes) } < 0 {
        return Err(io::Error::last_os_error());
    }
    if unsafe { ioctl(fd, BLKSSZGET, &mut sector_size) } < 0 {
        return Err(io::Error::last_os_error());
    }
    if sector_size <= 0 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "block device reported a non-positive logical sector size",
        ));
    }

    Ok(FormatOptions::new(data_bytes, uuid).with_sector_size(sector_size as u32))
}

#[cfg(not(target_os = "linux"))]
fn format_options_from_non_file_target(
    _file: &std::fs::File,
    _metadata: std::fs::Metadata,
    _uuid: [u8; 16],
) -> io::Result<FormatOptions> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "block device probing is only implemented on Linux",
    ))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InodeGenerationPolicy {
    /// Use zero inode generation numbers.
    Zero,
    /// Reproduce xfsprogs when `DETERMINISTIC_SEED=1` is set.
    XfsprogsDeterministicSeed,
    /// Use a caller-provided generation number.
    Fixed(u32),
    /// Generate inode generation numbers from OS randomness.
    Random,
}

impl InodeGenerationPolicy {
    pub const XFSPROGS_DETERMINISTIC_SEED: u32 = 0x5345_4544;

    /// Return a generation number for this policy.
    ///
    /// On Linux, [`InodeGenerationPolicy::Random`] reads from `getrandom(2)`.
    /// On unsupported platforms, random generation returns an error.
    pub fn generation(self) -> io::Result<u32> {
        match self {
            Self::Random => random_u32(),
            _ => self.deterministic_generation().ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "inode generation policy has no deterministic value",
                )
            }),
        }
    }

    /// Return a generation number only when this policy is reproducible.
    pub fn deterministic_generation(self) -> Option<u32> {
        match self {
            Self::Zero => Some(0),
            Self::XfsprogsDeterministicSeed => Some(Self::XFSPROGS_DETERMINISTIC_SEED),
            Self::Fixed(value) => Some(value),
            Self::Random => None,
        }
    }
}

#[cfg(target_os = "linux")]
fn random_u32() -> io::Result<u32> {
    use std::ffi::c_void;
    use std::os::raw::c_uint;

    unsafe extern "C" {
        fn getrandom(buf: *mut c_void, buflen: usize, flags: c_uint) -> isize;
    }

    let mut bytes = [0_u8; std::mem::size_of::<u32>()];
    let mut filled = 0;
    while filled < bytes.len() {
        let ret = unsafe {
            getrandom(
                bytes[filled..].as_mut_ptr().cast::<c_void>(),
                bytes.len() - filled,
                0,
            )
        };
        if ret < 0 {
            let err = io::Error::last_os_error();
            if err.kind() == io::ErrorKind::Interrupted {
                continue;
            }
            return Err(err);
        }
        if ret == 0 {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "getrandom returned no bytes",
            ));
        }
        filled += usize::try_from(ret).map_err(|_| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                "getrandom returned invalid size",
            )
        })?;
    }

    Ok(u32::from_ne_bytes(bytes))
}

#[cfg(not(target_os = "linux"))]
fn random_u32() -> io::Result<u32> {
    Err(io::Error::new(
        io::ErrorKind::Unsupported,
        "random inode generation is only implemented on Linux",
    ))
}

/// Validated geometry and derived defaults needed to write an XFS filesystem.
///
/// A plan is deterministic for a given [`FormatOptions`] value. It performs
/// validation, chooses allocation-group geometry, computes initial free-space
/// accounting, and exposes the on-disk metadata structures consumed by
/// [`image::MetadataImage`].
///
/// ```rust
/// use mkfs_xfs::{FormatOptions, FormatPlan};
///
/// let uuid = [0_u8; 16];
/// let plan = FormatPlan::new(FormatOptions::new(1 << 30, uuid))?;
///
/// assert_eq!(plan.block_size(), 4096);
/// assert_eq!(plan.data_blocks(), 262_144);
/// # Ok::<(), mkfs_xfs::Error>(())
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatPlan {
    data_blocks: u64,
    ag_blocks: u32,
    ag_count: u32,
    block_size: u32,
    sector_size: u32,
    inode_size: u32,
    dir_block_size: u32,
    imax_pct: u8,
    uuid: [u8; 16],
    label: Option<String>,
    features: Features,
    inode_generation: InodeGenerationPolicy,
    internal_log: InternalLogGeometry,
    root_inode: InodeLocation,
    initial_inode_chunk: InitialInodeChunk,
    initial_free_data_blocks: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllocationGroupHeaders {
    pub agf: ondisk::Agf,
    pub agi: ondisk::Agi,
    pub agfl: ondisk::Agfl,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BtreeRoots {
    pub bno: ondisk::ShortBtreeBlock,
    pub cnt: ondisk::ShortBtreeBlock,
    pub inobt: ondisk::ShortBtreeBlock,
    pub finobt: Option<ondisk::ShortBtreeBlock>,
    pub rmap: Option<ondisk::ShortBtreeBlock>,
    pub refcount: Option<ondisk::ShortBtreeBlock>,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct InternalLogGeometry {
    pub start_block: u64,
    pub blocks: u32,
    pub agno: u32,
    pub agbno: u32,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct InodeLocation {
    pub ino: u64,
    pub agno: u32,
    pub agino: u32,
    pub agbno: u32,
    pub offset: u32,
    pub byte_offset: u64,
}

impl FormatPlan {
    /// Total data-device blocks in filesystem block units.
    pub fn data_blocks(&self) -> u64 {
        self.data_blocks
    }

    /// Blocks per full allocation group.
    pub fn ag_blocks(&self) -> u32 {
        self.ag_blocks
    }

    /// Number of allocation groups.
    pub fn ag_count(&self) -> u32 {
        self.ag_count
    }

    /// Filesystem block size in bytes.
    pub fn block_size(&self) -> u32 {
        self.block_size
    }

    /// Filesystem sector size in bytes.
    pub fn sector_size(&self) -> u32 {
        self.sector_size
    }

    /// Inode size in bytes.
    pub fn inode_size(&self) -> u32 {
        self.inode_size
    }

    /// Directory block size in bytes.
    pub fn dir_block_size(&self) -> u32 {
        self.dir_block_size
    }

    /// Maximum inode percentage.
    pub fn imax_pct(&self) -> u8 {
        self.imax_pct
    }

    /// Filesystem UUID.
    pub fn uuid(&self) -> [u8; 16] {
        self.uuid
    }

    /// Optional filesystem label.
    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    /// Supported filesystem features selected for the plan.
    pub fn features(&self) -> Features {
        self.features
    }

    /// Inode generation-number policy selected for the plan.
    pub fn inode_generation(&self) -> InodeGenerationPolicy {
        self.inode_generation
    }

    /// Validate format options and compute derived filesystem geometry.
    ///
    /// This mirrors the default AG sizing policy in xfsprogs
    /// `libxfs/topology.c:calc_default_ag_geometry` for the implemented
    /// single-data-device path.
    pub fn new(options: FormatOptions) -> Result<Self, Error> {
        validate_power_of_two("block_size", options.block_size as u64)?;
        validate_power_of_two("sector_size", options.sector_size as u64)?;
        validate_power_of_two("inode_size", options.inode_size as u64)?;
        validate_supported_features(options.features)?;

        validate_range(
            "block_size",
            options.block_size as u64,
            XFS_MIN_BLOCKSIZE as u64,
            XFS_MAX_BLOCKSIZE as u64,
        )?;
        validate_range(
            "sector_size",
            options.sector_size as u64,
            XFS_MIN_SECTORSIZE as u64,
            XFS_MAX_SECTORSIZE as u64,
        )?;
        validate_range(
            "inode_size",
            options.inode_size as u64,
            XFS_DINODE_MIN_SIZE as u64,
            (options.block_size / XFS_MIN_INODE_PERBLOCK) as u64,
        )?;

        if options.block_size < options.sector_size {
            return Err(Error::OutOfRange {
                name: "sector_size",
                value: options.sector_size as u64,
                min: XFS_MIN_SECTORSIZE as u64,
                max: options.block_size as u64,
            });
        }
        if !options.data_bytes.is_multiple_of(options.block_size as u64) {
            return Err(Error::NotMultiple {
                name: "data_bytes",
                value: options.data_bytes,
                unit: options.block_size as u64,
            });
        }
        if let Some(label) = &options.label {
            if label.len() > ondisk::XFSLABEL_MAX {
                return Err(Error::LabelTooLong {
                    len: label.len(),
                    max: ondisk::XFSLABEL_MAX,
                });
            }
        }

        let block_log = log2_exact(options.block_size as u64) as u8;
        let data_blocks = options.data_bytes / options.block_size as u64;
        let min_blocks = ag_min_blocks(block_log);
        if data_blocks < min_blocks {
            return Err(Error::TooSmall {
                name: "data_blocks",
                value: data_blocks,
                min: min_blocks,
            });
        }

        let (ag_blocks, ag_count) =
            calc_default_ag_geometry(block_log, data_blocks, options.multidisk);
        let ag_blocks = u32::try_from(ag_blocks).map_err(|_| Error::OutOfRange {
            name: "ag_blocks",
            value: ag_blocks,
            min: 0,
            max: u64::from(u32::MAX),
        })?;
        let ag_count = u32::try_from(ag_count).map_err(|_| Error::OutOfRange {
            name: "ag_count",
            value: ag_count,
            min: 0,
            max: u64::from(u32::MAX),
        })?;
        let inode_offset_bits = log2_exact(u64::from(options.block_size / options.inode_size));
        let agino_bits = log2_roundup(u64::from(ag_blocks)) + inode_offset_bits;
        if agino_bits > u32::BITS {
            return Err(Error::OutOfRange {
                name: "agino_bits",
                value: u64::from(agino_bits),
                min: 0,
                max: u64::from(u32::BITS),
            });
        }

        let mut plan = Self {
            data_blocks,
            ag_blocks,
            ag_count,
            block_size: options.block_size,
            sector_size: options.sector_size,
            inode_size: options.inode_size,
            dir_block_size: options.block_size.max(1 << XFS_MIN_REC_DIRSIZE_LOG),
            imax_pct: calc_imax_pct(data_blocks, block_log),
            uuid: options.uuid,
            label: options.label,
            features: options.features,
            inode_generation: options.inode_generation,
            internal_log: InternalLogGeometry::default(),
            root_inode: InodeLocation::default(),
            initial_inode_chunk: InitialInodeChunk::default(),
            initial_free_data_blocks: 0,
        };
        let layout = validate_initial_metadata_layout(&plan)?;
        plan.internal_log = layout.internal_log;
        plan.root_inode = layout.root_inode;
        plan.initial_inode_chunk = layout.initial_inode_chunk;
        plan.initial_free_data_blocks = layout.initial_free_data_blocks;

        Ok(plan)
    }

    /// Build a superblock value with fields known before AG headers, root inode
    /// allocation, log placement, and free-space accounting are finished.
    pub fn initial_superblock(&self) -> ondisk::Superblock {
        let block_log = log2_exact(self.block_size as u64) as u8;
        let sector_log = log2_exact(self.sector_size as u64) as u8;
        let inode_log = log2_exact(self.inode_size as u64) as u8;
        let log = self.internal_log();
        let root_inode = self.root_inode_location().ino;
        let realtime_bitmap_inode = root_inode + 1;
        let realtime_summary_inode = root_inode + 2;
        let inode_chunk = self.initial_inode_chunk();
        let mut sb = ondisk::Superblock {
            block_size: self.block_size,
            data_blocks: self.data_blocks,
            uuid: self.uuid,
            log_start: self.agb_to_fsb(log.agno, log.agbno),
            root_inode,
            realtime_bitmap_inode,
            realtime_summary_inode,
            realtime_extent_size: 1,
            log_blocks: log.blocks,
            ag_blocks: self.ag_blocks,
            ag_count: self.ag_count,
            sector_size: self.sector_size as u16,
            inode_size: self.inode_size as u16,
            inodes_per_block: (self.block_size / self.inode_size) as u16,
            inode_count: u64::from(inode_chunk.count),
            free_inode_count: u64::from(inode_chunk.free_count),
            free_data_blocks: self.initial_free_data_blocks(),
            block_log,
            sector_log,
            inode_log,
            inodes_per_block_log: block_log - inode_log,
            ag_blocks_log: log2_roundup(self.ag_blocks as u64) as u8,
            inprogress: 0,
            imax_pct: self.imax_pct,
            dir_block_log: (log2_exact(self.dir_block_size as u64) as u8) - block_log,
            log_sector_log: 0,
            log_sector_size: 0,
            log_sunit: if self.features.log_version == 2 {
                if self.sector_size as u64 > BBSIZE {
                    self.sector_size
                } else {
                    1
                }
            } else {
                0
            },
            metadir_inode: 0,
            rg_count: 0,
            ..ondisk::Superblock::default()
        };

        if let Some(label) = &self.label {
            sb.set_label(label.as_bytes());
        }

        apply_features(self, &mut sb);
        sb
    }

    pub fn internal_log(&self) -> InternalLogGeometry {
        self.internal_log
    }

    fn compute_internal_log(&self) -> Option<InternalLogGeometry> {
        let blocks = StaticMetadataBlocks::new(self);
        let prealloc_blocks = blocks.prealloc_blocks(self.features);
        let block_log = log2_exact(self.block_size as u64) as u8;
        let log_agno = self.ag_count / 2;
        let ag_blocks = self.ag_blocks_for(log_agno);
        let max_logblocks = u64::from(ag_blocks.checked_sub(prealloc_blocks)?.checked_sub(1)?);
        let min_logblocks = self.minimum_log_blocks(block_log);

        if max_logblocks < min_logblocks {
            return None;
        }

        let mut logblocks = self.data_blocks / 2048;
        logblocks = logblocks.max(XFS_MIN_REALISTIC_LOG_BYTES >> block_log);
        if self.data_blocks < (XFS_MIN_REALISTIC_FS_BYTES >> block_log) {
            logblocks = min_logblocks;
        }
        logblocks = logblocks.max(min_logblocks).min(max_logblocks);
        logblocks = logblocks.min(XFS_MAX_LOG_BLOCKS);
        if (logblocks << block_log) > XFS_MAX_LOG_BYTES {
            logblocks = XFS_MAX_LOG_BYTES >> block_log;
        }

        let agbno = prealloc_blocks;
        Some(InternalLogGeometry {
            start_block: u64::from(log_agno) * u64::from(self.ag_blocks) + u64::from(agbno),
            blocks: logblocks as u32,
            agno: log_agno,
            agbno,
        })
    }

    /// Compute the root inode number that `xfs_ialloc_calc_rootino` would
    /// choose for the currently supported default mkfs path.
    pub fn root_inode_location(&self) -> InodeLocation {
        self.root_inode
    }

    fn compute_root_inode_location(&self, log: InternalLogGeometry) -> Option<InodeLocation> {
        let mut agbno = root_inode_start_block(self)?;
        if log.agno == 0 {
            agbno = agbno.checked_add(log.blocks)?;
        }

        let inode_align = initial_inode_align_blocks(self);
        if inode_align > 1 {
            agbno = round_up_u32(agbno, inode_align)?;
        }

        self.inode_location_from_parts(0, agbno, 0)
    }

    pub fn inode_location(&self, ino: u64) -> Option<InodeLocation> {
        let agino_bits = self.agino_bits();
        if agino_bits >= u64::BITS {
            return None;
        }
        let agino_mask = (1_u64 << agino_bits) - 1;
        let agno = u32::try_from(ino >> agino_bits).ok()?;
        let agino = u32::try_from(ino & agino_mask).ok()?;
        let offset_bits = self.inode_offset_bits();
        let offset_mask = (1_u32 << offset_bits) - 1;
        let agbno = agino >> offset_bits;
        let offset = agino & offset_mask;

        self.inode_location_from_parts(agno, agbno, offset)
    }

    pub fn inode_location_from_parts(
        &self,
        agno: u32,
        agbno: u32,
        offset: u32,
    ) -> Option<InodeLocation> {
        if agno >= self.ag_count {
            return None;
        }
        if agbno >= self.ag_blocks_for(agno) {
            return None;
        }
        if offset >= self.block_size / self.inode_size {
            return None;
        }

        let agino =
            u32::try_from((u64::from(agbno) << self.inode_offset_bits()) | u64::from(offset))
                .ok()?;
        let ino = (u64::from(agno) << self.agino_bits()) | u64::from(agino);
        let byte_offset = self.ag_base_offset(agno)?
            + u64::from(agbno) * u64::from(self.block_size)
            + u64::from(offset) * u64::from(self.inode_size);

        Some(InodeLocation {
            ino,
            agno,
            agino,
            agbno,
            offset,
            byte_offset,
        })
    }

    /// Build the initial AG headers for `agno`, matching `xfs_ag_init_headers`
    /// for the currently supported default-data-device path.
    pub fn initial_ag_headers(&self, agno: u32) -> Option<AllocationGroupHeaders> {
        if agno >= self.ag_count {
            return None;
        }

        let ag_blocks = self.ag_blocks_for(agno);
        let blocks = StaticMetadataBlocks::new(self);
        let prealloc_blocks = blocks.prealloc_blocks(self.features);
        let alloc_records = self.initial_alloc_records(agno, ag_blocks, prealloc_blocks)?;
        let (agfl_start, agfl_count) = self.initial_agfl_extent(agno, prealloc_blocks)?;
        let free_blocks = alloc_records
            .iter()
            .try_fold(0_u32, |sum, record| sum.checked_add(record.block_count))?;
        let longest = alloc_records
            .iter()
            .map(|record| record.block_count)
            .max()?;
        let inode_chunk =
            (self.initial_inode_chunk().agno == agno).then_some(self.initial_inode_chunk());
        let (inode_count, free_inode_count, newino) = match inode_chunk {
            Some(chunk) => (chunk.count, chunk.free_count, chunk.start_agino),
            None => (0, 0, ondisk::NULLAGINO),
        };
        let uuid = if self.features.crcs_enabled {
            self.uuid
        } else {
            [0; 16]
        };

        let agf = ondisk::Agf {
            seqno: agno,
            length: ag_blocks,
            bno_root: blocks.bno,
            cnt_root: blocks.cnt,
            rmap_root: if self.features.rmapbt { blocks.rmap } else { 0 },
            bno_level: 1,
            cnt_level: 1,
            rmap_level: u32::from(self.features.rmapbt),
            flfirst: 1,
            fllast: agfl_count,
            flcount: agfl_count,
            free_blocks,
            longest,
            btree_blocks: 0,
            uuid,
            rmap_blocks: u32::from(self.features.rmapbt),
            refcount_blocks: u32::from(self.features.reflink),
            refcount_root: if self.features.reflink {
                blocks.refcount
            } else {
                0
            },
            refcount_level: u32::from(self.features.reflink),
            lsn: 0,
        };

        let mut unlinked = [0_u32; 64];
        unlinked.fill(ondisk::NULLAGINO);
        let agi = ondisk::Agi {
            seqno: agno,
            length: ag_blocks,
            count: inode_count,
            root: blocks.ibt,
            level: 1,
            free_count: free_inode_count,
            newino,
            dirino: ondisk::NULLAGINO,
            unlinked,
            uuid,
            lsn: 0,
            free_root: if self.features.finobt { blocks.fibt } else { 0 },
            free_level: u32::from(self.features.finobt),
            iblocks: u32::from(self.features.inobtcnt),
            fblocks: u32::from(self.features.inobtcnt && self.features.finobt),
        };

        let mut agfl = ondisk::Agfl::new(agno, uuid, self.sector_size as usize)?;
        for idx in 0..agfl_count {
            agfl.block_numbers[(idx + 1) as usize] = agfl_start.checked_add(idx)?;
        }
        Some(AllocationGroupHeaders { agf, agi, agfl })
    }

    /// Build the initial per-AG btree root blocks written by
    /// `xfs_ag_init_headers` for the currently supported no-internal-log path.
    pub fn initial_btree_roots(&self, agno: u32) -> Option<BtreeRoots> {
        if agno >= self.ag_count {
            return None;
        }

        let ag_blocks = self.ag_blocks_for(agno);
        let blocks = StaticMetadataBlocks::new(self);
        let prealloc_blocks = blocks.prealloc_blocks(self.features);
        let alloc_records = ondisk::ShortBtreeRecords::Alloc(self.initial_alloc_records(
            agno,
            ag_blocks,
            prealloc_blocks,
        )?);
        let uuid = if self.features.crcs_enabled {
            self.uuid
        } else {
            [0; 16]
        };

        let bno = self.short_btree(
            ondisk::ShortBtreeKind::Bno,
            agno,
            blocks.bno,
            uuid,
            alloc_records.clone(),
        );
        let cnt = self.short_btree(
            ondisk::ShortBtreeKind::Cnt,
            agno,
            blocks.cnt,
            uuid,
            alloc_records,
        );
        let inobt = self.short_btree(
            ondisk::ShortBtreeKind::Inobt,
            agno,
            blocks.ibt,
            uuid,
            ondisk::ShortBtreeRecords::Inode(self.initial_inode_records(agno)),
        );
        let finobt = self.features.finobt.then(|| {
            self.short_btree(
                ondisk::ShortBtreeKind::Finobt,
                agno,
                blocks.fibt,
                uuid,
                ondisk::ShortBtreeRecords::Inode(
                    self.initial_inode_records(agno)
                        .into_iter()
                        .filter(|record| record.free_count > 0)
                        .collect(),
                ),
            )
        });
        let rmap = self.features.rmapbt.then(|| {
            self.short_btree(
                ondisk::ShortBtreeKind::Rmap,
                agno,
                blocks.rmap,
                uuid,
                ondisk::ShortBtreeRecords::Rmap(self.initial_rmap_records(agno, &blocks)),
            )
        });
        let refcount = self.features.reflink.then(|| {
            self.short_btree(
                ondisk::ShortBtreeKind::Refcount,
                agno,
                blocks.refcount,
                uuid,
                ondisk::ShortBtreeRecords::None,
            )
        });

        Some(BtreeRoots {
            bno,
            cnt,
            inobt,
            finobt,
            rmap,
            refcount,
        })
    }

    fn ag_blocks_for(&self, agno: u32) -> u32 {
        if agno + 1 == self.ag_count {
            (self.data_blocks - u64::from(agno) * u64::from(self.ag_blocks)) as u32
        } else {
            self.ag_blocks
        }
    }

    fn ag_base_offset(&self, agno: u32) -> Option<u64> {
        (u64::from(agno))
            .checked_mul(u64::from(self.ag_blocks))?
            .checked_mul(u64::from(self.block_size))
    }

    fn inode_offset_bits(&self) -> u32 {
        log2_exact(u64::from(self.block_size / self.inode_size))
    }

    fn agino_bits(&self) -> u32 {
        log2_roundup(u64::from(self.ag_blocks)) + self.inode_offset_bits()
    }

    fn short_btree(
        &self,
        kind: ondisk::ShortBtreeKind,
        agno: u32,
        agbno: u32,
        uuid: [u8; 16],
        records: ondisk::ShortBtreeRecords,
    ) -> ondisk::ShortBtreeBlock {
        ondisk::ShortBtreeBlock::new_leaf(kind, self.agb_to_daddr(agno, agbno), agno, uuid, records)
    }

    fn initial_rmap_records(
        &self,
        agno: u32,
        blocks: &StaticMetadataBlocks,
    ) -> Vec<ondisk::RmapRecord> {
        let mut records = vec![
            ondisk::RmapRecord {
                start_block: 0,
                block_count: blocks.bno,
                owner: ondisk::XFS_RMAP_OWN_FS,
                offset: 0,
            },
            ondisk::RmapRecord {
                start_block: blocks.bno,
                block_count: 2,
                owner: ondisk::XFS_RMAP_OWN_AG,
                offset: 0,
            },
            ondisk::RmapRecord {
                start_block: blocks.ibt,
                block_count: blocks.rmap - blocks.ibt,
                owner: ondisk::XFS_RMAP_OWN_INOBT,
                offset: 0,
            },
            ondisk::RmapRecord {
                start_block: blocks.rmap,
                block_count: 1,
                owner: ondisk::XFS_RMAP_OWN_AG,
                offset: 0,
            },
        ];

        if self.features.reflink {
            records.push(ondisk::RmapRecord {
                start_block: blocks.refcount,
                block_count: 1,
                owner: ondisk::XFS_RMAP_OWN_REFC,
                offset: 0,
            });
        }

        let log = self.internal_log();
        if log.agno == agno {
            records.push(ondisk::RmapRecord {
                start_block: log.agbno,
                block_count: log.blocks,
                owner: ondisk::XFS_RMAP_OWN_LOG,
                offset: 0,
            });
        }

        let prealloc_blocks = blocks.prealloc_blocks(self.features);
        if let Some((agfl_start, agfl_count)) = self.initial_agfl_extent(agno, prealloc_blocks) {
            records.push(ondisk::RmapRecord {
                start_block: agfl_start,
                block_count: agfl_count,
                owner: ondisk::XFS_RMAP_OWN_AG,
                offset: 0,
            });
        }

        let chunk = self.initial_inode_chunk();
        if chunk.agno == agno {
            records.push(ondisk::RmapRecord {
                start_block: chunk.agbno,
                block_count: chunk.block_count,
                owner: ondisk::XFS_RMAP_OWN_INODES,
                offset: 0,
            });
        }

        records.sort_by_key(|record| record.start_block);
        records
    }

    fn initial_inode_records(&self, agno: u32) -> Vec<ondisk::InodeRecord> {
        let chunk = self.initial_inode_chunk();
        if chunk.agno != agno {
            return Vec::new();
        }
        vec![ondisk::InodeRecord {
            start_inode: chunk.start_agino,
            holemask: 0,
            count: chunk.count as u8,
            free_count: chunk.free_count as u8,
            free: chunk.free_mask,
        }]
    }

    fn initial_alloc_records(
        &self,
        agno: u32,
        ag_blocks: u32,
        prealloc_blocks: u32,
    ) -> Option<Vec<ondisk::AllocRecord>> {
        let mut records = Vec::new();
        let mut start = prealloc_blocks;
        let mut reserved = Vec::new();

        let log = self.internal_log();
        if log.agno == agno {
            reserved.push((log.agbno, log.blocks));
        }
        if let Some((agfl_start, agfl_count)) = self.initial_agfl_extent(agno, prealloc_blocks) {
            reserved.push((agfl_start, agfl_count));
        }
        let chunk = self.initial_inode_chunk();
        if chunk.agno == agno {
            reserved.push((chunk.agbno, chunk.block_count));
        }
        reserved.sort_by_key(|&(agbno, _)| agbno);

        for (agbno, block_count) in reserved {
            if agbno > start {
                records.push(ondisk::AllocRecord {
                    start_block: start,
                    block_count: agbno - start,
                });
            }
            start = agbno.checked_add(block_count)?;
        }

        if start < ag_blocks {
            records.push(ondisk::AllocRecord {
                start_block: start,
                block_count: ag_blocks - start,
            });
        }

        Some(records)
    }

    fn initial_agfl_extent(&self, agno: u32, prealloc_blocks: u32) -> Option<(u32, u32)> {
        self.compute_initial_agfl_extent(agno, prealloc_blocks, self.internal_log())
    }

    fn compute_initial_agfl_extent(
        &self,
        agno: u32,
        prealloc_blocks: u32,
        log: InternalLogGeometry,
    ) -> Option<(u32, u32)> {
        let count = min_agfl_blocks(self.features);
        let mut start = prealloc_blocks;
        if log.agno == agno {
            start = log.agbno.checked_add(log.blocks)?;
        }
        Some((start, count))
    }

    fn initial_inode_chunk(&self) -> InitialInodeChunk {
        self.initial_inode_chunk
    }

    fn compute_initial_inode_chunk(&self, root: InodeLocation) -> Option<InitialInodeChunk> {
        let block_count = inode_chunk_blocks(self)?;
        Some(InitialInodeChunk {
            agno: root.agno,
            start_agino: root.agino,
            agbno: root.agbno,
            block_count,
            count: ondisk::XFS_INODES_PER_CHUNK,
            free_count: ondisk::XFS_INODES_PER_CHUNK - 3,
            free_mask: ondisk::XFS_INOBT_ALL_FREE & !0b111,
        })
    }

    pub(crate) fn initial_inode_chunk_blocks(&self) -> u32 {
        self.initial_inode_chunk.block_count
    }

    fn initial_free_data_blocks(&self) -> u64 {
        self.initial_free_data_blocks
    }

    fn compute_initial_free_data_blocks(
        &self,
        log: InternalLogGeometry,
        chunk: InitialInodeChunk,
    ) -> Option<u64> {
        let blocks = StaticMetadataBlocks::new(self);
        let prealloc_blocks = blocks.prealloc_blocks(self.features);
        let mut free = 0_u64;
        for agno in 0..self.ag_count {
            free = free.checked_add(u64::from(
                self.ag_blocks_for(agno).checked_sub(prealloc_blocks)?,
            ))?;
        }
        free = free.checked_sub(u64::from(log.blocks))?;
        free = free.checked_sub(u64::from(chunk.block_count))?;
        Some(free)
    }

    fn minimum_log_blocks(&self, block_log: u8) -> u64 {
        let mut min_logblocks = XFS_MIN_LOG_BLOCKS;
        if self.data_blocks >= (1_u64 << 30) >> block_log {
            min_logblocks = min_logblocks.max(XFS_MIN_LOG_BYTES >> block_log);
        }
        min_logblocks
    }

    fn agb_to_daddr(&self, agno: u32, agbno: u32) -> u64 {
        (u64::from(agno) * u64::from(self.ag_blocks) + u64::from(agbno))
            << block_basic_block_log(self)
    }

    fn agb_to_fsb(&self, agno: u32, agbno: u32) -> u64 {
        (u64::from(agno) << log2_roundup(u64::from(self.ag_blocks))) | u64::from(agbno)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct StaticMetadataBlocks {
    bno: u32,
    cnt: u32,
    ibt: u32,
    fibt: u32,
    rmap: u32,
    refcount: u32,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct InitialInodeChunk {
    agno: u32,
    start_agino: u32,
    agbno: u32,
    block_count: u32,
    count: u32,
    free_count: u32,
    free_mask: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct InitialMetadataLayout {
    internal_log: InternalLogGeometry,
    root_inode: InodeLocation,
    initial_inode_chunk: InitialInodeChunk,
    initial_free_data_blocks: u64,
}

impl StaticMetadataBlocks {
    fn new(plan: &FormatPlan) -> Self {
        let agfl = hdr_block(plan, 3_u64 << sector_basic_block_log(plan));
        let bno = agfl + 1;
        let cnt = bno + 1;
        let ibt = cnt + 1;
        let fibt = ibt + 1;
        let rmap = if plan.features.finobt {
            fibt + 1
        } else {
            ibt + 1
        };
        let refcount = if plan.features.rmapbt {
            rmap + 1
        } else if plan.features.finobt {
            fibt + 1
        } else {
            ibt + 1
        };

        Self {
            bno,
            cnt,
            ibt,
            fibt,
            rmap,
            refcount,
        }
    }

    fn prealloc_blocks(&self, features: Features) -> u32 {
        if features.reflink {
            self.refcount + 1
        } else if features.rmapbt {
            self.rmap + 1
        } else if features.finobt {
            self.fibt + 1
        } else {
            self.ibt + 1
        }
    }
}

fn root_inode_start_block(plan: &FormatPlan) -> Option<u32> {
    let headers = div_round_up(4 * u64::from(plan.sector_size), u64::from(plan.block_size));
    let mut agbno = u32::try_from(headers).ok()?;

    agbno = agbno.checked_add(2)?; // by-block and by-size free space btree roots
    agbno = agbno.checked_add(1)?; // inode btree root
    agbno = agbno.checked_add(min_agfl_blocks(plan.features))?;
    if plan.features.finobt {
        agbno = agbno.checked_add(1)?;
    }
    if plan.features.rmapbt {
        agbno = agbno.checked_add(1)?;
    }
    if plan.features.reflink {
        agbno = agbno.checked_add(1)?;
    }

    Some(agbno)
}

fn inode_chunk_blocks(plan: &FormatPlan) -> Option<u32> {
    let bytes = ondisk::XFS_INODES_PER_CHUNK.checked_mul(plan.inode_size)?;
    Some(bytes / plan.block_size)
}

fn min_agfl_blocks(features: Features) -> u32 {
    // This mirrors xfs_alloc_min_freelist for the initial mkfs state, where
    // the by-block, by-size, and rmap btrees all start at level 1.
    let alloc_roots = 2;
    let rmap_roots = u32::from(features.rmapbt);
    (alloc_roots + rmap_roots) * 2
}

fn initial_inode_align_blocks(plan: &FormatPlan) -> u32 {
    if !plan.features.inode_align {
        return 0;
    }

    let block_log = log2_exact(u64::from(plan.block_size));
    let mut align = if plan.features.crcs_enabled {
        (ondisk::XFS_INODE_BIG_CLUSTER_SIZE * (plan.inode_size / XFS_DINODE_MIN_SIZE)) >> block_log
    } else {
        ondisk::XFS_INODE_BIG_CLUSTER_SIZE >> block_log
    };
    if plan.features.crcs_enabled && plan.features.spinodes {
        align = (ondisk::XFS_INODES_PER_CHUNK * plan.inode_size) >> block_log;
    }
    align
}

fn apply_features(plan: &FormatPlan, sb: &mut ondisk::Superblock) {
    let fp = plan.features;

    sb.version = ondisk::XFS_DFL_SB_VERSION_BITS;
    if fp.crcs_enabled {
        sb.version |= ondisk::XFS_SB_VERSION_5;
    } else {
        sb.version |= ondisk::XFS_SB_VERSION_4;
    }

    if fp.inode_align {
        sb.version |= ondisk::XFS_SB_VERSION_ALIGNBIT;
        sb.inode_align = if fp.crcs_enabled {
            (ondisk::XFS_INODE_BIG_CLUSTER_SIZE * (plan.inode_size / XFS_DINODE_MIN_SIZE))
                >> sb.block_log
        } else {
            ondisk::XFS_INODE_BIG_CLUSTER_SIZE >> sb.block_log
        };
    }

    if fp.log_version == 2 {
        sb.version |= ondisk::XFS_SB_VERSION_LOGV2BIT;
    }
    if fp.attr_version == 1 {
        sb.version |= ondisk::XFS_SB_VERSION_ATTRBIT;
    }
    if plan.sector_size as u64 > BBSIZE {
        sb.version |= ondisk::XFS_SB_VERSION_SECTORBIT;
        sb.log_sector_log = sb.sector_log;
        sb.log_sector_size = plan.sector_size as u16;
    }

    if fp.lazy_sb_counters {
        sb.features2 |= ondisk::XFS_SB_VERSION2_LAZYSBCOUNTBIT;
    }
    if fp.projid32bit {
        sb.features2 |= ondisk::XFS_SB_VERSION2_PROJID32BIT;
    }
    if fp.crcs_enabled {
        sb.features2 |= ondisk::XFS_SB_VERSION2_CRCBIT;
    }
    if fp.attr_version == 2 {
        sb.features2 |= ondisk::XFS_SB_VERSION2_ATTR2BIT;
    }
    if fp.dirftype && !fp.crcs_enabled {
        sb.features2 |= ondisk::XFS_SB_VERSION2_FTYPE;
    }
    if sb.features2 != 0 {
        sb.version |= ondisk::XFS_SB_VERSION_MOREBITSBIT;
    }
    sb.bad_features2 = sb.features2;

    if fp.crcs_enabled {
        sb.features_incompat = ondisk::XFS_SB_FEAT_INCOMPAT_FTYPE;
        if fp.finobt {
            sb.features_ro_compat |= ondisk::XFS_SB_FEAT_RO_COMPAT_FINOBT;
        }
        if fp.rmapbt {
            sb.features_ro_compat |= ondisk::XFS_SB_FEAT_RO_COMPAT_RMAPBT;
        }
        if fp.reflink {
            sb.features_ro_compat |= ondisk::XFS_SB_FEAT_RO_COMPAT_REFLINK;
        }
        if fp.inobtcnt {
            sb.features_ro_compat |= ondisk::XFS_SB_FEAT_RO_COMPAT_INOBTCNT;
        }
        if fp.bigtime {
            sb.features_incompat |= ondisk::XFS_SB_FEAT_INCOMPAT_BIGTIME;
        }
        if fp.spinodes {
            sb.spino_align = sb.inode_align;
            sb.inode_align = (ondisk::XFS_INODES_PER_CHUNK * plan.inode_size) >> sb.block_log;
            sb.features_incompat |= ondisk::XFS_SB_FEAT_INCOMPAT_SPINODES;
        }
        if fp.nrext64 {
            sb.features_incompat |= ondisk::XFS_SB_FEAT_INCOMPAT_NREXT64;
        }
        if fp.exchrange {
            sb.features_incompat |= ondisk::XFS_SB_FEAT_INCOMPAT_EXCHRANGE;
        }
        if fp.parent_pointers {
            sb.features_incompat |= ondisk::XFS_SB_FEAT_INCOMPAT_PARENT;
            sb.version |= ondisk::XFS_SB_VERSION_ATTRBIT;
        }
    }
}

fn calc_default_ag_geometry(block_log: u8, data_blocks: u64, multidisk: bool) -> (u64, u64) {
    let blocks;
    let shift;

    if data_blocks >= tebibytes(32, block_log)
        || (!multidisk && data_blocks >= tebibytes(4, block_log))
    {
        blocks = ag_max_blocks(block_log);
    } else {
        if !multidisk && data_blocks >= mebibytes(128, block_log) {
            shift = XFS_NOMULTIDISK_AGLOG;
        } else {
            let mut s = XFS_MULTIDISK_AGLOG;
            if data_blocks <= gibibytes(512, block_log) {
                s -= 1;
            }
            if data_blocks <= gibibytes(8, block_log) {
                s -= 1;
            }
            if data_blocks < mebibytes(128, block_log) {
                s -= 1;
            }
            if data_blocks < mebibytes(64, block_log) {
                s -= 1;
            }
            if data_blocks < mebibytes(32, block_log) {
                s -= 1;
            }
            shift = s;
        }

        blocks = rounded_blocks_for_shift(block_log, data_blocks, shift);
    }

    let count = div_round_up(data_blocks, blocks);
    (blocks, count)
}

fn rounded_blocks_for_shift(block_log: u8, data_blocks: u64, shift: u8) -> u64 {
    let mut blocks = data_blocks >> shift;
    if shift > 0 && (data_blocks & ((1_u64 << shift) - 1)) != 0 && blocks < ag_max_blocks(block_log)
    {
        blocks += 1;
    }
    blocks
}

fn calc_imax_pct(data_blocks: u64, block_log: u8) -> u8 {
    if data_blocks < tebibytes(1, block_log) {
        XFS_DFL_IMAXIMUM_PCT
    } else if data_blocks < tebibytes(50, block_log) {
        5
    } else {
        1
    }
}

fn ag_min_blocks(block_log: u8) -> u64 {
    XFS_AG_MIN_BYTES >> block_log
}

fn ag_max_blocks(block_log: u8) -> u64 {
    (XFS_AG_MAX_BYTES - 1) >> block_log
}

fn mebibytes(count: u64, block_log: u8) -> u64 {
    count << (20 - block_log)
}

fn gibibytes(count: u64, block_log: u8) -> u64 {
    count << (30 - block_log)
}

fn tebibytes(count: u64, block_log: u8) -> u64 {
    count << (40 - block_log)
}

fn div_round_up(n: u64, d: u64) -> u64 {
    n.div_ceil(d)
}

fn round_up_u32(value: u32, unit: u32) -> Option<u32> {
    if unit == 0 {
        return None;
    }
    let rem = value % unit;
    if rem == 0 {
        Some(value)
    } else {
        value.checked_add(unit - rem)
    }
}

fn sector_basic_block_log(plan: &FormatPlan) -> u8 {
    (log2_exact(plan.sector_size as u64) as u8) - BBSHIFT
}

fn block_basic_block_log(plan: &FormatPlan) -> u8 {
    (log2_exact(plan.block_size as u64) as u8) - BBSHIFT
}

fn hdr_block(plan: &FormatPlan, daddr: u64) -> u32 {
    (daddr >> block_basic_block_log(plan)) as u32
}

fn log2_exact(value: u64) -> u32 {
    value.trailing_zeros()
}

fn log2_roundup(value: u64) -> u32 {
    u64::BITS - (value - 1).leading_zeros()
}

fn validate_power_of_two(name: &'static str, value: u64) -> Result<(), Error> {
    if value.is_power_of_two() {
        Ok(())
    } else {
        Err(Error::InvalidPowerOfTwo { name, value })
    }
}

fn validate_range(name: &'static str, value: u64, min: u64, max: u64) -> Result<(), Error> {
    if value >= min && value <= max {
        Ok(())
    } else {
        Err(Error::OutOfRange {
            name,
            value,
            min,
            max,
        })
    }
}

fn validate_supported_features(features: Features) -> Result<(), Error> {
    validate_feature(
        "log_version",
        features.log_version == 2,
        "only log version 2 is supported",
    )?;
    validate_feature(
        "attr_version",
        features.attr_version == 2,
        "only attr version 2 is supported",
    )?;
    validate_feature(
        "inode_align",
        features.inode_align,
        "inode alignment must be enabled",
    )?;
    validate_feature(
        "lazy_sb_counters",
        features.lazy_sb_counters,
        "lazy superblock counters must be enabled",
    )?;
    validate_feature(
        "projid32bit",
        features.projid32bit,
        "32-bit project ids must be enabled",
    )?;
    validate_feature(
        "crcs_enabled",
        features.crcs_enabled,
        "only CRC-enabled v5 metadata is supported",
    )?;
    validate_feature(
        "dirftype",
        features.dirftype,
        "directory file types must be enabled",
    )?;
    validate_feature(
        "finobt",
        features.finobt,
        "free inode btree must be enabled",
    )?;
    validate_feature(
        "spinodes",
        features.spinodes,
        "sparse inode chunks must be enabled",
    )?;
    validate_feature(
        "rmapbt",
        features.rmapbt,
        "reverse mapping btree must be enabled",
    )?;
    validate_feature("reflink", features.reflink, "reflink must be enabled")?;
    validate_feature(
        "inobtcnt",
        features.inobtcnt,
        "inode btree counters must be enabled",
    )?;
    validate_feature(
        "parent_pointers",
        features.parent_pointers,
        "parent pointers must be enabled",
    )?;
    validate_feature("bigtime", features.bigtime, "bigtime must be enabled")?;
    validate_feature(
        "nrext64",
        features.nrext64,
        "64-bit extent counters must be enabled",
    )?;
    validate_feature(
        "exchrange",
        features.exchrange,
        "exchange-range support must be enabled",
    )
}

fn validate_initial_metadata_layout(plan: &FormatPlan) -> Result<InitialMetadataLayout, Error> {
    let blocks = StaticMetadataBlocks::new(plan);
    let prealloc_blocks = blocks.prealloc_blocks(plan.features);

    validate_extent_within_ag(plan, 0, 0, prealloc_blocks, "static_metadata_blocks")?;

    let log = plan.compute_internal_log().ok_or(Error::TooSmall {
        name: "internal_log_ag_blocks",
        value: u64::from(plan.ag_blocks_for(plan.ag_count / 2)),
        min: u64::from(prealloc_blocks) + XFS_MIN_LOG_BLOCKS + 1,
    })?;
    validate_extent_within_ag(plan, log.agno, log.agbno, log.blocks, "internal_log_blocks")?;

    let root = plan
        .compute_root_inode_location(log)
        .ok_or(Error::OutOfRange {
            name: "root_inode_location",
            value: 0,
            min: 1,
            max: u64::from(u32::MAX),
        })?;
    root.ino.checked_add(2).ok_or(Error::OutOfRange {
        name: "root_realtime_inodes",
        value: root.ino,
        min: 0,
        max: u64::MAX - 2,
    })?;
    let chunk = plan
        .compute_initial_inode_chunk(root)
        .ok_or(Error::OutOfRange {
            name: "initial_inode_chunk",
            value: 0,
            min: 1,
            max: u64::from(u32::MAX),
        })?;
    validate_extent_within_ag(
        plan,
        chunk.agno,
        chunk.agbno,
        chunk.block_count,
        "initial_inode_chunk_blocks",
    )?;
    if root.agno != chunk.agno || root.agbno != chunk.agbno {
        return Err(Error::OutOfRange {
            name: "root_inode_chunk",
            value: u64::from(root.agbno),
            min: u64::from(chunk.agbno),
            max: u64::from(chunk.agbno),
        });
    }

    for agno in 0..plan.ag_count {
        let ag_blocks = plan.ag_blocks_for(agno);
        if ag_blocks < prealloc_blocks {
            return Err(Error::TooSmall {
                name: "ag_blocks",
                value: u64::from(ag_blocks),
                min: u64::from(prealloc_blocks),
            });
        }

        let (agfl_start, agfl_count) = plan
            .compute_initial_agfl_extent(agno, prealloc_blocks, log)
            .ok_or(Error::OutOfRange {
                name: "agfl_extent",
                value: u64::from(agno),
                min: 0,
                max: u64::from(plan.ag_count.saturating_sub(1)),
            })?;
        validate_extent_within_ag(plan, agno, agfl_start, agfl_count, "agfl_blocks")?;
    }

    let initial_free_data_blocks =
        plan.compute_initial_free_data_blocks(log, chunk)
            .ok_or(Error::OutOfRange {
                name: "initial_free_data_blocks",
                value: 0,
                min: 1,
                max: u64::MAX,
            })?;

    Ok(InitialMetadataLayout {
        internal_log: log,
        root_inode: root,
        initial_inode_chunk: chunk,
        initial_free_data_blocks,
    })
}

fn validate_extent_within_ag(
    plan: &FormatPlan,
    agno: u32,
    start: u32,
    blocks: u32,
    name: &'static str,
) -> Result<(), Error> {
    let end = start.checked_add(blocks).ok_or(Error::OutOfRange {
        name,
        value: u64::from(start) + u64::from(blocks),
        min: 0,
        max: u64::from(u32::MAX),
    })?;
    let ag_blocks = plan.ag_blocks_for(agno);
    if end <= ag_blocks {
        Ok(())
    } else {
        Err(Error::OutOfRange {
            name,
            value: u64::from(end),
            min: 0,
            max: u64::from(ag_blocks),
        })
    }
}

fn validate_feature(
    name: &'static str,
    supported: bool,
    reason: &'static str,
) -> Result<(), Error> {
    if supported {
        Ok(())
    } else {
        Err(Error::UnsupportedFeature { name, reason })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const UUID: [u8; 16] = [
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
        0xff,
    ];

    #[test]
    fn defaults_match_current_xfsprogs_policy_for_1g_single_disk() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();

        assert_eq!(plan.block_size, 4096);
        assert_eq!(plan.sector_size, 512);
        assert_eq!(plan.inode_size, 512);
        assert_eq!(plan.data_blocks, 262_144);
        assert_eq!(plan.ag_blocks, 65_536);
        assert_eq!(plan.ag_count, 4);
        assert_eq!(plan.imax_pct, 25);
    }

    #[test]
    fn rejects_non_crc_feature_set() {
        let mut options = FormatOptions::new(1 << 30, UUID);
        options.features.crcs_enabled = false;

        assert_eq!(
            FormatPlan::new(options),
            Err(Error::UnsupportedFeature {
                name: "crcs_enabled",
                reason: "only CRC-enabled v5 metadata is supported",
            })
        );
    }

    #[test]
    fn rejects_unsupported_feature_versions() {
        let mut options = FormatOptions::new(1 << 30, UUID);
        options.features.log_version = 1;

        assert_eq!(
            FormatPlan::new(options),
            Err(Error::UnsupportedFeature {
                name: "log_version",
                reason: "only log version 2 is supported",
            })
        );
    }

    #[test]
    fn format_options_for_regular_file_uses_file_size_and_default_sector_size() {
        let mut path = std::env::temp_dir();
        path.push(format!("mkfs-xfs-regular-file-{}", std::process::id()));

        {
            let file = std::fs::File::create(&path).unwrap();
            file.set_len(1 << 30).unwrap();
        }

        let options = FormatOptions::from_target(&path, UUID).unwrap();
        std::fs::remove_file(&path).unwrap();

        assert_eq!(options.data_bytes, 1 << 30);
        assert_eq!(options.sector_size, 512);
    }

    #[test]
    fn four_k_sector_size_sets_sector_superblock_fields() {
        let plan =
            FormatPlan::new(FormatOptions::new(1 << 30, UUID).with_sector_size(4096)).unwrap();
        let sb = plan.initial_superblock();

        assert_eq!(plan.sector_size, 4096);
        assert_eq!(sb.sector_size, 4096);
        assert_eq!(sb.sector_log, 12);
        assert_eq!(sb.log_sector_log, 12);
        assert_eq!(sb.log_sector_size, 4096);
        assert_eq!(sb.log_sunit, 4096);
        assert_ne!(sb.version & ondisk::XFS_SB_VERSION_SECTORBIT, 0);
    }

    #[test]
    fn very_large_single_disk_uses_max_ag_size() {
        let data_bytes = ag_max_blocks(12) * 4 * (1 << 12);
        let plan = FormatPlan::new(FormatOptions::new(data_bytes, UUID)).unwrap();

        assert_eq!(plan.ag_blocks as u64, ag_max_blocks(12));
    }

    #[test]
    fn rejects_filesystem_smaller_than_minimum_allocation_group() {
        let err = FormatPlan::new(FormatOptions::new((16 << 20) - 4096, UUID)).unwrap_err();

        assert_eq!(
            err,
            Error::TooSmall {
                name: "data_blocks",
                value: 4095,
                min: 4096,
            }
        );
    }

    #[test]
    fn rejects_tiny_tail_allocation_group() {
        let err = FormatPlan::new(FormatOptions::new(4 << 40, UUID)).unwrap_err();

        assert_eq!(
            err,
            Error::TooSmall {
                name: "ag_blocks",
                value: 4,
                min: 7,
            }
        );
    }

    #[test]
    fn rejects_non_block_multiple_size() {
        let err = FormatPlan::new(FormatOptions::new((1 << 30) + 1, UUID)).unwrap_err();

        assert!(matches!(
            err,
            Error::NotMultiple {
                name: "data_bytes",
                ..
            }
        ));
    }

    #[test]
    fn initial_superblock_contains_default_feature_bits() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let sb = plan.initial_superblock();

        assert_eq!(sb.root_inode, 128);
        assert_eq!(sb.realtime_bitmap_inode, 129);
        assert_eq!(sb.realtime_summary_inode, 130);
        assert_eq!(sb.inode_count, 64);
        assert_eq!(sb.free_inode_count, 61);
        assert_eq!(sb.free_data_blocks, 245_724);
        assert_eq!(sb.inprogress, 0);
        assert_eq!(sb.log_start, 131_079);
        assert_eq!(sb.realtime_extent_size, 1);
        assert_eq!(sb.log_blocks, 16_384);
        assert_eq!(sb.version, 0xb4b5);
        assert_eq!(sb.features2, 0x18a);
        assert_eq!(sb.features_ro_compat, 0xf);
        assert_eq!(sb.features_incompat, 0xeb);
        assert_eq!(sb.inode_align, 8);
        assert_eq!(sb.spino_align, 4);
        assert_eq!(sb.metadir_inode, 0);
        assert_eq!(sb.rg_count, 0);
    }

    #[test]
    fn inode_generation_policy_matches_xfsprogs_deterministic_seed() {
        assert_eq!(
            InodeGenerationPolicy::XfsprogsDeterministicSeed.deterministic_generation(),
            Some(0x5345_4544)
        );
        assert_eq!(
            InodeGenerationPolicy::Zero.deterministic_generation(),
            Some(0)
        );
        assert_eq!(
            InodeGenerationPolicy::Fixed(0xdead_beef).deterministic_generation(),
            Some(0xdead_beef)
        );
        assert_eq!(
            InodeGenerationPolicy::Random.deterministic_generation(),
            None
        );
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn random_inode_generation_policy_returns_a_value() {
        assert!(InodeGenerationPolicy::Random.generation().is_ok());
    }

    #[test]
    fn initial_ag_headers_match_default_static_metadata_layout() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let headers = plan.initial_ag_headers(0).unwrap();

        assert_eq!(headers.agf.seqno, 0);
        assert_eq!(headers.agf.length, 65_536);
        assert_eq!(headers.agf.bno_root, 1);
        assert_eq!(headers.agf.cnt_root, 2);
        assert_eq!(headers.agi.root, 3);
        assert_eq!(headers.agi.free_root, 4);
        assert_eq!(headers.agf.rmap_root, 5);
        assert_eq!(headers.agf.refcount_root, 6);
        assert_eq!(headers.agf.fllast, 6);
        assert_eq!(headers.agf.flcount, 6);
        assert_eq!(headers.agf.free_blocks, 65_515);
        assert_eq!(headers.agf.longest, 65_512);
        assert_eq!(headers.agf.rmap_blocks, 1);
        assert_eq!(headers.agf.refcount_blocks, 1);
        assert_eq!(headers.agi.count, 64);
        assert_eq!(headers.agi.free_count, 61);
        assert_eq!(headers.agi.newino, 128);
        assert_eq!(headers.agi.iblocks, 1);
        assert_eq!(headers.agi.fblocks, 1);
        assert_eq!(headers.agfl.block_numbers.len(), 119);
        assert_eq!(headers.agfl.block_numbers[0], ondisk::NULLAGBLOCK);
        assert_eq!(&headers.agfl.block_numbers[1..7], &[7, 8, 9, 10, 11, 12]);
        assert!(headers.agfl.block_numbers[7..]
            .iter()
            .all(|&bno| bno == ondisk::NULLAGBLOCK));
    }

    #[test]
    fn default_internal_log_sits_after_static_metadata_in_middle_ag() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let log = plan.internal_log();

        assert_eq!(
            log,
            InternalLogGeometry {
                start_block: 131_079,
                blocks: 16_384,
                agno: 2,
                agbno: 7,
            }
        );

        let headers = plan.initial_ag_headers(log.agno).unwrap();
        assert_eq!(headers.agf.free_blocks, 49_139);
        assert_eq!(headers.agf.longest, 49_139);
        assert_eq!(
            &headers.agfl.block_numbers[1..7],
            &[16_391, 16_392, 16_393, 16_394, 16_395, 16_396]
        );
    }

    #[test]
    fn root_inode_location_matches_default_mkfs_geometry() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let root = plan.root_inode_location();

        assert_eq!(
            root,
            InodeLocation {
                ino: 128,
                agno: 0,
                agino: 128,
                agbno: 16,
                offset: 0,
                byte_offset: 65_536,
            }
        );
        assert_eq!(plan.inode_location(root.ino), Some(root));
        assert_eq!(plan.inode_location_from_parts(0, 16, 0), Some(root));
    }

    #[test]
    fn inode_location_converts_across_allocation_groups() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let loc = plan.inode_location_from_parts(2, 16, 3).unwrap();

        assert_eq!(loc.ino, (2 << 19) | 131);
        assert_eq!(loc.agno, 2);
        assert_eq!(loc.agino, 131);
        assert_eq!(loc.agbno, 16);
        assert_eq!(loc.offset, 3);
        assert_eq!(loc.byte_offset, 536_936_448 + 1_536);
        assert_eq!(plan.inode_location(loc.ino), Some(loc));
        assert!(plan.inode_location_from_parts(4, 16, 0).is_none());
        assert!(plan.inode_location_from_parts(0, 16, 8).is_none());
    }

    #[test]
    fn last_ag_header_uses_tail_length() {
        let options = FormatOptions::new((1 << 30) + 4096, UUID);
        let plan = FormatPlan::new(options).unwrap();
        let headers = plan.initial_ag_headers(plan.ag_count - 1).unwrap();
        let expected = plan.data_blocks - u64::from(plan.ag_count - 1) * u64::from(plan.ag_blocks);
        let sb = plan.initial_superblock();

        assert_eq!(headers.agf.length as u64, expected);
        assert!(headers.agf.length <= plan.ag_blocks);
        assert_eq!(plan.internal_log().start_block, 131_081);
        assert_eq!(sb.log_start, 262_151);
    }

    #[test]
    fn initial_btree_roots_match_default_static_metadata_layout() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let roots = plan.initial_btree_roots(0).unwrap();

        assert_eq!(roots.bno.kind, ondisk::ShortBtreeKind::Bno);
        assert_eq!(roots.bno.daddr, 8);
        assert_eq!(roots.cnt.daddr, 16);
        assert_eq!(roots.inobt.daddr, 24);
        assert_eq!(roots.finobt.as_ref().unwrap().daddr, 32);
        assert_eq!(roots.rmap.as_ref().unwrap().daddr, 40);
        assert_eq!(roots.refcount.as_ref().unwrap().daddr, 48);

        match &roots.bno.records {
            ondisk::ShortBtreeRecords::Alloc(records) => {
                assert_eq!(records.len(), 2);
                assert_eq!(records[0].start_block, 13);
                assert_eq!(records[0].block_count, 3);
                assert_eq!(records[1].start_block, 24);
                assert_eq!(records[1].block_count, 65_512);
            }
            other => panic!("unexpected bno records: {other:?}"),
        }

        match &roots.inobt.records {
            ondisk::ShortBtreeRecords::Inode(records) => {
                assert_eq!(records.len(), 1);
                assert_eq!(records[0].start_inode, 128);
                assert_eq!(records[0].holemask, 0);
                assert_eq!(records[0].count, 64);
                assert_eq!(records[0].free_count, 61);
                assert_eq!(records[0].free, ondisk::XFS_INOBT_ALL_FREE & !0b111);
            }
            other => panic!("unexpected inobt records: {other:?}"),
        }

        match &roots.finobt.as_ref().unwrap().records {
            ondisk::ShortBtreeRecords::Inode(records) => {
                assert_eq!(records.len(), 1);
                assert_eq!(records[0].start_inode, 128);
                assert_eq!(records[0].free_count, 61);
            }
            other => panic!("unexpected finobt records: {other:?}"),
        }

        match &roots.rmap.as_ref().unwrap().records {
            ondisk::ShortBtreeRecords::Rmap(records) => {
                assert_eq!(records.len(), 7);
                assert_eq!(records[0].start_block, 0);
                assert_eq!(records[0].block_count, 1);
                assert_eq!(records[0].owner, ondisk::XFS_RMAP_OWN_FS);
                assert_eq!(records[2].start_block, 3);
                assert_eq!(records[2].block_count, 2);
                assert_eq!(records[2].owner, ondisk::XFS_RMAP_OWN_INOBT);
                assert_eq!(records[4].start_block, 6);
                assert_eq!(records[4].owner, ondisk::XFS_RMAP_OWN_REFC);
                assert_eq!(records[5].start_block, 7);
                assert_eq!(records[5].block_count, 6);
                assert_eq!(records[5].owner, ondisk::XFS_RMAP_OWN_AG);
                assert_eq!(records[6].start_block, 16);
                assert_eq!(records[6].block_count, 8);
                assert_eq!(records[6].owner, ondisk::XFS_RMAP_OWN_INODES);
            }
            other => panic!("unexpected rmap records: {other:?}"),
        }
    }

    #[test]
    fn log_ag_btrees_exclude_internal_log_blocks() {
        let plan = FormatPlan::new(FormatOptions::new(1 << 30, UUID)).unwrap();
        let roots = plan.initial_btree_roots(2).unwrap();

        match &roots.bno.records {
            ondisk::ShortBtreeRecords::Alloc(records) => {
                assert_eq!(records.len(), 1);
                assert_eq!(records[0].start_block, 16_397);
                assert_eq!(records[0].block_count, 49_139);
            }
            other => panic!("unexpected bno records: {other:?}"),
        }

        match &roots.rmap.as_ref().unwrap().records {
            ondisk::ShortBtreeRecords::Rmap(records) => {
                let log = records
                    .iter()
                    .find(|record| record.owner == ondisk::XFS_RMAP_OWN_LOG)
                    .unwrap();
                assert_eq!(log.start_block, 7);
                assert_eq!(log.block_count, 16_384);
                let agfl = records.last().unwrap();
                assert_eq!(agfl.start_block, 16_391);
                assert_eq!(agfl.block_count, 6);
                assert_eq!(agfl.owner, ondisk::XFS_RMAP_OWN_AG);
            }
            other => panic!("unexpected rmap records: {other:?}"),
        }
    }
}
