// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Block filling operations for testing.
//!
//! This module provides utilities to populate blocks with specific patterns
//! for verification in round-trip tests.

use crate::BlockId;

use super::PhysicalLayout;

use aligned_vec::{AVec, avec};
use anyhow::{Result, anyhow};
use cudarc::runtime::sys::{cudaMemcpy, cudaMemcpyKind};
use dynamo_memory::StorageKind;

use std::{
    fs::File,
    io::{Seek, Write},
    mem::ManuallyDrop,
    ops::Range,
    os::fd::FromRawFd,
};

/// Fill strategy for block memory.
#[derive(Debug, Clone, Copy)]
pub enum FillPattern {
    /// Fill with a constant byte value
    Constant(u8),

    /// Fill with a sequential pattern: block_id + layer_id + offset % 256
    Sequential,
}

/// Fill blocks in a physical layout with a specific pattern.
///
/// This operation directly writes to memory and should only be used on
/// local layouts. Remote layouts cannot be filled directly.
///
/// # Arguments
/// * `layout` - The physical layout containing the blocks
/// * `block_ids` - List of block IDs to fill
/// * `pattern` - Fill pattern to use
///
/// # Errors
/// Returns an error if:
/// - Layout is remote (cannot fill remote memory directly)
/// - Block IDs are out of range
/// - Memory access fails
pub fn fill_blocks(
    layout: &PhysicalLayout,
    block_ids: &[BlockId],
    pattern: FillPattern,
) -> Result<()> {
    // Can only fill local layouts
    let config = layout.layout().config();
    let num_layers = config.num_layers;
    let outer_dim = config.outer_dim;

    for &block_id in block_ids {
        if block_id >= config.num_blocks as BlockId {
            return Err(anyhow!("Block ID {} out of range", block_id));
        }

        // Fill all layers and outer dimensions for this block
        for layer_id in 0..num_layers {
            for outer_id in 0..outer_dim {
                let region = layout.memory_region(block_id, layer_id, outer_id)?;

                match layout.location() {
                    StorageKind::System | StorageKind::Pinned => {
                        fill_memory_region(
                            region.addr(),
                            region.size(),
                            block_id,
                            layer_id,
                            pattern,
                        )?;
                    }
                    StorageKind::Device(_) => {
                        let system_region: Vec<u8> = vec![0; region.size()];
                        fill_memory_region(
                            system_region.as_ptr() as usize,
                            system_region.len(),
                            block_id,
                            layer_id,
                            pattern,
                        )?;
                        let err = unsafe {
                            cudaMemcpy(
                                region.addr() as *mut std::ffi::c_void,
                                system_region.as_ptr() as *const std::ffi::c_void,
                                region.size(),
                                cudaMemcpyKind::cudaMemcpyHostToDevice,
                            )
                        };
                        if err != cudarc::runtime::sys::cudaError::cudaSuccess {
                            return Err(anyhow!("cudaMemcpy H2D failed in fill_blocks: {:?}", err));
                        }
                    }
                    StorageKind::Disk(fd) => {
                        let system_region: AVec<u8, _> = avec![[4096]| 0; region.size()];
                        fill_memory_region(
                            system_region.as_ptr() as usize,
                            system_region.len(),
                            block_id,
                            layer_id,
                            pattern,
                        )?;

                        let mut file = ManuallyDrop::new(unsafe { File::from_raw_fd(fd as i32) });

                        file.seek(std::io::SeekFrom::Start(region.addr() as u64))?;
                        file.write_all(&system_region)?;
                        file.sync_all()?;
                        file.flush()?;
                    }
                }
            }
        }
    }

    Ok(())
}

/// Fill a subset of layers in blocks with a specific pattern.
///
/// # Arguments
/// * `layout` - The physical layout containing the blocks
/// * `block_ids` - List of block IDs to fill
/// * `layer_range` - Range of layers to fill
/// * `pattern` - Fill pattern to use
pub fn fill_layers(
    layout: &PhysicalLayout,
    block_ids: &[usize],
    layer_range: Range<usize>,
    pattern: FillPattern,
) -> Result<()> {
    let config = layout.layout().config();
    let num_layers = config.num_layers;
    let outer_dim = config.outer_dim;

    if layer_range.end > num_layers {
        return Err(anyhow!(
            "Layer range {:?} exceeds num_layers {}",
            layer_range,
            num_layers
        ));
    }

    for &block_id in block_ids {
        if block_id >= config.num_blocks {
            return Err(anyhow!("Block ID {} out of range", block_id));
        }

        // Fill specified layers and all outer dimensions
        for layer_id in layer_range.clone() {
            for outer_id in 0..outer_dim {
                let region = layout.memory_region(block_id, layer_id, outer_id)?;

                match layout.location() {
                    StorageKind::System | StorageKind::Pinned => {
                        fill_memory_region(
                            region.addr(),
                            region.size(),
                            block_id,
                            layer_id,
                            pattern,
                        )?;
                    }
                    StorageKind::Device(_) | StorageKind::Disk(_) => {
                        return Err(anyhow!(
                            "fill_layers only supports host-accessible storage (System/Pinned)"
                        ));
                    }
                }
            }
        }
    }

    Ok(())
}

