#[cfg(target_arch = "aarch64")]
use crate::constants::{BR_X16, LDR_X16_LITERAL_8};
use crate::error::SigHookError;
use crate::memory::{flush_instruction_cache, last_errno};
use libc::c_void;
use std::ptr::null_mut;

pub(crate) fn create_original_trampoline(
    address: u64,
    original_bytes: &[u8],
    step_len: u8,
) -> Result<u64, SigHookError> {
    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    if page_size <= 0 {
        return Err(SigHookError::PageSizeUnavailable);
    }

    let page_size = page_size as usize;

    let memory = unsafe {
        libc::mmap(
            null_mut(),
            page_size,
            libc::PROT_READ | libc::PROT_WRITE,
            libc::MAP_PRIVATE | libc::MAP_ANON,
            -1,
            0,
        )
    };

    if memory == libc::MAP_FAILED {
        return Err(SigHookError::MmapFailed {
            errno: last_errno(),
        });
    }

    let base = memory as usize;
    let next_pc = address.wrapping_add(step_len as u64);

    #[cfg(target_arch = "aarch64")]
    {
        if original_bytes.len() != 4 {
            return Err(SigHookError::InvalidAddress);
        }

        let mut insn = [0u8; 4];
        insn.copy_from_slice(original_bytes);
        let original_opcode = u32::from_le_bytes(insn);

        unsafe {
            std::ptr::write_unaligned(base as *mut u32, original_opcode.to_le());
            std::ptr::write_unaligned((base + 4) as *mut u32, LDR_X16_LITERAL_8.to_le());
            std::ptr::write_unaligned((base + 8) as *mut u32, BR_X16.to_le());
            std::ptr::write_unaligned((base + 12) as *mut u64, next_pc.to_le());
        }

        flush_instruction_cache(memory, 20);
    }

    #[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
    {
        if original_bytes.is_empty() {
            return Err(SigHookError::InvalidAddress);
        }

        unsafe {
            std::ptr::copy_nonoverlapping(
                original_bytes.as_ptr(),
                base as *mut u8,
                original_bytes.len(),
            );
        }

        let jmp_site = base + original_bytes.len();
        if let Ok(rel_jmp) = crate::memory::encode_jmp_rel32(jmp_site as u64, next_pc) {
            unsafe {
                std::ptr::copy_nonoverlapping(rel_jmp.as_ptr(), jmp_site as *mut u8, rel_jmp.len());
            }
            flush_instruction_cache(memory, original_bytes.len() + rel_jmp.len());
        } else {
            let abs = encode_abs_jmp_indirect(next_pc);
            unsafe {
                std::ptr::copy_nonoverlapping(abs.as_ptr(), jmp_site as *mut u8, abs.len());
            }
            flush_instruction_cache(memory, original_bytes.len() + abs.len());
        }
    }

    unsafe {
        if libc::mprotect(memory, page_size, libc::PROT_READ | libc::PROT_EXEC) != 0 {
            return Err(SigHookError::TrampolineProtectFailed {
                errno: last_errno(),
            });
        }
    }

    Ok(base as u64)
}

pub(crate) unsafe fn free_original_trampoline(trampoline_pc: u64) {
    if trampoline_pc == 0 {
        return;
    }

    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    if page_size <= 0 {
        return;
    }

    unsafe {
        let _ = libc::munmap(trampoline_pc as *mut c_void, page_size as usize);
    }
}

#[cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "macos")))]
fn encode_abs_jmp_indirect(to_address: u64) -> [u8; 14] {
    let mut bytes = [0u8; 14];
    bytes[0] = 0xFF;
    bytes[1] = 0x25;
    bytes[2] = 0x00;
    bytes[3] = 0x00;
    bytes[4] = 0x00;
    bytes[5] = 0x00;
    bytes[6..14].copy_from_slice(&to_address.to_le_bytes());
    bytes
}