/// Fill a memory region with the specified pattern.
///
/// # Safety
/// This function performs unsafe memory writes. The caller must ensure:
/// - The memory region is valid and accessible
/// - No other references exist to this memory
fn fill_memory_region(
    addr: usize,
    size: usize,
    block_id: BlockId,
    layer_id: usize,
    pattern: FillPattern,
) -> Result<()> {
    unsafe {
        let ptr = addr as *mut u8;
        match pattern {
            FillPattern::Constant(value) => {
                std::ptr::write_bytes(ptr, value, size);
            }
            FillPattern::Sequential => {
                for offset in 0..size {
                    let value = ((block_id + layer_id + offset) % 256) as u8;
                    ptr.add(offset).write(value);
                }
            }
        }
    }
    Ok(())
}

#[cfg(all(test, feature = "testing-kvbm"))]
mod tests {
    use super::super::tests::*;
    use super::*;

    #[test]
    fn test_fill_blocks_constant() {
        let physical = builder(2)
            .fully_contiguous()
            .allocate_system()
            .build()
            .unwrap();

        fill_blocks(&physical, &[0, 1], FillPattern::Constant(42)).unwrap();

        // Verify all bytes are set to 42
        assert!(unsafe {
            physical
                .memory_region(0, 0, 0)
                .unwrap()
                .as_slice()
                .unwrap()
                .iter()
                .all(|&b| b == 42)
        });
    }

    #[test]
    fn test_fill_blocks_sequential() {
        let physical = builder(2)
            .fully_contiguous()
            .allocate_system()
            .build()
            .unwrap();

        fill_blocks(&physical, &[0, 1], FillPattern::Sequential).unwrap();

        let mr = physical.memory_region(0, 0, 0).unwrap();
        let mr_slice = unsafe { mr.as_slice().unwrap() };

        // Verify pattern is applied (spot check a few bytes)
        let first_byte = mr_slice[0];
        let second_byte = mr_slice[1];
        assert_eq!(first_byte, 0);
        assert_eq!(second_byte, first_byte.wrapping_add(1));

        let mr = physical.memory_region(1, 1, 0).unwrap();
        let mr_slice = unsafe { mr.as_slice().unwrap() };

        let first_byte = mr_slice[0];
        let second_byte = mr_slice[1];
        assert_eq!(first_byte, 2);
        assert_eq!(second_byte, first_byte.wrapping_add(1));
    }

    #[test]
    fn test_fill_layers() {
        let physical = builder(2)
            .fully_contiguous()
            .allocate_system()
            .build()
            .unwrap();

        // Fill only layer 0
        fill_layers(&physical, &[0], 0..1, FillPattern::Constant(0)).unwrap();
        fill_layers(&physical, &[0], 1..2, FillPattern::Constant(1)).unwrap();
        fill_layers(&physical, &[1], 0..1, FillPattern::Constant(100)).unwrap();
        fill_layers(&physical, &[1], 1..2, FillPattern::Constant(101)).unwrap();

        let mr_00 = unsafe { physical.memory_region(0, 0, 0).unwrap().as_slice().unwrap()[0] };
        let mr_01 = unsafe { physical.memory_region(0, 1, 0).unwrap().as_slice().unwrap()[0] };
        let mr_10 = unsafe { physical.memory_region(1, 0, 0).unwrap().as_slice().unwrap()[0] };
        let mr_11 = unsafe { physical.memory_region(1, 1, 0).unwrap().as_slice().unwrap()[0] };
        assert_eq!(mr_00, 0);
        assert_eq!(mr_01, 1);
        assert_eq!(mr_10, 100);
        assert_eq!(mr_11, 101);
    }
}
